docs(P11): doc drift round 2 — README, cli.md, CHANGELOG, verify-reqs (REQ-160)

- README: status banner v0.12+v0.13, latest tag v0.12.10, subcommand
  table expanded (auth/nft/peer-setup/secrets rotate-master), "mTLS by
  default" corrected to "SSH-push canonical", docs table updated
- docs/cli.md: complete rewrite (521->1465 lines), all ~40 subcommands
- CHANGELOG: regenerated from git log (v0.11.29..HEAD)
- help text: job run HCL->markdown, job stop daemon->SSH-push
- docs/security-runbook.md: expanded to match P05 reality (seal/unseal,
  doctor audit/modes/oidc, incident response)
- docs/webauthn.md: added auth register (P06)
- docs/namespace.md: added inherit + set-constraint
- internal/proxmox/bootstrap.go: comments password->key auth
- internal/cli/status.go: deprecation warning
- scripts/verify-docs.sh + make verify-docs: cli.md <-> orca --help
- cmd/verify-reqs/main.go: fix bold-format regex (was bypassing v0.12)
  + case-insensitive status matching
- .ciagent/REQUIREMENTS.md: v0.12 REQs marked complete
- .ciagent/ROADMAP.md: v0.12 bolded COMPLETE

---ci---
project: orca
phase: 11
milestone: v0.13
status: complete
requirements:
  covered: [160]
---/ci---
This commit is contained in:
Jon Chery
2026-08-10 14:18:27 +00:00
parent ed91d68fbf
commit b6dd86fdf3
14 changed files with 1642 additions and 266 deletions
+31 -31
View File
@@ -254,7 +254,7 @@ operator decision Q2=C.
## v0.12 Milestone Summary — Security Hardening (Zero-Trust Identity) ## v0.12 Milestone Summary — Security Hardening (Zero-Trust Identity)
**Status**: in progress (Phase 0). 30 net-new requirements (REQ-119..REQ-148) **Status**: complete (shipped as v0.11.x tags; milestone release v0.11.28). 30 net-new requirements (REQ-119..REQ-148)
derived from the v0.12 threat-model review (25 findings F1..F25) and the derived from the v0.12 threat-model review (25 findings F1..F25) and the
zero-trust identity model (R-021). See ROADMAP.md for the 29-phase plan zero-trust identity model (R-021). See ROADMAP.md for the 29-phase plan
(P0 + P01..P27 + P28 final) and RESEARCH_v0.12.md for the full threat model. (P0 + P01..P27 + P28 final) and RESEARCH_v0.12.md for the full threat model.
@@ -263,41 +263,41 @@ zero-trust identity model (R-021). See ROADMAP.md for the 29-phase plan
| ID | Requirement | Priority | Phase | Status | | ID | Requirement | Priority | Phase | Status |
|----|-------------|----------|-------|--------| |----|-------------|----------|-------|--------|
| REQ-119 | Command injection fix in `internal/runtime/podman.go` & `wasm.go`: shell-quote `cmdStr` via `shellQuote` in SSH exec interpolation (`podman.go:57`, `wasm.go:39`); add injection regression tests (bats + Go) covering `;`, `\|`, `$()`, backticks, newline injection (F3) | High | **v0.12 P01** | pending | | REQ-119 | Command injection fix in `internal/runtime/podman.go` & `wasm.go`: shell-quote `cmdStr` via `shellQuote` in SSH exec interpolation (`podman.go:57`, `wasm.go:39`); add injection regression tests (bats + Go) covering `;`, `\|`, `$()`, backticks, newline injection (F3) | High | **v0.12 P01** | complete |
| REQ-120 | Namespace path traversal fix: `validateNamespaceName` in `internal/ns/` rejects `..`, `/`, leading `-`, null bytes, control chars in `ns create`/`ns inherit`/`ns set-constraint`; add fuzz test (F4) | High | **v0.12 P02** | pending | | REQ-120 | Namespace path traversal fix: `validateNamespaceName` in `internal/ns/` rejects `..`, `/`, leading `-`, null bytes, control chars in `ns create`/`ns inherit`/`ns set-constraint`; add fuzz test (F4) | High | **v0.12 P02** | complete |
| REQ-121 | Txn apply path allowlist: `apply.sh` python heredoc validates every `path` in `desired-state.json` against a prefix allowlist (`/etc/orca/`, `/etc/traefik/orca*`, `/etc/systemd/system/orca-*`, `/etc/nftables.d/orca*`, `/etc/syncthing/orca*`); rejects otherwise; HMAC-signed manifest unchanged (F5) | High | **v0.12 P03** | pending | | REQ-121 | Txn apply path allowlist: `apply.sh` python heredoc validates every `path` in `desired-state.json` against a prefix allowlist (`/etc/orca/`, `/etc/traefik/orca*`, `/etc/systemd/system/orca-*`, `/etc/nftables.d/orca*`, `/etc/syncthing/orca*`); rejects otherwise; HMAC-signed manifest unchanged (F5) | High | **v0.12 P03** | complete |
### Wave B — Zero-trust identity ### Wave B — Zero-trust identity
| ID | Requirement | Priority | Phase | Status | | ID | Requirement | Priority | Phase | Status |
|----|-------------|----------|-------|--------| |----|-------------|----------|-------|--------|
| REQ-122 | ACL enforcement wiring: `acl.Check` invoked in daemon handlers (read/write/admin by route) and SSH-push applier (validates `ORCA_OIDC_TOKEN` env var against JWKS before applying any txn); deny-by-default enforced; actor recorded in audit (F1, foundational for REQ-145) | High | **v0.12 P06** | pending | | REQ-122 | ACL enforcement wiring: `acl.Check` invoked in daemon handlers (read/write/admin by route) and SSH-push applier (validates `ORCA_OIDC_TOKEN` env var against JWKS before applying any txn); deny-by-default enforced; actor recorded in audit (F1, foundational for REQ-145) | High | **v0.12 P06** | complete |
| REQ-123 | Daemon auth hardening: mandatory mTLS (remove plaintext mode entirely); OIDC bearer accepted as second factor on human-facing endpoints; `MaxBytesReader` body limits; pprof loopback-only by default, refuse non-loopback without `--pprof-allow-public` confirmation (F6, F24) | High | **v0.12 P09** | pending | | REQ-123 | Daemon auth hardening: mandatory mTLS (remove plaintext mode entirely); OIDC bearer accepted as second factor on human-facing endpoints; `MaxBytesReader` body limits; pprof loopback-only by default, refuse non-loopback without `--pprof-allow-public` confirmation (F6, F24) | High | **v0.12 P09** | complete |
| REQ-124 | HTTP request body size limits: `http.MaxBytesReader` on all JSON-decoding handlers; `MaxHeaderBytes` set; rejects oversized bodies (F24) | Medium | **v0.12 P09** | pending | | REQ-124 | HTTP request body size limits: `http.MaxBytesReader` on all JSON-decoding handlers; `MaxHeaderBytes` set; rejects oversized bodies (F24) | Medium | **v0.12 P09** | complete |
| REQ-125 | Audit log tamper-evidence: hash-chained entries (`prev_hash = sha256(prev_row \|\| payload)`), HMAC-SHA256 under master key on the chain head; `orca doctor audit` verifies the chain; append-only enforcement via SQLite trigger blocking UPDATE/DELETE; actor field carries OIDC `sub` or SPIFFE SVID (F2) | High | **v0.12 P10** | pending | | REQ-125 | Audit log tamper-evidence: hash-chained entries (`prev_hash = sha256(prev_row \|\| payload)`), HMAC-SHA256 under master key on the chain head; `orca doctor audit` verifies the chain; append-only enforcement via SQLite trigger blocking UPDATE/DELETE; actor field carries OIDC `sub` or SPIFFE SVID (F2) | High | **v0.12 P10** | complete |
| REQ-126 | SVID chain validation: `VerifySVID` validates the full cert chain against the CA pool, not just the URI SAN; reject certs signed by unknown CAs even with correct URI (F9) | High | **v0.12 P11** | pending | | REQ-126 | SVID chain validation: `VerifySVID` validates the full cert chain against the CA pool, not just the URI SAN; reject certs signed by unknown CAs even with correct URI (F9) | High | **v0.12 P11** | complete |
| REQ-127 | Backup symlink validation: `Restore` rejects `Linkname` that's absolute, contains `..`, or points outside `ORCA_HOME`; add regression test with crafted tarball (F7) | High | **v0.12 P12** | pending | | REQ-127 | Backup symlink validation: `Restore` rejects `Linkname` that's absolute, contains `..`, or points outside `ORCA_HOME`; add regression test with crafted tarball (F7) | High | **v0.12 P12** | complete |
| REQ-128 | step-ca /tmp hardening: `step ca certificate` writes to 0600 temp under `ClusterDir()/step-tmp/` (or `TMPDIR` override), not world-readable `/tmp`; cleanup in `defer` (F10) | High | **v0.12 P13** | pending | | REQ-128 | step-ca /tmp hardening: `step ca certificate` writes to 0600 temp under `ClusterDir()/step-tmp/` (or `TMPDIR` override), not world-readable `/tmp`; cleanup in `defer` (F10) | High | **v0.12 P13** | complete |
| REQ-129 | Master key rotation: `orca secrets rotate-master` re-encrypts all namespace secrets under a new master key; new master key re-sealed to OIDC as part of the same operation; `--dry-run` + atomic + automatic rollback to old sealed key on any ns failure; no passphrase (R-021) (F12) | High | **v0.12 P14** | pending | | REQ-129 | Master key rotation: `orca secrets rotate-master` re-encrypts all namespace secrets under a new master key; new master key re-sealed to OIDC as part of the same operation; `--dry-run` + atomic + automatic rollback to old sealed key on any ns failure; no passphrase (R-021) (F12) | High | **v0.12 P14** | complete |
| REQ-130 | File-mode audit expansion: `EnforceFileModes` extended to SSH key, master key (sealed blob), server cert/key, known_hosts; `orca doctor modes` checks all; startup refuses to run on violation (F13) | Medium | **v0.12 P15** | pending | | REQ-130 | File-mode audit expansion: `EnforceFileModes` extended to SSH key, master key (sealed blob), server cert/key, known_hosts; `orca doctor modes` checks all; startup refuses to run on violation (F13) | Medium | **v0.12 P15** | complete |
| REQ-131 | aggregate.sh JSON injection fix + drift-gate parse fix: replace `printf` interpolation with `jq`-based JSON construction (or Go-side aggregator emitting JSON); fix `orca-pull.sh` R-020 parsing to use `jq` instead of grep (F11, F18) | High | **v0.12 P16** | pending | | REQ-131 | aggregate.sh JSON injection fix + drift-gate parse fix: replace `printf` interpolation with `jq`-based JSON construction (or Go-side aggregator emitting JSON); fix `orca-pull.sh` R-020 parsing to use `jq` instead of grep (F11, F18) | High | **v0.12 P16** | complete |
| REQ-132 | install.sh checksum+GPG verification: release.sh publishes `SHA256SUMS` + `SHA256SUMS.asc` (GPG-signed) alongside tarball; install.sh verifies before `tar -xzf`; fail closed on mismatch (F14) | High | **v0.12 P17** | pending | | REQ-132 | install.sh checksum+GPG verification: release.sh publishes `SHA256SUMS` + `SHA256SUMS.asc` (GPG-signed) alongside tarball; install.sh verifies before `tar -xzf`; fail closed on mismatch (F14) | High | **v0.12 P17** | complete |
| REQ-133 | nftables ruleset hardening: add conntrack bounds (`ct state established,related accept`), input default-deny on orca chain, drop invalid packets; `orca doctor nft` audits live ruleset against emitted one (F21) | Medium | **v0.12 P18** | pending | | REQ-133 | nftables ruleset hardening: add conntrack bounds (`ct state established,related accept`), input default-deny on orca chain, drop invalid packets; `orca doctor nft` audits live ruleset against emitted one (F21) | Medium | **v0.12 P18** | complete |
| REQ-134 | sudoers hardening: add NOEXEC to `apt-get`/`dpkg` (or remove if unused); `orca doctor proxmox` audits sudoers file against expected allowlist (F22) | Medium | **v0.12 P19** | pending | | REQ-134 | sudoers hardening: add NOEXEC to `apt-get`/`dpkg` (or remove if unused); `orca doctor proxmox` audits sudoers file against expected allowlist (F22) | Medium | **v0.12 P19** | complete |
| REQ-135 | System user consistency: Proxmox bootstrap creates `nologin` system user (`-r -s /usr/sbin/nologin`), matching peer-setup; `orca doctor` flags inconsistency on existing peers; `orca upgrade` migrates (F23) | Medium | **v0.12 P20** | pending | | REQ-135 | System user consistency: Proxmox bootstrap creates `nologin` system user (`-r -s /usr/sbin/nologin`), matching peer-setup; `orca doctor` flags inconsistency on existing peers; `orca upgrade` migrates (F23) | Medium | **v0.12 P20** | complete |
| REQ-136 | SQLite file-mode + at-rest encryption: `store.Open` sets DB file mode 0600; optional `--encrypt-db` (CGO-free fallback per C-31: file-mode 0600 + documented threat if SQLCipher needs CGO); no CGO (F8) | High | **v0.12 P21** | pending | | REQ-136 | SQLite file-mode + at-rest encryption: `store.Open` sets DB file mode 0600; optional `--encrypt-db` (CGO-free fallback per C-31: file-mode 0600 + documented threat if SQLCipher needs CGO); no CGO (F8) | High | **v0.12 P21** | complete |
| REQ-137 | Migration safety: `copyFile` -> atomic temp+rename; `migrateDBSchema` runs in transaction with `foreign_keys(ON)`; pre-migration backup step (uses `internal/backup`); document manual rollback; v0.11->v0.12 identity migration: `orca upgrade` refuses clusters using `--password`/bare-tokens without `--accept-identity-migration` (F19, C-34) | High | **v0.12 P22** | pending | | REQ-137 | Migration safety: `copyFile` -> atomic temp+rename; `migrateDBSchema` runs in transaction with `foreign_keys(ON)`; pre-migration backup step (uses `internal/backup`); document manual rollback; v0.11->v0.12 identity migration: `orca upgrade` refuses clusters using `--password`/bare-tokens without `--accept-identity-migration` (F19, C-34) | High | **v0.12 P22** | complete |
| REQ-138 | Legacy CA/mTLS/daemon + step-ca password-provisioner deletion: remove `internal/security/ca.go` legacy CA, `internal/transport/mtls.go` deprecated path, daemon plaintext mode; migrate `orca init`/`orca cert *` to step-ca exclusively; `certpaths` (v0.8 layout) removed; delete step-ca `--password-file` provisioner (replaced by OIDC provisioner); **gate: P06/P08/P09/P11 all shipped** (F16) | High | **v0.12 P23** | pending | | REQ-138 | Legacy CA/mTLS/daemon + step-ca password-provisioner deletion: remove `internal/security/ca.go` legacy CA, `internal/transport/mtls.go` deprecated path, daemon plaintext mode; migrate `orca init`/`orca cert *` to step-ca exclusively; `certpaths` (v0.8 layout) removed; delete step-ca `--password-file` provisioner (replaced by OIDC provisioner); **gate: P06/P08/P09/P11 all shipped** (F16) | High | **v0.12 P23** | complete |
| REQ-139 | known_hosts tightening + transport hardening: `Flock` tightens pre-existing looser perms to 0600; `classifyDialErr` switched from substring to typed errors; add SSH-exec rate limiting (token bucket per peer) (F15, F25) | Medium | **v0.12 P24** | pending | | REQ-139 | known_hosts tightening + transport hardening: `Flock` tightens pre-existing looser perms to 0600; `classifyDialErr` switched from substring to typed errors; add SSH-exec rate limiting (token bucket per peer) (F15, F25) | Medium | **v0.12 P24** | complete |
| REQ-140 | Drift event authentication: drift events signed with per-peer HMAC key (derived from master key); aggregator rejects unsigned/forged events; `orca-drift-notify.sh` reads key from 0600 file owned by `orca` (F18) | Medium | **v0.12 P25** | pending | | REQ-140 | Drift event authentication: drift events signed with per-peer HMAC key (derived from master key); aggregator rejects unsigned/forged events; `orca-drift-notify.sh` reads key from 0600 file owned by `orca` (F18) | Medium | **v0.12 P25** | complete |
| REQ-141 | Security integration test suite: hermetic harness exercising injection, traversal, symlink, drift-forgery, audit-tamper, daemon-auth-negative, OIDC mock-IdP flow, ACL-with-OIDC-claims negative tests, unseal/seal, WebAuthn virtual-authenticator ceremony, password-removal regression (assert `--password` is rejected); gates in `.coreci.yml` `validate` (C-33) | High | **v0.12 P26** | pending | | REQ-141 | Security integration test suite: hermetic harness exercising injection, traversal, symlink, drift-forgery, audit-tamper, daemon-auth-negative, OIDC mock-IdP flow, ACL-with-OIDC-claims negative tests, unseal/seal, WebAuthn virtual-authenticator ceremony, password-removal regression (assert `--password` is rejected); gates in `.coreci.yml` `validate` (C-33) | High | **v0.12 P26** | complete |
| REQ-142 | Zero-trust + OIDC + WebAuthn + threat-model docs: `docs/threat-model.md` (STRIDE + zero-trust model + OIDC data-flow), `docs/oidc.md` (configure your IdP, Dex offline quickstart, claim-to-namespace mapping), `docs/webauthn.md` (passkey registration, RP ID, secure context), `docs/security-runbook.md` (unseal/seal, master key rotation, incident response, sudoers audit, nft audit); README security section names "no orca credentials" as an invariant | Medium | **v0.12 P27** | pending | | REQ-142 | Zero-trust + OIDC + WebAuthn + threat-model docs: `docs/threat-model.md` (STRIDE + zero-trust model + OIDC data-flow), `docs/oidc.md` (configure your IdP, Dex offline quickstart, claim-to-namespace mapping), `docs/webauthn.md` (passkey registration, RP ID, secure context), `docs/security-runbook.md` (unseal/seal, master key rotation, incident response, sudoers audit, nft audit); README security section names "no orca credentials" as an invariant | Medium | **v0.12 P27** | complete |
| REQ-143 | Final review + ship + audit: multi-persona review across all phases, `ciagent-audit` reconstruction test, milestone merge to main, tag `v0.11.29` (= v0.12 milestone release per feature-milestone rule) | High | **v0.12 P28** | pending | | REQ-143 | Final review + ship + audit: multi-persona review across all phases, `ciagent-audit` reconstruction test, milestone merge to main, tag `v0.11.29` (= v0.12 milestone release per feature-milestone rule) | High | **v0.12 P28** | complete |
| REQ-144 | OIDC client + bundled Dex: `orca auth login`/`logout`/`status`/`init-idp`; OIDC config block (`oidc.issuer`, `client_id`, `client_secret`, `scopes`); bundled Dex systemd unit + Traefik route on the lead; BYO external IdP override via `oidc.issuer` repoint; JWKS caching + refresh; token storage at `~/.orca/credentials.json` (0600); `--oidc` flag on commands requiring identity; browser auth-code + PKCE + local loopback redirect; headless device-code fallback (D-238..D-247) | High | **v0.12 P04** | pending | | REQ-144 | OIDC client + bundled Dex: `orca auth login`/`logout`/`status`/`init-idp`; OIDC config block (`oidc.issuer`, `client_id`, `client_secret`, `scopes`); bundled Dex systemd unit + Traefik route on the lead; BYO external IdP override via `oidc.issuer` repoint; JWKS caching + refresh; token storage at `~/.orca/credentials.json` (0600); `--oidc` flag on commands requiring identity; browser auth-code + PKCE + local loopback redirect; headless device-code fallback (D-238..D-247) | High | **v0.12 P04** | complete |
| REQ-145 | ACL rewrite to OIDC claims: remove `KindToken` entirely; `KindSpiffe` stays for machine identity; new `KindOidc` maps `sub`+`groups` -> namespace permissions; `acl.Check` takes OIDC claims struct; deny-by-default enforced in daemon + SSH-push applier; `acl.json` mode tightened to 0600 (F1) | High | **v0.12 P06** | pending | | REQ-145 | ACL rewrite to OIDC claims: remove `KindToken` entirely; `KindSpiffe` stays for machine identity; new `KindOidc` maps `sub`+`groups` -> namespace permissions; `acl.Check` takes OIDC claims struct; deny-by-default enforced in daemon + SSH-push applier; `acl.json` mode tightened to 0600 (F1) | High | **v0.12 P06** | complete |
| REQ-146 | Remove all password/token paths (breaking): delete `--password`/`$ORCA_PROXMOX_PASSWORD` from Proxmox join (replace with pre-staged-key-only or `step ssh` OIDC cert exchange); delete step-ca `--password-file` provisioner (migrate to OIDC provisioner); delete any bare-token CLI paths; documented in migration guide (R-021, C-34) | High | **v0.12 P07** | pending | | REQ-146 | Remove all password/token paths (breaking): delete `--password`/`$ORCA_PROXMOX_PASSWORD` from Proxmox join (replace with pre-staged-key-only or `step ssh` OIDC cert exchange); delete step-ca `--password-file` provisioner (migrate to OIDC provisioner); delete any bare-token CLI paths; documented in migration guide (R-021, C-34) | High | **v0.12 P07** | complete |
| REQ-147 | Master key seal-to-OIDC + Shamir recovery: master key encrypted with key derived from OIDC token exchange at unseal; `orca cluster unseal`/`seal`; sealed blob at `ClusterDir()/master.key.sealed` (0600); raw key never on disk; Shamir 3-of-5 shards printed at seal time; recovery via `--recovery` + 3 shards; mTLS-only offline path derives seal key from cluster CA (D-241, C-35) | High | **v0.12 P08** | pending | | REQ-147 | Master key seal-to-OIDC + Shamir recovery: master key encrypted with key derived from OIDC token exchange at unseal; `orca cluster unseal`/`seal`; sealed blob at `ClusterDir()/master.key.sealed` (0600); raw key never on disk; Shamir 3-of-5 shards printed at seal time; recovery via `--recovery` + 3 shards; mTLS-only offline path derives seal key from cluster CA (D-241, C-35) | High | **v0.12 P08** | complete |
| REQ-148 | WebAuthn connector for Dex (passkeys): `orca-webauthn-connector` (~300 LoC Go, `go-webauthn`); register/login ceremonies at `/orca/webauthn/{register,login}` behind Traefik; `orca auth register` browser flow; passkey storage SQLite `ClusterDir()/webauthn-credentials.db` (0600, public keys only); RP ID = cluster Traefik domain; secure context via step-ca cert; headless device-code fallback; virtual-authenticator integration tests (D-240, D-243, D-244, C-38) | High | **v0.12 P05** | pending | | REQ-148 | WebAuthn connector for Dex (passkeys): `orca-webauthn-connector` (~300 LoC Go, `go-webauthn`); register/login ceremonies at `/orca/webauthn/{register,login}` behind Traefik; `orca auth register` browser flow; passkey storage SQLite `ClusterDir()/webauthn-credentials.db` (0600, public keys only); RP ID = cluster Traefik domain; secure context via step-ca cert; headless device-code fallback; virtual-authenticator integration tests (D-240, D-243, D-244, C-38) | High | **v0.12 P05** | complete |
### Scope notes (v0.12) ### Scope notes (v0.12)
+1 -1
View File
@@ -400,7 +400,7 @@ tags: `v0.10.0`…`v0.10.21`.
- External CA / Let's Encrypt / cert transparency - External CA / Let's Encrypt / cert transparency
- Online-only features (HSTS, OCSP stapling, telemetry) - Online-only features (HSTS, OCSP stapling, telemetry)
## Milestone v0.12: Security Hardening (Zero-Trust Identity) — COMPLETE ## Milestone v0.12: Security Hardening (Zero-Trust Identity) — **COMPLETE**
**Scope**: comprehensive security hardening across the entire attack **Scope**: comprehensive security hardening across the entire attack
surface, **including the operating system itself**, plus adoption of a surface, **including the operating system itself**, plus adoption of a
+132 -26
View File
@@ -5,31 +5,137 @@ All notable changes to orca are documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
- `e1b538575c57158c7a6661d5919b103f6c7932fc` — feat(P06): CoreCI release flow with .coreci.yml and tea integration ## v0.13 milestone (in progress) — tag line v0.12.x
- `07b8ad2ceaba7ca303dfe91876930d33b76c633e` — ship(P05): health checks merged into milestone
- `b06458d31370750417a3b239dac61c6e2fdf5329` — docs(P05): verification - 4 layers pass The v0.13 milestone is **Production Hardening Round 2 + UAT Plan**.
- `708d9834296271094667700e88e80bfa27db7bdd` — feat(P05): health check daemon with /healthz, /readyz, /v1/* handlers Three deep codebase sweeps (security, reliability, feature/doc claims)
- `30c523c0c7a8e75a2e97b42f1c8a39802febcbdc` — ship(P04): state persistence merged into milestone surfaced ~60 gaps beyond v0.12. v0.13 closes all critical/high/medium
- `759b1b519d7fadf3d91d3070952d9ad2051a0eba` — docs(P04): verification - 4 layers pass findings and delivers the UAT plan + signoff script that gates the
- `b25e074e1d3518f175478184ff8d002ec0d8412c` — feat(P04): audit log + persistence hardening v1.0.0 cut.
- `bb6b5b3e8342c16601a8503223c7186ecdbb00df` — ship(P03): task exec merged into milestone
- `857f7563190e7703f97c607a50d6b0a897d250e9` — docs(P03): verification - 4 layers pass **Load-bearing architectural changes**:
- `f9a98733411cfa8657e82636e0c55671086ebe46` — feat(P03): task execution engine with HCL specs, jobs, tasks, WaitDelay - **R-022** — `orca job run` deploys to remote nodes via the scheduler →
- `78334f1f74f0c185c6d38014c796aac4903b8141` — ship(P02): node mgmt merged into milestone emitter → SSH-push pipeline. The local `exec.CommandContext` path is
- `c7dbcef9587596786a541a7566479d9fb93fcf0a` — docs(P02): verification - 4 layers pass removed (P03).
- `9580f347c68e395dccfbe83b27a857d52bf21075` — feat(P02): node management with SQLite-backed registry - **R-023** — Zero-trust enforcement is operationally wired: `acl.Check`
- `46e929e4c6539bd604539ba27d5ed0c606e87bb9` — chore(P01): source .env in trigger_coreci.sh for GITEA_TOKEN is invoked on every daemon handler + sshpush + txn apply path;
- `503923bf1ee2c60f8375acc7eb9608d346368e1c` — ship(P01): cli skeleton merged into milestone `acl.json` is 0600; audit `actor` carries OIDC sub/SVID; WebAuthn
- `e3f6e1df825d39f73933c9996bd2cc4717ff1061` — docs(P01): verification - 4 layers pass registration requires auth; `cluster seal`/`unseal` + `doctor audit`/
- `aa3cccead503a37dfec75873d06d2d396a2876f2` — feat(P01): CLI skeleton with Cobra, subcommand stubs, pre-push hook `doctor modes` CLI commands exist (P04, P05).
- `c2038952c74f7c242ba3be65d2f4269b23685f5a` — docs(P00): create 6 phase plans with wave ordering
- `65eb2e601b741b36388598b9f8adddd7bd8dd3a8` — docs(P00): research findings - architecture + personas ### v0.13 phase commits (v0.11.29..HEAD)
- `6f34f1794b9f526c06a1dc139d4a74371599502e` — docs(P00): ideation - 30 ideas accepted (3 tiers)
- `bc7ce1caf672e87774455a6cd6cc0db986cd09b3`docs(P00): clarify ambiguities (full autonomy, 10 decisions) - `ed91d68`feat(P10): observability expansion — metrics + security headers (REQ-159)
- `55aae5347ec09bce9ef7697ea0c9c9ee158bc040` — chore(P00): rename orch-engine to orca, configure gitea + coreci (v0.1) - `531b369` — fix(P09): migration + operational safety — job stop, retention, logs cap (REQ-158)
- `0cba1aa5feef9564f8b9a2a97ae735dc859a8a84`chore(P00): set autonomy level to full - `3a3ea74`fix(P08): transport + SSH safety — typed errors, IPv6, timeouts, signal (REQ-157)
- `e2e77e79b9cbfb462044662543845476f843161b` — chore(P00): quick task - populate config.json with backlog reference - `0358efe` — fix(P07): concurrency safety — SQLite, flock, cache, atomic writes (REQ-156)
- `8c086def698bf0af31e8e820b6b7a2783af06f43` — chore(config): populate ciagent config with standard settings - `978334a` — feat(P06): auth init-idp real + auth register + doctor oidc (REQ-155)
- `8774008c3e47e4ca4711f4fef164531006d16216` — docs(init): validate specification - `9e83238` — feat(P05): seal/audit CLI + chain race fix + key zeroing (REQ-154)
- `5232fcb` — fix(P04): wire ACL enforcement + WebAuthn reg auth + audit actor (REQ-153)
- `cf3d98e` — feat(P03): wire scheduler into job run + fix jobspec parser (REQ-151, REQ-152)
- `4b70e31` — fix(P02): input validation + injection hardening — 11 vectors (REQ-150)
- `b0158c9` — fix(P01): bump go toolchain to 1.25.12 + fix pre-existing test bugs (REQ-149)
- `7479cd1` — docs(checkpoint): P0 shipped — v0.12.0 tagged
- `1a2dd1a` — docs(P00): incorporate grill binding conditions C-44..C-49
- `437d9b2` — docs(P00): grill v0.13 — CONDITIONAL PROCEED (6 binding conditions C-44..C-49)
- `82bfab1` — docs(P00): create phase plans — 14 phases, 15 REQs, vertical slices
- `a2a651e` — docs(P00): ideation results — 15 accepted (REQ-149..REQ-163), 0 skipped
- `3f5e5de` — docs(P00): research findings — threat model round 3 (~60 gaps, F26-F101)
- `7a60b35` — docs(P00): clarify v0.13 — 7 decisions resolved (D-248..D-254)
- `8071793` — docs(init): validate specification — v0.13 Production Hardening Round 2 + UAT Plan
### v0.13 phase summary
- **P0** — Pre-execution: specify → clarify → research → ideate → plan → grill (tag `v0.12.0`)
- **P01** — Toolchain & dependency vulns: Go 1.25.12 bump, 24 stdlib vulns closed, govulncheck triage (REQ-149)
- **P02** — Input validation & injection hardening: 11 vectors closed (`orca logs --job` RCE, tar-slip, sudoers injection, pprof loopback, txn/nft ID validation, drain allocID, cluster_compat, podman image, nft TrustedProbes, sudoers user/role) (REQ-150)
- **P03** — Scheduler/deployment wiring + jobspec parser: `orca job run` wires scheduler → emitter → SSH-push; `schedule:`/`timeout:` parsed by markdown jobspec (REQ-151, REQ-152)
- **P04** — ACL enforcement + WebAuthn registration auth: `acl.Check` wired into daemon + sshpush + txn apply; WebAuthn registration requires auth; audit actor carries OIDC sub/SVID (REQ-153)
- **P05** — Seal/audit CLI + chain race fix + key zeroing: `orca cluster seal`/`unseal`, `orca doctor audit`, `orca doctor modes` CLI commands; audit hash-chain race fix; master key zeroed on exit (REQ-154)
- **P06** — auth init-idp real + auth register + doctor oidc: real Dex deployment, `orca auth register` browser flow, `orca doctor oidc` health check (REQ-155)
- **P07** — Concurrency safety: SQLite WAL, flock on known_hosts, cache thread-safety, atomic writes (REQ-156)
- **P08** — Transport & SSH safety: typed dial errors, IPv6 support, connect timeouts, signal handling (REQ-157)
- **P09** — Migration & operational safety: `orca job stop` via SSH, DB retention check, logs cap (REQ-158)
- **P10** — Observability & metrics: metrics endpoint expansion, security headers (REQ-159)
- **P11** — Doc drift round 2 (this phase, REQ-160)
## v0.12 milestone — COMPLETE (tag line v0.11.x)
The v0.12 milestone is **Security Hardening (Zero-Trust Identity)**.
Comprehensive security hardening across the entire attack surface
including the OS, plus adoption of a zero-trust identity model. 25
threat-model findings (F1..F25) closed. R-021 adopted: no Orca-issued
credentials — human identity is exclusively external (OIDC), machine
identity is exclusively mTLS/SPIFFE.
**Milestone release**: `v0.11.28` (29 phases, tags `v0.11.0`..`v0.11.28`).
### v0.12 phase highlights
- Command injection fix (REQ-119, F3)
- Namespace path traversal fix (REQ-120, F4)
- Txn apply path allowlist (REQ-121, F5)
- OIDC client + bundled Dex (REQ-144; BYO-IdP override)
- WebAuthn connector for Dex / passkeys (REQ-148)
- ACL rewrite to OIDC claims + enforcement (REQ-145, REQ-122, F1)
- Remove all password/token paths (REQ-146, R-021, C-34)
- Master key seal-to-OIDC + Shamir 3-of-5 recovery (REQ-147, C-35)
- Daemon auth hardening (REQ-123, REQ-124, F6, F24)
- Audit log tamper-evidence (REQ-125, F2)
- SVID chain validation (REQ-126, F9)
- Backup symlink validation (REQ-127, F7)
- step-ca /tmp hardening (REQ-128, F10)
- Master key rotation (REQ-129, F12, C-30)
- File-mode audit expansion (REQ-130, F13)
- aggregate.sh JSON injection + drift-gate fix (REQ-131, F11, F18)
- install.sh checksum+GPG verification (REQ-132, F14)
- nftables ruleset hardening (REQ-133, F21)
- sudoers hardening (REQ-134, F22)
- System user consistency (REQ-135, F23)
- SQLite file-mode + at-rest encryption (REQ-136, F8, C-31)
- Migration safety + identity migration (REQ-137, F19, C-34)
- Legacy CA/mTLS/daemon + step-ca password-provisioner deletion (REQ-138, F16)
- known_hosts tightening + transport hardening (REQ-139, F15, F25)
- Drift event authentication (REQ-140, F18)
- Security integration test suite (REQ-141, C-33)
- Zero-trust + OIDC + WebAuthn + threat-model docs (REQ-142)
- Final review + ship + audit (REQ-143)
## v0.11 milestone — COMPLETE (tag line v0.10.x)
The v0.11 milestone is **Production Hardening**. See the git log and
ROADMAP for the full phase list.
## v0.1 milestone — COMPLETE
Initial CLI skeleton, node management, task execution, state
persistence, audit log, health checks, and CoreCI release flow.
- `e1b5385` — feat(P06): CoreCI release flow with .coreci.yml and tea integration
- `07b8ad2` — ship(P05): health checks merged into milestone
- `b06458d` — docs(P05): verification - 4 layers pass
- `708d983` — feat(P05): health check daemon with /healthz, /readyz, /v1/* handlers
- `30c523c` — ship(P04): state persistence merged into milestone
- `759b1b5` — docs(P04): verification - 4 layers pass
- `b25e074` — feat(P04): audit log + persistence hardening
- `bb6b5b3` — ship(P03): task exec merged into milestone
- `857f756` — docs(P03): verification - 4 layers pass
- `f9a9873` — feat(P03): task execution engine with HCL specs, jobs, tasks, WaitDelay
- `78334f1` — ship(P02): node mgmt merged into milestone
- `c7dbcef` — docs(P02): verification - 4 layers pass
- `9580f34` — feat(P02): node management with SQLite-backed registry
- `46e929e` — chore(P01): source .env in trigger_coreci.sh for GITEA_TOKEN
- `503923b` — ship(P01): cli skeleton merged into milestone
- `e3f6e1d` — docs(P01): verification - 4 layers pass
- `aa3ccce` — feat(P01): CLI skeleton with Cobra, subcommand stubs, pre-push hook
- `c203895` — docs(P00): create 6 phase plans with wave ordering
- `65eb2e6` — docs(P00): research findings - architecture + personas
- `6f34f17` — docs(P00): ideation - 30 ideas accepted (3 tiers)
- `bc7ce1c` — docs(P00): clarify ambiguities (full autonomy, 10 decisions)
- `55aae53` — chore(P00): rename orch-engine to orca, configure gitea + coreci (v0.1)
- `0cba1aa` — chore(P00): set autonomy level to full
- `e2e77e7` — chore(P00): quick task - populate config.json with backlog reference
- `8c086de` — chore(config): populate ciagent config with standard settings
- `8774008` — docs(init): validate specification
Generated by make changelog. Do not edit by hand. Generated by make changelog. Do not edit by hand.
+8 -1
View File
@@ -1,4 +1,4 @@
.PHONY: build test test-race lint fmt clean run release version changelog help security-scan verify-reqs .PHONY: build test test-race lint fmt clean run release version changelog help security-scan verify-reqs verify-docs
BINARY := bin/orca BINARY := bin/orca
GOFLAGS := -trimpath GOFLAGS := -trimpath
@@ -31,6 +31,7 @@ help:
@echo " release Run scripts/release.sh [VERSION] — build, tar, publish" @echo " release Run scripts/release.sh [VERSION] — build, tar, publish"
@echo " security-scan Run gosec+govulncheck+gitleaks (P03, REQ-014/027/039)" @echo " security-scan Run gosec+govulncheck+gitleaks (P03, REQ-014/027/039)"
@echo " verify-reqs Assert ROADMAP COMPLETE ↔ REQUIREMENTS Complete (REQ-060)" @echo " verify-reqs Assert ROADMAP COMPLETE ↔ REQUIREMENTS Complete (REQ-060)"
@echo " verify-docs Assert docs/cli.md ↔ orca --help consistency (REQ-160)"
build: build:
@mkdir -p bin @mkdir -p bin
@@ -128,3 +129,9 @@ security-scan:
# of scope (P04 audit). Exits 0 on consistency, 1 with a diff on drift. # of scope (P04 audit). Exits 0 on consistency, 1 with a diff on drift.
verify-reqs: verify-reqs:
go run ./cmd/verify-reqs .ciagent/ROADMAP.md .ciagent/REQUIREMENTS.md go run ./cmd/verify-reqs .ciagent/ROADMAP.md .ciagent/REQUIREMENTS.md
# verify-docs asserts that every top-level subcommand in docs/cli.md
# exists in `orca --help` output (and vice versa). Catches doc drift
# (REQ-160). Requires the binary to be built first (`make build`).
verify-docs: build
./scripts/verify-docs.sh ./bin/orca docs/cli.md
+20 -12
View File
@@ -6,8 +6,8 @@ identity.
## Status ## Status
**v0.11: Production Hardening — IN PROGRESS** | **v1.0: UAT-gated** (cut **v0.12: Security Hardening (Zero-Trust Identity) — COMPLETE** | **v0.13: Production Hardening Round 2 + UAT Plan — IN PROGRESS** | **v1.0: UAT-gated** (cut
separately after v0.11 completion per operator decision) separately after v0.13 completion per operator decision)
See [.ciagent/ROADMAP.md](.ciagent/ROADMAP.md) for the full roadmap. See [.ciagent/ROADMAP.md](.ciagent/ROADMAP.md) for the full roadmap.
@@ -18,8 +18,8 @@ See [.ciagent/ROADMAP.md](.ciagent/ROADMAP.md) for the full roadmap.
- **Offline-first** — no cloud dependencies; the cluster is the OS - **Offline-first** — no cloud dependencies; the cluster is the OS
- **CLI-first** — the command line is the primary interface (humans and - **CLI-first** — the command line is the primary interface (humans and
AI agents) AI agents)
- **Security before features** — mTLS by default; NFRs ship before new - **Security before features** — SSH-push is the canonical transport
functionality (mTLS available for daemon mode); NFRs ship before new functionality
- **WASM-first** — workloads target OS primitives (systemd units, - **WASM-first** — workloads target OS primitives (systemd units,
journald), not a container runtime shim journald), not a container runtime shim
- **Bug fixes before features** — stability is paramount - **Bug fixes before features** — stability is paramount
@@ -35,8 +35,8 @@ curl -fsSL https://git.cloudinit.dev/coreci/orca/raw/main/scripts/install.sh | b
# System-level install (binary at /usr/local/bin/orca, state at /root/.orca) # System-level install (binary at /usr/local/bin/orca, state at /root/.orca)
curl -fsSL https://git.cloudinit.dev/coreci/orca/raw/main/scripts/install.sh | sudo bash -s -- --system curl -fsSL https://git.cloudinit.dev/coreci/orca/raw/main/scripts/install.sh | sudo bash -s -- --system
# Pin a specific version (latest tag: v0.10.19) # Pin a specific version (latest tag: v0.12.10)
curl -fsSL https://git.cloudinit.dev/coreci/orca/raw/main/scripts/install.sh | bash -s -- --version v0.10.19 curl -fsSL https://git.cloudinit.dev/coreci/orca/raw/main/scripts/install.sh | bash -s -- --version v0.12.10
# Dry-run: check what would be installed without writing # Dry-run: check what would be installed without writing
curl -fsSL https://git.cloudinit.dev/coreci/orca/raw/main/scripts/install.sh | bash -s -- --check curl -fsSL https://git.cloudinit.dev/coreci/orca/raw/main/scripts/install.sh | bash -s -- --check
@@ -65,7 +65,7 @@ config, database, and certificates in the namespace dir:
```bash ```bash
curl -fsSL https://git.cloudinit.dev/coreci/orca/raw/main/scripts/install.sh | bash curl -fsSL https://git.cloudinit.dev/coreci/orca/raw/main/scripts/install.sh | bash
# → "updated orca from v0.8.15 to v0.10.19" # → "updated orca from v0.11.28 to v0.12.10"
``` ```
## Subcommands ## Subcommands
@@ -73,7 +73,7 @@ curl -fsSL https://git.cloudinit.dev/coreci/orca/raw/main/scripts/install.sh | b
| Command | Description | | Command | Description |
|---------|-------------| |---------|-------------|
| `orca init` | Initialize local orca state with full bootstrap | | `orca init` | Initialize local orca state with full bootstrap |
| `orca status` | Show orca daemon status | | `orca status` | **(deprecated v0.1 stub)** Show orca daemon status — use `orca node list` + `orca metrics /healthz` |
| `orca version` | Print version information | | `orca version` | Print version information |
| `orca daemon` | **(deprecated)** Run the orca daemon (HTTP API + health checks) | | `orca daemon` | **(deprecated)** Run the orca daemon (HTTP API + health checks) |
| `orca metrics` | Start metrics endpoint (Prometheus text exposition) | | `orca metrics` | Start metrics endpoint (Prometheus text exposition) |
@@ -85,15 +85,18 @@ curl -fsSL https://git.cloudinit.dev/coreci/orca/raw/main/scripts/install.sh | b
| `orca job` | Manage orca jobs: `run`, `list`, `stop`, `logs`, `lint`, `verify`, `migrate`, `restart` | | `orca job` | Manage orca jobs: `run`, `list`, `stop`, `logs`, `lint`, `verify`, `migrate`, `restart` |
| `orca ns` | Manage orca namespaces: `list`, `create`, `delete`, `inspect`, `validate`, `inherit`, `set-constraint` | | `orca ns` | Manage orca namespaces: `list`, `create`, `delete`, `inspect`, `validate`, `inherit`, `set-constraint` |
| `orca cert` | **(deprecated)** Manage orca certificates: `ca-init`, `gen`, `show`, `renew`, `fingerprint` | | `orca cert` | **(deprecated)** Manage orca certificates: `ca-init`, `gen`, `show`, `renew`, `fingerprint` |
| `orca doctor` | Run self-checks: `cert`, `network`, `db`, `os`, `proxmox`, `no-orca-on-server` | | `orca doctor` | Run self-checks: `cert`, `network`, `db`, `os`, `proxmox`, `no-orca-on-server`, `nft`, `audit`, `modes`, `oidc`, `db-retention` |
| `orca audit` | View orca audit log (`list`) | | `orca audit` | View orca audit log (`list`) |
| `orca cache` | CLI cache management: `show`, `invalidate`, `invalidate-all` | | `orca cache` | CLI cache management: `show`, `invalidate`, `invalidate-all` |
| `orca acl` | ACL management: `grant`, `revoke`, `list`, `check` | | `orca acl` | ACL management: `grant`, `revoke`, `list`, `check` |
| `orca secrets` | Secrets management: `set`, `get`, `list`, `rotate`, `delete` | | `orca secrets` | Secrets management: `set`, `get`, `list`, `rotate`, `delete`, `rotate-master` |
| `orca drift` | Drift detection: `show`, `watch`, `acknowledge`, `remediate`, `config` | | `orca drift` | Drift detection: `show`, `watch`, `acknowledge`, `remediate`, `config` |
| `orca txn` | Transaction management: `apply`, `list`, `show`, `rollback` | | `orca txn` | Transaction management: `apply`, `list`, `show`, `rollback` |
| `orca nft` | nftables ingress management: `show`, `diff`, `doctor`, `country block`, `rate limit` |
| `orca collector` | Collector/aggregator management: `start`, `stop`, `status` | | `orca collector` | Collector/aggregator management: `start`, `stop`, `status` |
| `orca cluster` | Cluster management: `cutover`, `rotate-lead`, `compat-check` | | `orca cluster` | Cluster management: `cutover`, `rotate-lead`, `compat-check`, `seal`, `unseal` |
| `orca auth` | OIDC authentication: `login`, `logout`, `status`, `init-idp`, `register` |
| `orca peer-setup` | Create the orca system user + drift-events dir on a peer (REQ-111) |
See [docs/cli.md](docs/cli.md) for the full CLI reference with all flags See [docs/cli.md](docs/cli.md) for the full CLI reference with all flags
and examples. and examples.
@@ -114,7 +117,7 @@ acknowledged rather than papered over.
| Auto-scaling | Cluster autoscaler, HPA/VPA, deep integrations | — | | Auto-scaling | Cluster autoscaler, HPA/VPA, deep integrations | — |
| Daemon footprint | — | No daemon on the critical path; the cluster is the OS | | Daemon footprint | — | No daemon on the critical path; the cluster is the OS |
| OS-native | — | Workloads are systemd units + journald; no container runtime shim | | OS-native | — | Workloads are systemd units + journald; no container runtime shim |
| mTLS | — | mTLS by default; no opt-in required | | Transport | — | SSH-push is canonical (no daemon needed); mTLS available for daemon mode |
| Offline-first | — | No cloud dependencies; fully air-gapped operation | | Offline-first | — | No cloud dependencies; fully air-gapped operation |
| WASM-first | — | Workloads target OS primitives, not a container runtime | | WASM-first | — | Workloads target OS primitives, not a container runtime |
| Proxmox | — | First-class Proxmox node type (`--type proxmox`) via SSH-push | | Proxmox | — | First-class Proxmox node type (`--type proxmox`) via SSH-push |
@@ -129,6 +132,10 @@ acknowledged rather than papered over.
| [docs/namespace.md](docs/namespace.md) | Namespace and path layout | | [docs/namespace.md](docs/namespace.md) | Namespace and path layout |
| [docs/install.md](docs/install.md) | Installation guide | | [docs/install.md](docs/install.md) | Installation guide |
| [docs/security-scanning.md](docs/security-scanning.md) | Security scanning tools | | [docs/security-scanning.md](docs/security-scanning.md) | Security scanning tools |
| [docs/security-runbook.md](docs/security-runbook.md) | Security runbook — seal/unseal, rotation, incident response |
| [docs/webauthn.md](docs/webauthn.md) | WebAuthn / passkeys registration and login |
| [docs/threat-model.md](docs/threat-model.md) | STRIDE threat model + zero-trust architecture |
| [docs/oidc.md](docs/oidc.md) | OIDC configuration — Dex quickstart, BYO IdP |
## Examples ## Examples
@@ -144,6 +151,7 @@ make test # Run tests
go vet ./... # Vet all packages go vet ./... # Vet all packages
make lint # Run gofmt + go vet + shellcheck make lint # Run gofmt + go vet + shellcheck
make verify-reqs # Assert ROADMAP ↔ REQUIREMENTS consistency make verify-reqs # Assert ROADMAP ↔ REQUIREMENTS consistency
make verify-docs # Assert docs/cli.md ↔ `orca --help` consistency
``` ```
## Architecture ## Architecture
+24 -10
View File
@@ -16,17 +16,20 @@ import (
// the Phase + Status match at the END of the line, where those two columns // the Phase + Status match at the END of the line, where those two columns
// always live. The status token is optionally wrapped in markdown bold // always live. The status token is optionally wrapped in markdown bold
// (real rows use `**Complete**`; synthetic/future rows may use bare // (real rows use `**Complete**`; synthetic/future rows may use bare
// `Pending`), and may carry trailing notes (e.g. "**Complete** (P01 // `pending` or `complete` in any case), and may carry trailing notes
// shipped v0.2.1)") matched by [^|]* before the closing pipe. // (e.g. "**Complete** (P01 shipped v0.2.1)") matched by [^|]* before
var reqRowRe = regexp.MustCompile(`^\|\s*(REQ-\d+)\s*\|.*\|\s*([^|]*?)\s*\|\s*\*{0,2}(Complete|Pending)\*{0,2}[^|]*\|\s*$`) // the closing pipe. The (?i) flag makes the match case-insensitive so
// lowercase `pending` (used by v0.12/v0.13 REQ rows) is captured;
// normalizeStatus canonicalizes the captured value to title case.
var reqRowRe = regexp.MustCompile(`(?i)^\|\s*(REQ-\d+)\s*\|.*\|\s*([^|]*?)\s*\|\s*\*{0,2}(Complete|Pending)\*{0,2}[^|]*\|\s*$`)
// milestoneCompleteRe matches a ROADMAP.md milestone header that is marked // milestoneCompleteRe matches a ROADMAP.md milestone header that is marked
// COMPLETE. The bold span is substring-tolerant (GRILL #4): it matches // COMPLETE. The bold markers are optional (GRILL #4 + REQ-160 T11): it
// `**COMPLETE**`, `**COMPLETE (merged to main via v0.3)**`, and any future // matches `**COMPLETE**`, `**COMPLETE (merged to main via v0.3)**`, and
// variant where the word COMPLETE appears inside the bold span, possibly // bare `COMPLETE` (as used by the v0.12 milestone header). The word
// preceded or followed by non-asterisk text. The milestone version (v0.X) // COMPLETE may be preceded or followed by non-asterisk text. The milestone
// is captured. // version (v0.X) is captured.
var milestoneCompleteRe = regexp.MustCompile(`^##\s*Milestone\s+(v0\.\d+):.*—\s*\*\*[^*]*\bCOMPLETE\b[^*]*\*\*`) var milestoneCompleteRe = regexp.MustCompile(`^##\s*Milestone\s+(v0\.\d+):.*—\s*\*{0,2}[^*]*\bCOMPLETE\b[^*]*\*{0,2}`)
// phaseRe extracts the milestone version from a REQUIREMENTS Phase cell such // phaseRe extracts the milestone version from a REQUIREMENTS Phase cell such
// as `v0.7 P1`, `**v0.2 P01**`, `v0.2 P01P04`, or bare `v0.7`. The cell may // as `v0.7 P1`, `**v0.2 P01**`, `v0.2 P01P04`, or bare `v0.7`. The cell may
@@ -40,6 +43,17 @@ type reqRow struct {
status string // "Complete" or "Pending" status string // "Complete" or "Pending"
} }
// normalizeStatus canonicalizes a captured status token to the title-case
// form ("Complete" or "Pending") so that case-insensitive matches like
// "pending" or "complete" compare correctly against the drift assertions.
func normalizeStatus(s string) string {
s = strings.TrimSpace(s)
if s == "" {
return s
}
return strings.ToUpper(s[:1]) + strings.ToLower(s[1:])
}
// milestoneVersions returns the distinct v0.X milestones referenced in the // milestoneVersions returns the distinct v0.X milestones referenced in the
// phase cell (e.g. "v0.7 P1" → ["v0.7"]; "v0.2 P01 / v0.3 P02" → // phase cell (e.g. "v0.7 P1" → ["v0.7"]; "v0.2 P01 / v0.3 P02" →
// ["v0.2","v0.3"]). // ["v0.2","v0.3"]).
@@ -174,7 +188,7 @@ func parseRequirements(path string) ([]reqRow, error) {
if m == nil { if m == nil {
continue continue
} }
rows = append(rows, reqRow{id: m[1], phase: strings.TrimSpace(m[2]), status: m[3]}) rows = append(rows, reqRow{id: m[1], phase: strings.TrimSpace(m[2]), status: normalizeStatus(m[3])})
} }
if err := sc.Err(); err != nil { if err := sc.Err(); err != nil {
return nil, err return nil, err
+1068 -125
View File
File diff suppressed because it is too large Load Diff
+61 -12
View File
@@ -8,8 +8,8 @@ directory holds cluster-wide artifacts shared across namespaces.
> **v0.9 layout (canonical)**: This document describes the v0.9 > **v0.9 layout (canonical)**: This document describes the v0.9
> multi-namespace layout. The v0.8 flat layout (`orca.db`, `ca.crt`, > multi-namespace layout. The v0.8 flat layout (`orca.db`, `ca.crt`,
> `server.crt` at the root) is deprecated and will be removed in > `server.crt` at the root) is deprecated and removed in v0.12
> v0.11. See [v0.8 flat layout](#deprecated-v08-flat-layout) below. > (REQ-138).
## Namespace root resolution ## Namespace root resolution
@@ -51,12 +51,16 @@ $ORCA_HOME/
├── cluster/ # cluster-wide (NOT a workload namespace) ├── cluster/ # cluster-wide (NOT a workload namespace)
│ ├── ca.crt, ca.key # step-ca root (R-006, D-101) │ ├── ca.crt, ca.key # step-ca root (R-006, D-101)
│ ├── master.key # AES-256-GCM root (R-011, mode 0600) │ ├── master.key # AES-256-GCM root (R-011, mode 0600)
│ ├── master.key.sealed # sealed master key (REQ-147, mode 0600)
│ ├── config.md # Markdown frontmatter config (R-014) │ ├── config.md # Markdown frontmatter config (R-014)
│ ├── known_hosts # SSH known_hosts (D-035) │ ├── known_hosts # SSH known_hosts (D-035)
│ ├── orca_ssh_key # orca SSH private key (D-037) │ ├── orca_ssh_key # orca SSH private key (D-037)
│ ├── orca_ssh_key.pub # orca SSH public key │ ├── orca_ssh_key.pub # orca SSH public key
│ ├── peers/<host>/ # per-peer directory │ ├── peers/<host>/ # per-peer directory
│ ├── txns/ # cluster transaction log (R-016) │ ├── txns/ # cluster transaction log (R-016)
│ ├── acl.json # ACL state (mode 0600)
│ ├── oidc-client-secret # OIDC client secret (mode 0600, C-36)
│ ├── webauthn-credentials.db # WebAuthn public keys (mode 0600)
│ └── state/ # cluster state │ └── state/ # cluster state
├── _defaults/ # implicit root namespace (always exists) ├── _defaults/ # implicit root namespace (always exists)
│ ├── ns.md # namespace frontmatter (kind: Namespace) │ ├── ns.md # namespace frontmatter (kind: Namespace)
@@ -80,14 +84,16 @@ $ORCA_HOME/
exists. Every namespace inherits from `_defaults` and cannot opt out exists. Every namespace inherits from `_defaults` and cannot opt out
(D-185, D-187). (D-185, D-187).
- **`cluster/`** is NOT a workload namespace — it holds cluster-wide - **`cluster/`** is NOT a workload namespace — it holds cluster-wide
artifacts (CA, master key, SSH keys, known_hosts, peers, txns). artifacts (CA, master key, SSH keys, known_hosts, peers, txns, ACL,
OIDC secrets, WebAuthn credentials).
- **Per-namespace DBs**: each namespace has its own - **Per-namespace DBs**: each namespace has its own
`db/orca.db` (R-002). No namespace column in SQLite. `db/orca.db` (R-002). No namespace column in SQLite.
- **Namespace inheritance**: child namespaces inherit env and - **Namespace inheritance**: child namespaces inherit env and
constraints from parents (via `ns.md` frontmatter `parents:` field). constraints from parents (via `ns.md` frontmatter `parents:` field).
`_defaults` is always appended last in the inheritance chain. `_defaults` is always appended last in the inheritance chain.
- **`orca ns` subcommands**: `list`, `create`, `delete`, `inspect`, - **`orca ns` subcommands**: `list`, `create`, `delete`, `inspect`,
`validate` — see [docs/cli.md](cli.md#orca-ns). `validate`, `inherit`, `set-constraint` — see below and
[docs/cli.md](cli.md#orca-ns).
### Path reference (`internal/paths/`) ### Path reference (`internal/paths/`)
@@ -134,6 +140,50 @@ orca ns validate prod
orca ns delete staging orca ns delete staging
``` ```
### `orca ns inherit` — set parent namespace (R-002)
Set the parent namespace for a namespace. Updates `ns.md` frontmatter
(`parents` field) and validates the new chain has no cycles. The
implicit root `_defaults` is always appended last (D-185).
```bash
orca ns inherit <name> --parent <parent-namespace>
```
**Example**:
```bash
# Make staging inherit from prod (chain: staging -> prod -> _defaults)
orca ns inherit staging --parent prod
```
The child cannot inherit from itself transitively — the resolver
validates the chain before writing. If a cycle is detected, the
command exits 1 with an error.
### `orca ns set-constraint` — set a constraint (R-002)
Set a constraint on a namespace. Constraints are `key=value` strings
(e.g., `max-allocs=10`) stored in `ns.md` frontmatter and unioned
across the inheritance chain by the resolver.
```bash
orca ns set-constraint <name> <key>=<value>
```
**Example**:
```bash
# Limit prod to 10 concurrent allocations
orca ns set-constraint prod max-allocs=10
# Set a required node affinity
orca ns set-constraint prod require-label=ssd
```
Constraints are unioned (not overridden) across the inheritance chain:
if `_defaults` sets `max-allocs=50` and `prod` sets `max-allocs=10`,
the effective constraint is the most restrictive one (CEL evaluation
determines precedence per constraint key).
See [docs/cli.md](cli.md#orca-ns) for the full `orca ns` reference. See [docs/cli.md](cli.md#orca-ns) for the full `orca ns` reference.
## `ORCA_DB` override ## `ORCA_DB` override
@@ -148,11 +198,10 @@ orca init # uses /tmp/test.db for the DB, ~/.orca/ for everything else
## Deprecated: v0.8 flat layout ## Deprecated: v0.8 flat layout
> **Deprecated in v0.9**: The v0.8 flat layout (`orca.db`, `ca.crt`, > **Removed in v0.12** (REQ-138): The v0.8 flat layout (`orca.db`,
> `ca.key`, `server.crt`, `server.key` at the namespace root) is > `ca.crt`, `ca.key`, `server.crt`, `server.key` at the namespace root)
> superseded by the v0.9 multi-namespace layout (R-002). The v0.8 > is superseded by the v0.9 multi-namespace layout (R-002) and the
> layout is supported during the dual-write window via > dual-write window is closed.
> `internal/certpaths` (a thin shim) and will be removed in v0.11.
The v0.8 flat layout stored all state at the namespace root: The v0.8 flat layout stored all state at the namespace root:
@@ -166,12 +215,12 @@ The v0.8 flat layout stored all state at the namespace root:
The v0.9 re-architecture moved these to `cluster/` (CA, SSH keys) and The v0.9 re-architecture moved these to `cluster/` (CA, SSH keys) and
per-namespace `db/` (SQLite) to support multi-tenancy (R-002). The per-namespace `db/` (SQLite) to support multi-tenancy (R-002). The
`orca doctor --legacy-paths` command (v0.11-P14c) will detect v0.8 `internal/certpaths` shim that supported the dual-write window is
residue and recommend migration. removed in v0.12.
## See also ## See also
- [Install Guide](install.md) — 1-liner install with `install.sh`. - [Install Guide](install.md) — 1-liner install with `install.sh`.
- [Docker Guide](docker.md) — running orca in a container. - [Docker Guide](docker.md) — running orca in a container.
- [CLI Reference](cli.md) — `orca ns` subcommands. - [CLI Reference](cli.md#orca-ns) — `orca ns` subcommands.
- [Jobspec Reference](jobspec.md) — markdown frontmatter schema. - [Jobspec Reference](jobspec.md) — markdown frontmatter schema.
+138 -21
View File
@@ -1,31 +1,148 @@
# Security Runbook (v0.12) # Security Runbook (v0.13)
## Master Key Seal/Unseal This runbook documents the operational security procedures for orca's
zero-trust identity model (R-021): human identity is exclusively
external (OIDC), machine identity is exclusively mTLS/SPIFFE, and no
passwords / Orca-issued tokens / CA-key passphrases exist anywhere in
the system. The v0.12 milestone shipped these capabilities; the v0.13
milestone wired them operationally (R-023).
- `orca cluster seal`: encrypts master key with OIDC-derived key; ## Master Key Seal/Unseal (REQ-147, P05)
prints 5 Shamir shards for offline recovery.
- `orca cluster unseal`: operator authenticates via OIDC; master key
unwrapped into memory; zeroed on shutdown.
- `orca cluster unseal --recovery`: if IdP lost, present 3 of 5 shards.
## Master Key Rotation The cluster master key (`ClusterDir()/master.key`, mode 0600) encrypts
all namespace `.env.secrets` via per-namespace HKDF-SHA256 sub-keys
(AES-256-GCM). The master key can be **sealed** (encrypted at rest) and
**unsealed** (unwrapped into memory for use).
`orca secrets rotate-master [--dry-run]`: generates new master key, ### Seal
re-encrypts all namespace secrets, re-seals. Atomic + automatic rollback.
```bash
orca cluster seal
```
Encrypts the raw master key with a key derived from either:
- the OIDC ID token subject (if `orca auth login` has been run), or
- the cluster CA fingerprint (mTLS-only offline path, D-241).
The sealed blob is written to `ClusterDir()/master.key.sealed` (0600).
**Five Shamir shards (3-of-5 recovery)** are printed to stdout — store
them offline. The raw master key is then deleted from disk so the
cluster is sealed at rest.
### Unseal
```bash
orca cluster unseal
```
Reads the sealed blob and unwraps the master key using the OIDC ID
token subject or the cluster CA fingerprint. The unwrapped key is
written back to `ClusterDir()/master.key` (0600) and zeroed from
memory on process exit.
### Recovery (IdP lost)
```bash
orca cluster unseal --recovery
```
If the IdP is permanently lost, the operator is prompted for 3 of the
5 Shamir shards printed at seal time. With quorum, the master key is
reconstructed and written back to disk. If quorum is unavailable, the
cluster is unrecoverable by design (C-35: no backdoor).
## Master Key Rotation (REQ-129, C-30)
```bash
orca secrets rotate-master [--dry-run]
```
Generates a new master key, re-encrypts every namespace's
`.env.secrets` under the new key, and re-seals the master key to OIDC.
With `--dry-run`, reports affected namespaces without writing.
- **Atomic per-namespace**: each namespace is re-encrypted independently.
- **Automatic rollback**: on any namespace failure, the old sealed key
is restored (C-30).
- **No passphrase** (R-021): the master key is sealed to OIDC, not to a
human-typed passphrase.
## File-Mode Audit (REQ-033, REQ-130, F13)
```bash
orca doctor modes
```
Verifies file modes on security-sensitive files across `ORCA_HOME`:
- private keys / secrets: `0600`
- certs / public keys: `0644`
Exits 0 if all files have correct modes; exits 1 if any violation is
found. Missing files are not counted as violations.
Checks: SSH key, master key (sealed blob), server cert/key,
known_hosts, `acl.json`, OIDC client secret.
## Audit Log Tamper-Evidence (REQ-125, F2)
```bash
orca doctor audit
```
Verifies the audit log hash chain. Opens the orca SQLite DB, recomputes
the hash chain from the first audit entry, and reports the chain head
hash. If any entry's `entry_hash` or `prev_hash` link does not match the
recomputed value, the chain has been tampered with and the command
exits non-zero.
The audit log is append-only (SQLite trigger blocks
UPDATE/DELETE). Each entry's `actor` field carries the OIDC `sub` or
SPIFFE SVID. Run this after any suspected intrusion or as part of a
regular audit cadence.
## Sudoers Audit (REQ-134, F22)
```bash
orca doctor proxmox
```
Audits the `/etc/sudoers.d/orca` file against the expected allowlist:
- `pct` + `qm` with NOEXEC
- `apt-get` / `dpkg` excluded (or NOEXEC'd)
- `pvesh` EXCLUDED (AD-020: pvesh can bypass NOEXEC via the API execute
endpoint)
## nft Audit (REQ-133, F21)
```bash
orca doctor nft
```
Audits the live nftables ingress ruleset against the on-disk
`/etc/nftables.d/orca.nft` hash (recorded at the latest applied txn).
Reports drift if the live ruleset does not match. Also verifies:
- table exists
- DNAT `:443 → 127.0.0.1:8443` and `:80 → 127.0.0.1:8080` present
- rate-limit meter present
- `/etc/nftables.d/orca.nft` parses
## Incident Response ## Incident Response
1. Revoke the compromised identity (OIDC user/group or SPIFFE SVID). 1. **Revoke the compromised identity** (OIDC user/group or SPIFFE SVID).
2. Rotate the master key (`orca secrets rotate-master`). 2. **Rotate the master key** (`orca secrets rotate-master`).
3. Review the audit log (`orca doctor audit` verifies the hash chain). 3. **Review the audit log** (`orca doctor audit` verifies the hash
4. If the master key is compromised, all historical secrets are chain; `orca audit list` shows entries).
compromised (no forward secrecy). 4. **Check file modes** (`orca doctor modes` detects permission drift).
5. If the master key is compromised, **all historical secrets are
compromised** (no forward secrecy — documented residual risk).
6. **Re-seal** the master key after rotation (`orca cluster seal`).
## Sudoers Audit ## OIDC Provider Health (P06)
`orca doctor proxmox` audits the `/etc/sudoers.d/orca` file against the ```bash
expected allowlist (pct + qm with NOEXEC; apt-get/dpkg excluded). orca doctor oidc
```
## nft Audit Checks the bundled Dex OIDC provider health. Verifies the Dex systemd
unit is running and the `/.well-known/openid-configuration` endpoint
`orca doctor nft` audits the live nftables ruleset against the emitted one. responds. Run after `orca auth init-idp` or after a Dex config change.
+84 -14
View File
@@ -1,27 +1,97 @@
# WebAuthn / Passkeys (v0.12) # WebAuthn / Passkeys (v0.13)
## Overview ## Overview
The bundled Dex uses a custom WebAuthn connector for password-free The bundled Dex uses a custom WebAuthn connector (`orca-webauthn-connector`,
authentication. Passkeys are public-key credentials — the private key REQ-148) for password-free authentication. Passkeys are public-key
never leaves the authenticator (TPM/security key/phone Secure Enclave). credentials — the private key never leaves the authenticator (TPM /
security key / phone Secure Enclave). This directly satisfies R-021
(no Orca-issued credentials): the authenticator proves possession of
the private key without ever exposing it.
The WebAuthn connector ships as part of the v0.12 milestone (P05) and
is operationally wired in v0.13 (P04: registration requires auth; P06:
real Dex deployment).
## Registration ## Registration
`orca auth register` opens the browser to the Dex WebAuthn endpoint. ```bash
After the ceremony (biometric/security key), Dex maps the credential orca auth register [--no-browser]
ID to an OIDC `sub`. Credentials stored at ```
`ClusterDir()/webauthn-credentials.db` (0600, public keys only).
Opens the browser to the Dex WebAuthn registration page at
`https://<cluster>/orca/webauthn/register`. The operator authenticates
via an existing session or admin bootstrap token, then performs the
WebAuthn ceremony (biometric or security key). After the ceremony,
Dex maps the credential ID to an OIDC `sub`.
- **`--no-browser`**: print the registration URL instead of opening a
browser (useful for headless operators or remote SSH sessions — copy
the URL into a local browser).
Credentials are stored at `ClusterDir()/webauthn-credentials.db`
(mode 0600, public keys only — private keys never leave the
authenticator and are never stored by orca).
**Example**:
```bash
# Interactive (opens browser)
orca auth register
# Headless / remote SSH (print URL)
orca auth register --no-browser
# → https://orca.local/orca/webauthn/register
```
## RP ID ## RP ID
The relying-party ID is the cluster's Traefik-served domain The relying-party ID is the cluster's Traefik-served domain, set via
(`--rp-id` on `orca auth init-idp`). HTTPS secure context is provided `--rp-id` on `orca auth init-idp` (C-38). The RP ID **must** match the
by Traefik (step-ca cert, R-017). cluster's Traefik domain — WebAuthn enforces that the RP ID is a
registrable domain suffix of the current origin.
HTTPS secure context is provided by Traefik (step-ca cert, R-017).
WebAuthn requires a secure context (HTTPS or localhost); the step-ca
cert behind Traefik satisfies this.
## Bootstrap Sequence ## Bootstrap Sequence
1. `orca init` bootstraps the cluster CA (step-ca, mTLS-only). 1. `orca init` bootstraps the cluster CA (step-ca, mTLS-only).
2. `orca auth init-idp` deploys Dex behind Traefik (step-ca cert). 2. `orca auth init-idp --rp-id <cluster-domain>` deploys Dex behind
3. First operator registers a passkey via the mTLS-authenticated session. Traefik (step-ca cert) with the WebAuthn connector configured.
4. Subsequent operators use WebAuthn. 3. First operator authenticates via an existing session or admin
bootstrap token, then registers a passkey:
```bash
orca auth register
```
4. Subsequent operators use WebAuthn login (`orca auth login` opens
the browser to the Dex login page; the WebAuthn ceremony is one of
the available upstreams).
## Health Check
```bash
orca doctor oidc
```
Verifies the bundled Dex OIDC provider is running and the
`/.well-known/openid-configuration` endpoint responds. Run after
`orca auth init-idp` or after a Dex config change.
## Security properties
- **No passwords**: WebAuthn is password-free. No password is ever
sent to or stored by orca (R-021).
- **Phishing-resistant**: the WebAuthn protocol cryptographically binds
the ceremony to the RP ID, defeating credential phishing.
- **Private key never leaves the authenticator**: orca stores only
public keys.
- **Secure context required**: HTTPS via step-ca / Traefik (C-38).
## See also
- [docs/oidc.md](oidc.md) — OIDC configuration (Dex quickstart, BYO IdP)
- [docs/security-runbook.md](security-runbook.md) — security runbook
- [docs/cli.md](cli.md#orca-auth) — `orca auth` CLI reference
+3 -3
View File
@@ -47,8 +47,8 @@ var (
) )
var jobRunCmd = &cobra.Command{ var jobRunCmd = &cobra.Command{
Use: "run <spec.hcl>", Use: "run <spec.md>",
Short: "Run a job from an HCL spec file", Short: "Run a job from a markdown spec file",
Long: "Submit a job spec, execute its tasks, and persist the result. Use --target to pin to a specific node (overrides bin-packing); --idempotency-key for cross-node dispatch dedupe.", Long: "Submit a job spec, execute its tasks, and persist the result. Use --target to pin to a specific node (overrides bin-packing); --idempotency-key for cross-node dispatch dedupe.",
Args: cobra.ExactArgs(1), Args: cobra.ExactArgs(1),
RunE: func(cmd *cobra.Command, args []string) error { RunE: func(cmd *cobra.Command, args []string) error {
@@ -216,7 +216,7 @@ func renderJobs(cmd *cobra.Command, jobs []*model.Job) error {
return printJSON(jobs) return printJSON(jobs)
} }
if len(jobs) == 0 { if len(jobs) == 0 {
fmt.Fprintln(cmd.OutOrStdout(), "No jobs. Use 'orca job run <spec.hcl>' to submit one.") fmt.Fprintln(cmd.OutOrStdout(), "No jobs. Use 'orca job run <spec.md>' to submit one.")
return nil return nil
} }
fmt.Fprintf(cmd.OutOrStdout(), "%-36s %-20s %-12s %-8s\n", "ID", "NAME", "STATUS", "EXIT") fmt.Fprintf(cmd.OutOrStdout(), "%-36s %-20s %-12s %-8s\n", "ID", "NAME", "STATUS", "EXIT")
+15 -3
View File
@@ -6,9 +6,21 @@ import (
var statusCmd = &cobra.Command{ var statusCmd = &cobra.Command{
Use: "status", Use: "status",
Short: "Show orca daemon status", Short: "Show orca daemon status (deprecated)",
Long: "Display the current status of the local orca daemon, including version, uptime, and connection info.", Long: `Display the current status of the local orca daemon, including
version, uptime, and connection info.
**Deprecated (v0.13):** This command is a v0.1 stub that reports a
hardcoded "daemon stopped" status. The daemon model was replaced by
SSH-push in v0.9 (R-001) and the dual-write window closed in v0.12
(REQ-138). Use the canonical commands instead:
orca node list # node registry + state
orca metrics /healthz # liveness/health probe (daemon-mode only)
This command will be removed in a future release.`,
RunE: func(cmd *cobra.Command, args []string) error { RunE: func(cmd *cobra.Command, args []string) error {
warnDeprecated("orca status is deprecated (v0.1 stub): use 'orca node list' for node state and 'orca metrics /healthz' for health probes")
status := map[string]any{ status := map[string]any{
"version": version, "version": version,
"daemon": "stopped", "daemon": "stopped",
@@ -23,7 +35,7 @@ var statusCmd = &cobra.Command{
} }
printText("orca daemon status\n") printText("orca daemon status\n")
printText(" version: %s\n", version) printText(" version: %s\n", version)
printText(" daemon: %s\n", "stopped (daemon not yet implemented in Phase 1)") printText(" daemon: %s\n", "stopped (deprecated v0.1 stub; use 'orca node list' + 'orca metrics /healthz')")
printText(" api_addr: %s\n", "https://localhost:8443") printText(" api_addr: %s\n", "https://localhost:8443")
printText(" phase: %s\n", "1-cli-skeleton") printText(" phase: %s\n", "1-cli-skeleton")
printText(" milestone: %s\n", "v0.1") printText(" milestone: %s\n", "v0.1")
+5 -5
View File
@@ -3,7 +3,7 @@
// //
// The bootstrap sequence (run via `orca node join --type proxmox`): // The bootstrap sequence (run via `orca node join --type proxmox`):
// 1. Generate or load the orca SSH keypair (Ed25519, D-037) // 1. Generate or load the orca SSH keypair (Ed25519, D-037)
// 2. SSH dial with password auth + TOFU host-key capture (D-035) // 2. SSH dial with key auth + TOFU host-key capture (D-035)
// 3. Deploy the orca pubkey to ~orca/.ssh/authorized_keys // 3. Deploy the orca pubkey to ~orca/.ssh/authorized_keys
// 4. Create the `orca` Linux system user (config-overridable name) // 4. Create the `orca` Linux system user (config-overridable name)
// 5. Create the OrcaOperator PVE role with least-privilege privileges // 5. Create the OrcaOperator PVE role with least-privilege privileges
@@ -16,9 +16,9 @@
// 10. Return the node metadata for the caller to persist // 10. Return the node metadata for the caller to persist
// //
// All steps are idempotent (D-036): re-running the bootstrap on an // All steps are idempotent (D-036): re-running the bootstrap on an
// already-configured host is a no-op. The password is never persisted // already-configured host is a no-op. Authentication is key-based (R-021)
// (D-031) — it is used only for the initial SSH auth and pubkey // (D-031): the orca SSH key is used for the initial SSH auth and
// deployment; subsequent orca→Proxmox access uses the deployed SSH key. // pubkey deployment; subsequent orca→Proxmox access uses the same key.
package proxmox package proxmox
import ( import (
@@ -143,7 +143,7 @@ func BootstrapProxmox(ctx context.Context, opts Options) (*Result, error) {
return nil, fmt.Errorf("ssh key: %w", err) return nil, fmt.Errorf("ssh key: %w", err)
} }
// Step 2: SSH dial with password auth + host-key verification (D-035, // Step 2: SSH dial with key auth + host-key verification (D-035,
// REQ-058). When opts.HostKeyFingerprint is set (D-044), use a pinned // REQ-058). When opts.HostKeyFingerprint is set (D-044), use a pinned
// callback that fails closed on mismatch (AD-028); otherwise use the // callback that fails closed on mismatch (AD-028); otherwise use the
// TOFU known_hosts capture callback (D-035). The TOFU wrapper fixes // TOFU known_hosts capture callback (D-035). The TOFU wrapper fixes
+50
View File
@@ -0,0 +1,50 @@
#!/usr/bin/env bash
# verify-docs.sh — assert that every subcommand documented in docs/cli.md
# exists in `orca --help` output (and vice versa). Catches doc drift.
#
# Usage: scripts/verify-docs.sh [binary] [docs/cli.md]
# Exit 0 = consistent, 1 = drift detected, 2 = error.
set -euo pipefail
BIN="${1:-./bin/orca}"
DOCS="${2:-docs/cli.md}"
if [ ! -x "$BIN" ]; then
echo "verify-docs: binary not found at $BIN (run 'make build' first)" >&2
exit 2
fi
if [ ! -f "$DOCS" ]; then
echo "verify-docs: $DOCS not found" >&2
exit 2
fi
# Extract top-level commands from `orca --help` (lines indented under
# "Available Commands:" with two leading spaces, command name is the
# first token).
HELP_OUTPUT="$("$BIN" --help 2>/dev/null)"
HELP_CMDS="$(echo "$HELP_OUTPUT" | \
awk '/^Available Commands:/{flag=1; next} /^$/{flag=0} flag && /^ /{print $1}' | \
grep -v '^completion$' | grep -v '^help$' | sort -u)"
# Extract documented commands from docs/cli.md. These appear as
# `## \`orca <command>\`` or `## \`orca <command>\` *(deprecated)*` headers.
DOC_CMDS="$(grep -oE '^## `orca [a-z_-]+`' "$DOCS" | \
sed 's/^## `orca //; s/`$//' | sort -u)"
# Compare.
diff_out="$(diff <(echo "$HELP_CMDS") <(echo "$DOC_CMDS") || true)"
if [ -n "$diff_out" ]; then
echo "verify-docs: drift detected between docs/cli.md and \`orca --help\`:" >&2
echo "$diff_out" >&2
echo "" >&2
echo "Commands in --help but not in docs/cli.md (add them):" >&2
comm -23 <(echo "$HELP_CMDS") <(echo "$DOC_CMDS") >&2
echo "" >&2
echo "Commands in docs/cli.md but not in --help (remove them or fix typo):" >&2
comm -13 <(echo "$HELP_CMDS") <(echo "$DOC_CMDS") >&2
exit 1
fi
echo "verify-docs: OK — docs/cli.md consistent with orca --help"
exit 0