diff --git a/.ciagent/REQUIREMENTS.md b/.ciagent/REQUIREMENTS.md index b473fea..83d6d06 100644 --- a/.ciagent/REQUIREMENTS.md +++ b/.ciagent/REQUIREMENTS.md @@ -254,7 +254,7 @@ operator decision Q2=C. ## 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 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. @@ -263,41 +263,41 @@ zero-trust identity model (R-021). See ROADMAP.md for the 29-phase plan | 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-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-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-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** | 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** | complete | ### Wave B — Zero-trust identity | 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-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-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-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-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-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-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-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-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-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-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-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-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-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-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-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-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-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-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-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-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-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-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-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-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-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-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-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** | 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** | 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** | 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** | 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** | 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** | 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** | 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** | 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** | 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** | 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** | 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** | 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** | 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** | 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** | 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** | 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** | 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** | 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** | 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** | 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** | 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** | 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** | 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** | 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** | 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** | complete | ### Scope notes (v0.12) diff --git a/.ciagent/ROADMAP.md b/.ciagent/ROADMAP.md index c869d83..a26fd5e 100644 --- a/.ciagent/ROADMAP.md +++ b/.ciagent/ROADMAP.md @@ -400,7 +400,7 @@ tags: `v0.10.0`…`v0.10.21`. - External CA / Let's Encrypt / cert transparency - 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 surface, **including the operating system itself**, plus adoption of a diff --git a/CHANGELOG.md b/CHANGELOG.md index 85d08e6..30e72f5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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/), 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 -- `07b8ad2ceaba7ca303dfe91876930d33b76c633e` — ship(P05): health checks merged into milestone -- `b06458d31370750417a3b239dac61c6e2fdf5329` — docs(P05): verification - 4 layers pass -- `708d9834296271094667700e88e80bfa27db7bdd` — feat(P05): health check daemon with /healthz, /readyz, /v1/* handlers -- `30c523c0c7a8e75a2e97b42f1c8a39802febcbdc` — ship(P04): state persistence merged into milestone -- `759b1b519d7fadf3d91d3070952d9ad2051a0eba` — docs(P04): verification - 4 layers pass -- `b25e074e1d3518f175478184ff8d002ec0d8412c` — feat(P04): audit log + persistence hardening -- `bb6b5b3e8342c16601a8503223c7186ecdbb00df` — ship(P03): task exec merged into milestone -- `857f7563190e7703f97c607a50d6b0a897d250e9` — docs(P03): verification - 4 layers pass -- `f9a98733411cfa8657e82636e0c55671086ebe46` — feat(P03): task execution engine with HCL specs, jobs, tasks, WaitDelay -- `78334f1f74f0c185c6d38014c796aac4903b8141` — ship(P02): node mgmt merged into milestone -- `c7dbcef9587596786a541a7566479d9fb93fcf0a` — docs(P02): verification - 4 layers pass -- `9580f347c68e395dccfbe83b27a857d52bf21075` — feat(P02): node management with SQLite-backed registry -- `46e929e4c6539bd604539ba27d5ed0c606e87bb9` — chore(P01): source .env in trigger_coreci.sh for GITEA_TOKEN -- `503923bf1ee2c60f8375acc7eb9608d346368e1c` — ship(P01): cli skeleton merged into milestone -- `e3f6e1df825d39f73933c9996bd2cc4717ff1061` — docs(P01): verification - 4 layers pass -- `aa3cccead503a37dfec75873d06d2d396a2876f2` — feat(P01): CLI skeleton with Cobra, subcommand stubs, pre-push hook -- `c2038952c74f7c242ba3be65d2f4269b23685f5a` — docs(P00): create 6 phase plans with wave ordering -- `65eb2e601b741b36388598b9f8adddd7bd8dd3a8` — docs(P00): research findings - architecture + personas -- `6f34f1794b9f526c06a1dc139d4a74371599502e` — docs(P00): ideation - 30 ideas accepted (3 tiers) -- `bc7ce1caf672e87774455a6cd6cc0db986cd09b3` — docs(P00): clarify ambiguities (full autonomy, 10 decisions) -- `55aae5347ec09bce9ef7697ea0c9c9ee158bc040` — chore(P00): rename orch-engine to orca, configure gitea + coreci (v0.1) -- `0cba1aa5feef9564f8b9a2a97ae735dc859a8a84` — chore(P00): set autonomy level to full -- `e2e77e79b9cbfb462044662543845476f843161b` — chore(P00): quick task - populate config.json with backlog reference -- `8c086def698bf0af31e8e820b6b7a2783af06f43` — chore(config): populate ciagent config with standard settings -- `8774008c3e47e4ca4711f4fef164531006d16216` — docs(init): validate specification +## v0.13 milestone (in progress) — tag line v0.12.x + +The v0.13 milestone is **Production Hardening Round 2 + UAT Plan**. +Three deep codebase sweeps (security, reliability, feature/doc claims) +surfaced ~60 gaps beyond v0.12. v0.13 closes all critical/high/medium +findings and delivers the UAT plan + signoff script that gates the +v1.0.0 cut. + +**Load-bearing architectural changes**: +- **R-022** — `orca job run` deploys to remote nodes via the scheduler → + emitter → SSH-push pipeline. The local `exec.CommandContext` path is + removed (P03). +- **R-023** — Zero-trust enforcement is operationally wired: `acl.Check` + is invoked on every daemon handler + sshpush + txn apply path; + `acl.json` is 0600; audit `actor` carries OIDC sub/SVID; WebAuthn + registration requires auth; `cluster seal`/`unseal` + `doctor audit`/ + `doctor modes` CLI commands exist (P04, P05). + +### v0.13 phase commits (v0.11.29..HEAD) + +- `ed91d68` — feat(P10): observability expansion — metrics + security headers (REQ-159) +- `531b369` — fix(P09): migration + operational safety — job stop, retention, logs cap (REQ-158) +- `3a3ea74` — fix(P08): transport + SSH safety — typed errors, IPv6, timeouts, signal (REQ-157) +- `0358efe` — fix(P07): concurrency safety — SQLite, flock, cache, atomic writes (REQ-156) +- `978334a` — feat(P06): auth init-idp real + auth register + doctor oidc (REQ-155) +- `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. diff --git a/Makefile b/Makefile index 8b65b10..4e67dde 100644 --- a/Makefile +++ b/Makefile @@ -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 GOFLAGS := -trimpath @@ -31,6 +31,7 @@ help: @echo " release Run scripts/release.sh [VERSION] — build, tar, publish" @echo " security-scan Run gosec+govulncheck+gitleaks (P03, REQ-014/027/039)" @echo " verify-reqs Assert ROADMAP COMPLETE ↔ REQUIREMENTS Complete (REQ-060)" + @echo " verify-docs Assert docs/cli.md ↔ orca --help consistency (REQ-160)" build: @mkdir -p bin @@ -128,3 +129,9 @@ security-scan: # of scope (P04 audit). Exits 0 on consistency, 1 with a diff on drift. verify-reqs: 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 diff --git a/README.md b/README.md index e36c77a..b46af0d 100644 --- a/README.md +++ b/README.md @@ -6,8 +6,8 @@ identity. ## Status -**v0.11: Production Hardening — IN PROGRESS** | **v1.0: UAT-gated** (cut -separately after v0.11 completion per operator decision) +**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.13 completion per operator decision) 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 - **CLI-first** — the command line is the primary interface (humans and AI agents) -- **Security before features** — mTLS by default; NFRs ship before new - functionality +- **Security before features** — SSH-push is the canonical transport + (mTLS available for daemon mode); NFRs ship before new functionality - **WASM-first** — workloads target OS primitives (systemd units, journald), not a container runtime shim - **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) 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) -curl -fsSL https://git.cloudinit.dev/coreci/orca/raw/main/scripts/install.sh | bash -s -- --version 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.12.10 # 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 @@ -65,7 +65,7 @@ config, database, and certificates in the namespace dir: ```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 @@ -73,7 +73,7 @@ curl -fsSL https://git.cloudinit.dev/coreci/orca/raw/main/scripts/install.sh | b | Command | Description | |---------|-------------| | `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 daemon` | **(deprecated)** Run the orca daemon (HTTP API + health checks) | | `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 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 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 cache` | CLI cache management: `show`, `invalidate`, `invalidate-all` | | `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 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 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 and examples. @@ -114,7 +117,7 @@ acknowledged rather than papered over. | Auto-scaling | Cluster autoscaler, HPA/VPA, deep integrations | — | | Daemon footprint | — | No daemon on the critical path; the cluster is the OS | | 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 | | WASM-first | — | Workloads target OS primitives, not a container runtime | | 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/install.md](docs/install.md) | Installation guide | | [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 @@ -144,6 +151,7 @@ make test # Run tests go vet ./... # Vet all packages make lint # Run gofmt + go vet + shellcheck make verify-reqs # Assert ROADMAP ↔ REQUIREMENTS consistency +make verify-docs # Assert docs/cli.md ↔ `orca --help` consistency ``` ## Architecture diff --git a/cmd/verify-reqs/main.go b/cmd/verify-reqs/main.go index a53f4e3..b065383 100644 --- a/cmd/verify-reqs/main.go +++ b/cmd/verify-reqs/main.go @@ -16,17 +16,20 @@ import ( // 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 // (real rows use `**Complete**`; synthetic/future rows may use bare -// `Pending`), and may carry trailing notes (e.g. "**Complete** (P01 -// shipped v0.2.1)") matched by [^|]* before the closing pipe. -var reqRowRe = regexp.MustCompile(`^\|\s*(REQ-\d+)\s*\|.*\|\s*([^|]*?)\s*\|\s*\*{0,2}(Complete|Pending)\*{0,2}[^|]*\|\s*$`) +// `pending` or `complete` in any case), and may carry trailing notes +// (e.g. "**Complete** (P01 shipped v0.2.1)") matched by [^|]* before +// 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 -// COMPLETE. The bold span is substring-tolerant (GRILL #4): it matches -// `**COMPLETE**`, `**COMPLETE (merged to main via v0.3)**`, and any future -// variant where the word COMPLETE appears inside the bold span, possibly -// preceded or followed by non-asterisk text. The milestone version (v0.X) -// is captured. -var milestoneCompleteRe = regexp.MustCompile(`^##\s*Milestone\s+(v0\.\d+):.*—\s*\*\*[^*]*\bCOMPLETE\b[^*]*\*\*`) +// COMPLETE. The bold markers are optional (GRILL #4 + REQ-160 T11): it +// matches `**COMPLETE**`, `**COMPLETE (merged to main via v0.3)**`, and +// bare `COMPLETE` (as used by the v0.12 milestone header). The word +// COMPLETE may be preceded or followed by non-asterisk text. The milestone +// version (v0.X) is captured. +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 // as `v0.7 P1`, `**v0.2 P01**`, `v0.2 P01–P04`, or bare `v0.7`. The cell may @@ -40,6 +43,17 @@ type reqRow struct { 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 // phase cell (e.g. "v0.7 P1" → ["v0.7"]; "v0.2 P01 / v0.3 P02" → // ["v0.2","v0.3"]). @@ -174,7 +188,7 @@ func parseRequirements(path string) ([]reqRow, error) { if m == nil { 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 { return nil, err diff --git a/docs/cli.md b/docs/cli.md index e8cd261..562c1b9 100644 --- a/docs/cli.md +++ b/docs/cli.md @@ -3,11 +3,13 @@ This document is the complete reference for the `orca` command-line interface. Every command, subcommand, and flag is documented here. -> **Canonical path (v0.9)**: The v0.9 re-architecture introduced the +> **Canonical path (v0.9+)**: The v0.9 re-architecture introduced the > SSH-push deployment model, markdown jobspec, multi-namespace layout, > and CLI-side scheduler. Commands marked **deprecated** below are from -> the v0.8 daemon/mTLS model and will be removed in v0.11. Use the -> v0.9 canonical path for all new work. +> the v0.8 daemon/mTLS model. The v0.12 milestone closed the dual-write +> window (REQ-138) — the legacy CA/mTLS/daemon paths are deleted. The +> v0.13 milestone (in progress) wires the scheduler into `orca job run` +> and delivers production hardening round 2. ## Global flags @@ -17,8 +19,8 @@ These flags are available on every `orca` command. |------|------|---------|-------------| | `--json` | bool | `false` | Output in JSON format (machine-readable) | | `--system` | bool | `false` | Use system-level namespace root (`/root/.orca`) instead of user-level (`~/.orca`). Errors if `ORCA_HOME` is already set to a conflicting value. | -| `--config` | string | `""` | Path to config file (overrides `~/.orca/config.hcl`). Supports `.hcl` (legacy) and `.md` (v0.9 canonical) formats. | -| `--no-deprecation-warnings` | bool | `false` | Suppress v0.9 deprecation warnings. Use during `orca upgrade` migrations. | +| `--config` | string | `""` | Path to config file (overrides `~/.orca/config.md`). Supports `.md` (canonical) and `.hcl` (legacy) formats. | +| `--no-deprecation-warnings` | bool | `false` | Suppress deprecation warnings. Use during `orca upgrade` migrations. | ### Output modes @@ -34,7 +36,6 @@ These flags are available on every `orca` command. |----------|-------------| | `ORCA_HOME` | Namespace root directory (default `~/.orca`). Overrides all on-disk paths. | | `ORCA_DB` | Fine-grained database path override. | -| `ORCA_PROXMOX_PASSWORD` | SSH password for `orca node join --type proxmox` (never persisted). | | `ORCA_LISTEN_ADDR` | Daemon listen address (deprecated). | | `ORCA_CA_PATH` | CA certificate path override. | | `ORCA_SERVER_CERT_PATH` | Server certificate path override. | @@ -81,100 +82,38 @@ orca --system init # system-level bootstrap at /root/.orca --- -## `orca job` +## `orca version` -Manage orca jobs — run, list, stop, and inspect. - -### `orca job run` - -Run a job from a spec file. +Print the orca version, git commit, and build time. ``` -orca job run [flags] +orca version ``` -Dispatches by file extension: -- `.md` → Markdown frontmatter parser (v0.9 canonical) -- `.yaml` / `.yml` → YAML frontmatter parser -- `.hcl` → Legacy HCL adapter (deprecated, see callout below) +**Flags**: none (global flags only). -| Flag | Type | Default | Description | -|------|------|---------|-------------| -| `--target` | string | `""` | Pin job to a specific node ID (overrides bin-packing scheduler) | -| `--idempotency-key` | string | `""` | Idempotency key for cross-node dispatch dedupe | +--- -**Examples**: -```bash -orca job run web-app.md -orca job run api.yaml --target node-abc-123 -orca job run worker.md --idempotency-key deploy-2026-08-05 -``` +## `orca status` *(deprecated)* -> **Deprecated**: `orca job run ` (legacy HCL jobspec) still -> works via the adapter but emits a deprecation warning. Migrate `.hcl` -> specs to `.md` (see [docs/jobspec.md](jobspec.md)). Removed in v0.11. - -### `orca job list` - -List all jobs. +Show orca daemon status. ``` -orca job list [flags] +orca status ``` -| Flag | Type | Default | Description | -|------|------|---------|-------------| -| `--watch` | bool | `false` | Stream jobs until Ctrl-C (table refresh or `--json` per-event) | - -**Output columns**: `ID NAME STATUS EXIT` - -**Examples**: -```bash -orca job list -orca job list --watch # table refresh -orca job list --watch --json # NDJSON: {"event":"update","job":{...}} -``` - -### `orca job stop` - -Stop a running job (soft stop). - -``` -orca job stop [job-id] [flags] -``` - -| Flag | Type | Default | Description | -|------|------|---------|-------------| -| `--id` | string | `""` | Job ID (alternative to positional argument) | - -**Example**: -```bash -orca job stop abc-123-def -orca job stop --id abc-123-def -``` - -### `orca job logs` - -Show task output for a job. - -``` -orca job logs [job-id] [flags] -``` - -| Flag | Type | Default | Description | -|------|------|---------|-------------| -| `--id` | string | `""` | Job ID (alternative to positional argument) | - -**Example**: -```bash -orca job logs abc-123-def -``` +> **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 `orca node list` for node registry/state and +> `orca metrics /healthz` for health probes. This command will be +> removed in a future release. --- ## `orca node` -Manage orca nodes — join, leave, or list nodes in the registry. +Manage orca nodes — join, leave, list, drain, key-reset, and capacity. ### `orca node join` @@ -188,17 +127,17 @@ Node types (via `--type`): - `localhost` (default): register a local or Linux node - `proxmox`: SSH-bootstrap a remote Proxmox VE 8/9 host (deploys orca pubkey, creates orca user + PVE role + sudoers allowlist; requires - `--host` + `--password`) + `--host` + `--ssh-key`; R-021: no passwords) | Flag | Type | Default | Description | |------|------|---------|-------------| | `--name` | string | `""` | Node name (required for `--type localhost`) | -| `--addr` | string | `""` | Node address (default `localhost:8443`) | +| `--addr` | string | `"localhost:8443"` | Node address | | `--ca-fingerprint` | string | `""` | Pin CA cert SHA-256 (fails if on-disk CA doesn't match) | | `--type` | string | `"localhost"` | Node type: `localhost` or `proxmox` | | `--host` | string | `""` | Proxmox host address (IP/hostname; required for `--type proxmox`) | | `--ssh-user` | string | `"root"` | SSH username for proxmox bootstrap | -| `--password` | string | `""` | SSH password for proxmox bootstrap (never persisted; prefer `$ORCA_PROXMOX_PASSWORD`) | +| `--ssh-key` | string | `""` | SSH private key path for proxmox bootstrap (R-021: no passwords; default: orca key) | | `--ssh-port` | int | `22` | SSH port for proxmox bootstrap | | `--proxmox-user` | string | `"orca"` | Linux system user to create on the proxmox host | | `--proxmox-role` | string | `"OrcaOperator"` | PVE custom role to create | @@ -206,22 +145,17 @@ Node types (via `--type`): **Examples**: ```bash -# Localhost (deprecated mTLS path) +# Localhost orca node join --name my-node # Proxmox (v0.9 canonical SSH-push path) orca node join --type proxmox --host 192.168.1.100 --ssh-user root -ORCA_PROXMOX_PASSWORD=secret orca node join --type proxmox --host 192.168.1.100 +orca node join --type proxmox --host 192.168.1.100 --ssh-key ~/.ssh/orca_ed25519 # Proxmox with pre-pinned host key orca node join --type proxmox --host 192.168.1.100 --host-key-fingerprint SHA256:abc123... ``` -> **Deprecated**: `orca node join` without `--type proxmox` (the -> localhost mTLS join path) is deprecated in v0.9. The v0.9 canonical -> path is SSH-push (`--type proxmox`) or local execution (no join -> needed). Removed in v0.11. - ### `orca node leave` Remove a node from the orca registry. @@ -268,6 +202,23 @@ host's `authorized_keys`. orca node key-reset 192.168.1.100 ``` +### `orca node drain` + +Drain a node: stop its allocations and mark it drained (REQ-061). + +``` +orca node drain [flags] +``` + +Marks the node as "draining" (scheduler skips draining nodes), stops +every running allocation via SSH (`systemctl stop orca-alloc-.service`), +waits for them to stop, and marks the node "drained". This is NOT +live-migration — use `orca job migrate` to reschedule first. + +| Flag | Type | Default | Description | +|------|------|---------|-------------| +| `--timeout` | duration | `30s` | Max time to wait for allocations to stop | + ### `orca node capacity` Manage node capacity declarations (bin-packing scheduler input). @@ -300,9 +251,9 @@ orca node capacity set [flags] | Flag | Type | Default | Description | |------|------|---------|-------------| -| `--cpu` | int64 | `0` | CPU capacity in millicores (1000 = 1 vCPU) | -| `--memory` | int64 | `0` | Memory capacity in MiB | -| `--disk` | int64 | `0` | Disk capacity in MiB | +| `--cpu` | int | `0` | CPU capacity in millicores (1000 = 1 vCPU) | +| `--memory` | int | `0` | Memory capacity in MiB | +| `--disk` | int | `0` | Disk capacity in MiB | | `--node` | string | `""` | Node ID (defaults to `self`) | **Example**: @@ -323,6 +274,159 @@ orca node capacity list --- +## `orca job` + +Manage orca jobs — run, list, stop, lint, verify, migrate, restart. + +### `orca job run` + +Run a job from a spec file. + +``` +orca job run [flags] +``` + +Dispatches by file extension: +- `.md` → Markdown frontmatter parser (v0.9 canonical) +- `.yaml` / `.yml` → YAML frontmatter parser +- `.hcl` → Legacy HCL adapter (deprecated) + +The v0.13 scheduler wiring (REQ-151) deploys to remote nodes via the +scheduler → emitter → SSH-push pipeline. When no remote nodes are +available, falls back to local execution (dev mode). + +| Flag | Type | Default | Description | +|------|------|---------|-------------| +| `--target` | string | `""` | Pin job to a specific node ID (overrides bin-packing scheduler) | +| `--idempotency-key` | string | `""` | Idempotency key for cross-node dispatch dedupe | + +**Examples**: +```bash +orca job run web-app.md +orca job run api.yaml --target node-abc-123 +orca job run worker.md --idempotency-key deploy-2026-08-05 +``` + +> **Deprecated**: `orca job run ` (legacy HCL jobspec) still +> works via the adapter but emits a deprecation warning. Migrate `.hcl` +> specs to `.md` (see [docs/jobspec.md](jobspec.md)). + +### `orca job list` + +List all jobs. + +``` +orca job list [flags] +``` + +| Flag | Type | Default | Description | +|------|------|---------|-------------| +| `--watch` | bool | `false` | Stream jobs until Ctrl-C (table refresh or `--json` per-event) | + +**Output columns**: `ID NAME STATUS EXIT` + +**Examples**: +```bash +orca job list +orca job list --watch # table refresh +orca job list --watch --json # NDJSON: {"event":"update","job":{...}} +``` + +### `orca job stop` + +Stop a running job by sending `systemctl stop orca-alloc--*` to +the node running the allocation via SSH, then mark the job as stopped +in the DB (REQ-158, P09). + +``` +orca job stop [job-id] [flags] +``` + +If `--peer` is not given, the node is looked up from the allocation +history. If no node is found, the DB status is updated anyway (soft +stop fallback for local-run jobs). + +| Flag | Type | Default | Description | +|------|------|---------|-------------| +| `--id` | string | `""` | Job ID (alternative to positional argument) | +| `--peer` | string | `""` | Peer address (host:port) running the allocation (auto-detected from alloc history if empty) | +| `--timeout` | duration | `2m0s` | SSH command timeout | + +**Example**: +```bash +orca job stop abc-123-def +orca job stop --id abc-123-def +orca job stop abc-123-def --peer 192.168.1.100:22 +``` + +### `orca job logs` + +Show task output for a job. + +``` +orca job logs [job-id] [flags] +``` + +| Flag | Type | Default | Description | +|------|------|---------|-------------| +| `--id` | string | `""` | Job ID (alternative to positional argument) | + +**Example**: +```bash +orca job logs abc-123-def +``` + +### `orca job lint` + +Lint a jobspec (schema, CEL, body, migration, best-practice) without +applying it (REQ-084). + +``` +orca job lint [flags] +``` + +| Flag | Type | Default | Description | +|------|------|---------|-------------| +| `--explain` | bool | `false` | Print the rationale for each finding | +| `--format` | string | `"text"` | Output format: `text` or `json` | + +Exit codes: 0 = no errors (warnings OK), 1 = errors found. + +### `orca job verify` + +Dry-run a jobspec through the lead (no apply). + +``` +orca job verify [flags] +``` + +Validates the spec through the render + txn pipeline without staging +or applying it. Reports any schema, constraint, or render errors. + +### `orca job migrate` + +Drain + reschedule a job onto a target node (REQ-116). + +``` +orca job migrate [flags] +``` + +Stops the job on its current node and re-submits it to the scheduler +(pinned to `--target` if given). + +### `orca job restart` + +Restart an allocation to pick up EnvironmentFile drift (REQ-113). + +``` +orca job restart [flags] +``` + +Restarts the systemd unit(s) for the job's allocation(s) via SSH so they +re-read the `.env` / `.env.secrets` files without a full redeploy. + +--- + ## `orca ns` Manage orca namespaces under `ORCA_HOME` (R-002). @@ -330,7 +434,7 @@ Manage orca namespaces under `ORCA_HOME` (R-002). Each namespace is a directory with `ns.md`, `.env`, `.env.secrets`, `db/`, `jobs/`, `alloc/`. The implicit root namespace `_defaults` always exists; every namespace inherits from `_defaults` and cannot -opt out. +opt out (D-185, D-187). ### `orca ns list` @@ -396,6 +500,260 @@ Exits 0 if valid, 1 on error. Runs over ALL namespaces under `ORCA_HOME` (parsing + resolving validates cycles and missing parents across the set). +### `orca ns inherit` + +Set the parent namespace for a namespace (R-002). + +``` +orca ns inherit [flags] +``` + +Updates `ns.md` frontmatter (`parents` field) and validates the new +chain has no cycles. The implicit root `_defaults` is always appended +last (D-185). + +| Flag | Type | Default | Description | +|------|------|---------|-------------| +| `--parent` | string | `""` | Parent namespace to inherit from (required) | + +**Example**: +```bash +orca ns inherit staging --parent prod +``` + +### `orca ns set-constraint` + +Set a constraint on a namespace (stored in `ns.md` frontmatter). + +``` +orca ns set-constraint = +``` + +Constraints are `key=value` strings (e.g., `max-allocs=10`) stored in +`ns.md` frontmatter and unioned across the inheritance chain by the +resolver. + +**Example**: +```bash +orca ns set-constraint prod max-allocs=10 +``` + +--- + +## `orca secrets` + +Manage encrypted `.env.secrets` per namespace (REQ-080). + +Each namespace has a `.env.secrets` file at `//.env.secrets` +containing one `base64(nonce||ciphertext||tag)` blob per line. Encryption +is AES-256-GCM with a per-namespace HKDF-SHA256 sub-key derived from the +cluster `master.key` (mode 0600). The AAD is the 1-based line number, +defeating line-swap attacks. + +### `orca secrets set` + +Encrypt `KEY=value` and add or update it in `/.env.secrets`. + +``` +orca secrets set +``` + +### `orca secrets get` + +Decrypt the secret named `KEY` from `/.env.secrets` and +print its value to stdout. + +``` +orca secrets get +``` + +The value is printed with no trailing newline added. The value is +never logged via `slog`. + +### `orca secrets list` + +List the keys of all secrets stored in `/.env.secrets`. +Values are never printed. + +``` +orca secrets list +``` + +### `orca secrets rotate` + +Re-encrypt the secret named `KEY` with a fresh nonce. The plaintext +value is unchanged. + +``` +orca secrets rotate +``` + +### `orca secrets delete` + +Remove the secret named `KEY` from `/.env.secrets`. + +``` +orca secrets delete +``` + +### `orca secrets rotate-master` + +Generate a new master key, re-encrypt every namespace's `.env.secrets` +under the new key, and re-seal the master key to OIDC (REQ-129, C-30). + +``` +orca secrets rotate-master [flags] +``` + +| Flag | Type | Default | Description | +|------|------|---------|-------------| +| `--dry-run` | bool | `false` | Report affected namespaces without writing (C-30) | + +Atomic per-namespace; automatic rollback to the old key on any failure. + +--- + +## `orca acl` + +Manage the cluster ACL (P02, v0.11). Identities are either +SPIFFE workload URIs (`spiffe://orca.local/ns//sa//`) or +operator token IDs. Permissions are deny-by-default. + +State is stored at `ClusterDir()/acl.json` (mode 0600). + +### `orca acl grant` + +Grant permissions to an identity on a namespace. + +``` +orca acl grant [flags] +``` + +| Flag | Type | Default | Description | +|------|------|---------|-------------| +| `--namespace` | string | `""` | Namespace scope (required for tokens; defaults to spiffe path ns) | +| `--permissions` | string | `"read"` | Comma-separated list of `read`,`write`,`admin` | + +### `orca acl revoke` + +Revoke an identity's entry on a namespace. + +``` +orca acl revoke [flags] +``` + +| Flag | Type | Default | Description | +|------|------|---------|-------------| +| `--namespace` | string | `""` | Namespace scope (required for tokens; defaults to spiffe path ns) | + +### `orca acl list` + +List all ACL entries. + +``` +orca acl list +``` + +### `orca acl check` + +Check whether an identity has the given permission on a namespace. +Exits 0 if allowed, 1 if denied. + +``` +orca acl check [flags] +``` + +| Flag | Type | Default | Description | +|------|------|---------|-------------| +| `--namespace` | string | `""` | Namespace scope (required for tokens; defaults to spiffe path ns) | +| `--permission` | string | `"read"` | Permission to check: `read`, `write`, or `admin` | + +--- + +## `orca cluster` + +Cluster-wide operations: cutover, lead rotation, mixed-version +compatibility checks, and master-key seal/unseal (REQ-147, D-241, C-35). + +### `orca cluster cutover` + +Stop v0.8 orca daemons and adopt running allocs (P14b). + +``` +orca cluster cutover +``` + +Stops the v0.8 orca-daemon on every peer that still runs one, discovers +its running allocations (`orca-alloc-*.service`), and adopts each into +the SSH-push path. Idempotent. + +### `orca cluster rotate-lead` + +Rotate the cluster lead to a new bare Linux node (REQ-114, R-003). + +``` +orca cluster rotate-lead --to [flags] +``` + +Steps: verify the new lead is a registered bare Linux node (Proxmox +nodes are ineligible — hypervisor kernel is shared with guests), copy +the cluster CA + master key + config + txn log to the new lead via +SSH, update local cluster state, rotate the SSH keypair, and deploy +the new public key to every peer. Workloads keep running. + +| Flag | Type | Default | Description | +|------|------|---------|-------------| +| `--to` | string | `""` | New lead host (required) | +| `--debug` | bool | `false` | Enable debug logging | + +### `orca cluster compat-check` + +Check mixed-version tolerance across peers (REQ-065, C-13). + +``` +orca cluster compat-check +``` + +Verifies each peer's orca version, txn manifest format compatibility, +render-contract JSON schema compatibility, and that no new required +fields break old peers. + +### `orca cluster seal` + +Seal the cluster master key (REQ-147, D-241, C-35). + +``` +orca cluster seal +``` + +Encrypts the raw master key at `ClusterDir()/master.key` with a key +derived from the OIDC ID token subject (if `orca auth login` has been +run) or the cluster CA fingerprint (mTLS-only offline path). 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. + +### `orca cluster unseal` + +Unseal the cluster master key (REQ-147, D-241, C-35). + +``` +orca cluster unseal [flags] +``` + +Reads the sealed blob at `ClusterDir()/master.key.sealed` and unwraps +the master key using the OIDC ID token subject or the cluster CA +fingerprint. The unwrapped master key is written back to +`ClusterDir()/master.key` (0600). The raw key is zeroed from memory on +process exit. + +With `--recovery`, the operator is prompted for 3 of the 5 Shamir +shards printed at seal time (IdP-lost recovery path). + +| Flag | Type | Default | Description | +|------|------|---------|-------------| +| `--recovery` | bool | `false` | Shamir recovery: prompt for 3-of-5 shards (if IdP lost) | + --- ## `orca doctor` @@ -417,13 +775,21 @@ report per check. | `orca doctor network` | Network reachability via mTLS `/healthz` probe | | `orca doctor db` | Database integrity (`PRAGMA integrity_check` + migration version) | | `orca doctor os` | OS detection self-check (verifies `/etc/os-release` matches stored node) | -| `orca doctor proxmox` | Proxmox node reachability via SSH `pveversion`/`pvecmd status` probe | +| `orca doctor proxmox` | Proxmox node reachability via SSH `pveversion`/`pvecmd status` probe; audits `/etc/sudoers.d/orca` | +| `orca doctor nft` | nftables ingress ruleset self-check (R-017); audits live ruleset against on-disk hash | +| `orca doctor audit` | Verify the audit log hash chain (tamper-evidence check, REQ-125) | +| `orca doctor modes` | Verify security-sensitive file permissions (REQ-033/130, F13) | +| `orca doctor oidc` | Check the bundled Dex OIDC provider health (P06) | +| `orca doctor db-retention` | Check DB row counts for unbounded growth (REQ-158) | +| `orca doctor no-orca-on-server` | Verify no orca binary/service/process on peers (REQ-086, R-001) | **Example**: ```bash orca doctor orca doctor cert orca doctor proxmox --json +orca doctor audit +orca doctor modes ``` --- @@ -448,62 +814,635 @@ orca audit list [flags] --- -## `orca version` +## `orca backup` -Print version information. +Create a signed tar.gz backup of `ORCA_HOME` (P04). ``` -orca version +orca backup [flags] ``` -**Output**: +Walks `ORCA_HOME` recursively, excludes `/run/orca/*`, `*.sock`, +`*.db-wal`, `*.db-shm`, packs the rest into a tar.gz, and computes an +HMAC-SHA256 signature using the cluster master key. + +| Flag | Type | Default | Description | +|------|------|---------|-------------| +| `--out` | string | `""` | Output tarball path (default: `orca-backup-.tar.gz` in CWD) | + +--- + +## `orca restore` + +Restore `ORCA_HOME` from a verified signed backup (P04/P07). + ``` -orca version v0.9.1 - git commit: abc1234 - build time: 2026-08-05T20:30:00Z +orca restore [flags] +``` + +Verifies the HMAC-SHA256 signature before extracting. Reconciles with +live state: refuses to clobber running allocations unless `--force` is +given. With `--dry-run`, extracts to a temp dir and reports what would +be restored without touching `ORCA_HOME`. + +| Flag | Type | Default | Description | +|------|------|---------|-------------| +| `--in` | string | `""` | Input tarball path (required) | +| `--force` | bool | `false` | Stop running allocs before restore | +| `--dry-run` | bool | `false` | Extract to temp dir; report without writing | + +--- + +## `orca upgrade` + +Upgrade orca to a new version (thin wrapper; R-017 cutover). + +``` +orca upgrade [flags] +``` + +Handles the R-017 binding cutover (Traefik :443 → 127.0.0.1:8443 + +nftables), creates the `orca` system user on existing peers (C-27), +optionally imports the v0.8 internal CA into step-ca, and runs +`Migratev08tov11` if a v0.8 layout is detected. Idempotent. + +| Flag | Type | Default | Description | +|------|------|---------|-------------| +| `--to` | string | `""` | Target version (e.g., `v0.12.10`) (required) | +| `--dry-run` | bool | `false` | Report what would be done without making changes | +| `--force` | bool | `false` | Skip cutover verification (use with caution) | +| `--import-ca` | bool | `false` | Import the v0.8 internal CA into step-ca during upgrade | + +--- + +## `orca metrics` + +Start a standalone HTTP server exposing Prometheus text-exposition +metrics at `/metrics` and a liveness probe at `/healthz`. + +``` +orca metrics [flags] +``` + +No orca daemon required (R-001). Polls cluster state periodically and +updates gauges. + +| Flag | Type | Default | Description | +|------|------|---------|-------------| +| `--addr` | string | `":9100"` | Listen address for the metrics HTTP server | + +--- + +## `orca logs` + +Aggregate journald logs across registered nodes via SSH fanout +(REQ-117). + +``` +orca logs [flags] +``` + +Runs `journalctl -u 'orca-alloc-*' --since --output json` on each +peer, parses the JSON-per-line output, and streams the entries with a +`[]` prefix (multi-node) or raw (single-node). `--json` +outputs the raw journalctl JSON lines verbatim. + +| Flag | Type | Default | Description | +|------|------|---------|-------------| +| `--all-nodes` | bool | `false` | Fan out to every registered node | +| `--node` | string | `""` | Single node host to query | +| `--job` | string | `""` | Filter units to `orca-alloc--*` | +| `--since` | string | `""` | Duration (e.g., `5m`, `1h`, `30m`) | +| `--json` | bool | `false` | Output raw journalctl JSON lines | + +**Examples**: +```bash +orca logs --all-nodes --since 5m +orca logs --node web-1 --since 1h +orca logs --all-nodes --job web --since 30m --json ``` --- -## `orca status` +## `orca cache` -Show orca daemon status. +Manage the CLI-side SQLite cache (R-008) at `ORCA_HOME/orca_cache.db`. ``` -orca status +orca cache ``` -> **Deprecated**: The daemon model is deprecated in v0.9 (replaced by -> SSH-push, R-001). This command returns a stub status. Removed in -> v0.11. +Read-only list commands (`node`/`job`/`ns list`) populate the cache; +writes bypass it. The `--watch` flag bypasses the cache entirely +(streaming). + +### `orca cache show` + +Print cache stats (per-class counts, sizes, oldest entry). + +``` +orca cache show +``` + +### `orca cache invalidate` + +Drop all entries for a cache class. + +``` +orca cache invalidate +``` + +**Example**: +```bash +orca cache invalidate nodes +``` + +### `orca cache invalidate-all` + +Drop every entry in the cache. + +``` +orca cache invalidate-all +``` + +--- + +## `orca drift` + +Detect and remediate control-plane drift (P10b). Orca's drift detector +is a BACKUP (R-019): the primary consistency mechanism is systemd / +Traefik / step-ca / Syncthing themselves. Pre-flight drift blocks +txn apply (R-020). + +``` +orca drift +``` + +### `orca drift show` + +Show the aggregated drift events from the lead. + +``` +orca drift show [flags] +``` + +| Flag | Type | Default | Description | +|------|------|---------|-------------| +| `--peer` | string | `""` | Filter to a single peer host | + +### `orca drift watch` + +Stream drift events from the lead's aggregated state. + +``` +orca drift watch [flags] +``` + +| Flag | Type | Default | Description | +|------|------|---------|-------------| +| `--interval` | duration | `2s` | Poll interval | +| `--paths` | strings | `[]` | Comma-separated glob patterns to watch (default: all) | + +### `orca drift acknowledge` + +Record operator acknowledgment for the given path in +`drift-acknowledgments.json` on the lead. Acknowledged drift no longer +blocks txn apply for that namespace (R-020). + +``` +orca drift acknowledge [flags] +``` + +| Flag | Type | Default | Description | +|------|------|---------|-------------| +| `--timeout` | duration | `2m0s` | SSH command timeout | + +### `orca drift remediate` + +Trigger `orca-remediate.sh` on the lead for the given path. + +``` +orca drift remediate [flags] +``` + +| Flag | Type | Default | Description | +|------|------|---------|-------------| +| `--force` | bool | `false` | Bypass the cooldown window (C4) | +| `--timeout` | duration | `2m0s` | SSH command timeout | + +### `orca drift config` + +Show or validate the drift config. + +``` +orca drift config +``` + +#### `orca drift config show` + +Show the current drift config. + +``` +orca drift config show +``` + +#### `orca drift config validate` + +Validate the drift config. + +``` +orca drift config validate +``` + +| Flag | Type | Default | Description | +|------|------|---------|-------------| +| `--config` | string | `""` | Path to drift config JSON (default: built-in) | + +--- + +## `orca txn` + +Manage orca's transactional control-plane updates (P10a). + +A transaction (txn) is a content-addressed desired-state bundle +(`apply.sh` + `verify.sh` + `rollback.sh` + signed manifest) staged to +the lead peer and applied idempotently. Cluster-wide txns require +explicit operator acknowledgement; namespace-scoped txns only touch +the given namespace (C-23). + +``` +orca txn +``` + +### `orca txn apply` + +Apply a staged txn on the lead peer (idempotent; C-09 failure contract). + +``` +orca txn apply [flags] +``` + +The txn must already be staged on the lead under +`/run/orca/txns//`. Cluster-wide txns (no `--namespace`) +require `--force` + `--i-understand-the-risk` (or `--yes` for +non-interactive). Namespace-scoped txns (`--namespace `) only +touch that namespace. + +| Flag | Type | Default | Description | +|------|------|---------|-------------| +| `--namespace` | string | `""` | Namespace scope (cluster-wide if empty) | +| `--force` | bool | `false` | Acknowledge cluster-wide risk | +| `--i-understand-the-risk` | bool | `false` | Acknowledge cluster-wide risk (explicit) | +| `--yes` | bool | `false` | Non-interactive acknowledgement (skip prompt) | + +### `orca txn list` + +List staged + applied transactions. + +``` +orca txn list +``` + +### `orca txn show` + +Show txn details (desired state, manifest, status). + +``` +orca txn show +``` + +### `orca txn rollback` + +Run `rollback.sh` for a staged txn on the lead peer. + +``` +orca txn rollback [flags] +``` + +| Flag | Type | Default | Description | +|------|------|---------|-------------| +| `--lead` | string | `""` | Lead peer address (host:port) | +| `--timeout` | duration | `2m0s` | SSH rollback timeout | + +--- + +## `orca nft` + +Inspect and manage the nftables ingress ruleset (R-017). Orca's ingress +is hybrid: Traefik binds `127.0.0.1:8443` and nftables DNATs the public +`:443` to it. + +``` +orca nft +``` + +### `orca nft show` + +Show the live `inet orca-ingress` table on the peer (default: lead). + +``` +orca nft show [flags] +``` + +| Flag | Type | Default | Description | +|------|------|---------|-------------| +| `--peer` | string | `""` | Peer host to query (default: lead) | + +### `orca nft diff` + +Compare the live nft ruleset against the expected from a txn. + +``` +orca nft diff [flags] +``` + +| Flag | Type | Default | Description | +|------|------|---------|-------------| +| `--against` | string | `""` | Txn-ID to diff against (required) | + +### `orca nft doctor` + +Alias for `orca doctor nft`. + +``` +orca nft doctor +``` + +### `orca nft country` + +GeoIP country-block management (opt-in). Manages the `orca_geoip_block` +nft set. + +``` +orca nft country +``` + +#### `orca nft country block add` + +Add country codes to the GeoIP block set. + +``` +orca nft country block add +``` + +Adds one or more ISO-3166 alpha-2 country codes (comma-separated) to the +`orca_geoip_block` nft set on the lead. + +**Example**: +```bash +orca nft country block add RU,CN +``` + +### `orca nft rate` + +Rate-limit meter management. + +``` +orca nft rate +``` + +#### `orca nft rate limit set` + +Adjust the forward-chain rate-limit meter. + +``` +orca nft rate limit set [flags] +``` + +| Flag | Type | Default | Description | +|------|------|---------|-------------| +| `--rate` | int | `0` | Rate limit in packets/second (required, >0) | + +Re-renders `/etc/nftables.d/orca.nft` with the new rate and applies it +on the lead. The burst is set to 2x the rate when not specified. + +--- + +## `orca cert` *(deprecated)* + +Manage orca certificates (CA, server, rotation). + +> **Deprecated**: The v0.9 re-architecture replaces the internal CA +> with step-ca (D-101/REQ-076). The `orca cert` command tree is +> retained for the dual-write window and scheduled for deletion. +> The v0.12 milestone closed the dual-write window (REQ-138). + +``` +orca cert +``` + +### Subcommands + +| Command | Description | +|---------|-------------| +| `orca cert ca-init` | Initialize a local orca CA (`ca.crt` + `ca.key`) | +| `orca cert gen` | Generate a server cert (CSR + sign) | +| `orca cert show` | Print the server cert (private keys redacted; REQ-035) | +| `orca cert renew` | Rotate the server cert (REQ-034) | +| `orca cert fingerprint` | Print the SHA-256 fingerprint of `ca.crt` or `server.crt` | + +--- + +## `orca auth` + +OIDC authentication for human operators (zero-trust identity, R-021). + +Orca uses OIDC for human-identity authentication (R-021: no Orca-issued +credentials). The bundled Dex (deployed by `orca auth init-idp`) is the +default issuer; `oidc.issuer` in config can repoint to a BYO external +IdP. The CLI performs the authorization-code + PKCE + local loopback +redirect flow; headless/CI uses the device-code flow. + +``` +orca auth +``` + +### `orca auth login` + +Authenticate via OIDC (browser or device-code flow). + +``` +orca auth login [flags] +``` + +By default, opens the default browser for the authorization-code + PKCE ++ local loopback redirect flow. Credentials are stored at +`~/.orca/credentials.json` (0600, short-lived + refresh). + +| Flag | Type | Default | Description | +|------|------|---------|-------------| +| `--client-id` | string | `"orca-cli"` | OIDC client ID | +| `--client-secret` | string | `""` | OIDC client secret (confidential clients; public PKCE clients omit) | +| `--device-code` | bool | `false` | Use device-code flow (headless/CI) | + +### `orca auth logout` + +Clear the stored OIDC credentials. + +``` +orca auth logout +``` + +### `orca auth status` + +Show the current OIDC authentication status. + +``` +orca auth status +``` + +### `orca auth init-idp` + +Bootstrap the bundled Dex OIDC provider on the lead. + +``` +orca auth init-idp [flags] +``` + +Deploys a bundled Dex instance on the lead node as a systemd unit, +fronted by Traefik (R-017, step-ca cert). The WebAuthn connector (P05) +provides the password-free upstream authenticator. + +| Flag | Type | Default | Description | +|------|------|---------|-------------| +| `--rp-id` | string | `""` | WebAuthn relying-party ID (cluster Traefik domain; C-38) | + +### `orca auth register` + +Open the WebAuthn passkey registration page in the browser. + +``` +orca auth register [flags] +``` + +Opens the browser to the Dex WebAuthn registration page at +`https:///orca/webauthn/register`. The operator authenticates +via an existing session, then registers a passkey (biometric or +security key). Use `--no-browser` to print the URL instead of opening +a browser. + +| Flag | Type | Default | Description | +|------|------|---------|-------------| +| `--no-browser` | bool | `false` | Print the URL instead of opening a browser | + +See [docs/webauthn.md](webauthn.md) for the WebAuthn / passkey guide. + +--- + +## `orca peer-setup` + +Create the orca system user + drift-events dir on a peer (REQ-111). + +``` +orca peer-setup [flags] +``` + +SSH to `` and idempotently create the `orca` system user +(`useradd -r orca -s /usr/sbin/nologin`) and +`/etc/orca/state/drift-events/` owned by `orca:orca`. Also detects NFS +on `/etc/orca` (REQ-112/D-233) and logs a warning. Use `--no-orca-user` +to skip user creation (for environments with an existing service +account). + +| Flag | Type | Default | Description | +|------|------|---------|-------------| +| `--no-orca-user` | bool | `false` | Skip orca system user creation (env has existing service account) | +| `--timeout` | duration | `2m0s` | SSH command timeout | + +--- + +## `orca collector` + +Manage the lead-side collector: the aggregator (`orca-aggregate.sh`, +10s cadence, merges per-peer state into `cluster.json` + drift-event +aggregation per REQ-107) and the watchdog (`orca-watchdog.sh`, 30s +cadence, detects aggregator starvation per C-11). Opt-in (C-12). + +``` +orca collector +``` + +### `orca collector start` + +Emit the collector scripts + systemd units and enable them. + +``` +orca collector start [flags] +``` + +| Flag | Type | Default | Description | +|------|------|---------|-------------| +| `--dry-run` | bool | `false` | Emit scripts/units without enabling or running systemctl | +| `--root` | string | `"/"` | Install root for emitted paths (for testing use a temp dir) | + +### `orca collector stop` + +Disable and remove the collector scripts + systemd units. + +``` +orca collector stop [flags] +``` + +| Flag | Type | Default | Description | +|------|------|---------|-------------| +| `--dry-run` | bool | `false` | Remove scripts/units without disabling or running systemctl | +| `--root` | string | `"/"` | Install root for emitted paths (for testing use a temp dir) | + +### `orca collector status` + +Report whether the collector (aggregator + watchdog) is running. + +``` +orca collector status +``` + +--- + +## `orca daemon` *(deprecated)* + +Run the orca daemon (HTTP API + health checks). + +> **Deprecated**: The v0.9 re-architecture replaces the daemon with +> SSH-push (R-001). The daemon is repurposed to `drain-and-stop` in +> v0.11-P05. The v0.12 milestone closed the dual-write window (REQ-138). +> The legacy daemon plaintext/mTLS mode is deleted; mTLS is available +> for metrics/health probes only. + +``` +orca daemon [flags] +``` + +| Flag | Type | Default | Description | +|------|------|---------|-------------| +| `--addr` | string | `":8080"` | Listen address | +| `--pprof` | bool | `false` | Enable pprof endpoint (loopback-only by default) | --- ## Deprecated commands The following commands are from the v0.8 daemon/mTLS model and are -**deprecated in v0.9**. They still work during the dual-write window -but emit `slog.Warn` deprecation warnings. They will be **removed in -v0.11**. +**deprecated**. The v0.12 milestone (REQ-138) closed the dual-write +window — the legacy CA/mTLS/daemon paths are deleted. The `orca cert` +command tree and `orca daemon` are retained as thin stubs during the +final removal cycle. -> **`orca daemon`** — Run the orca daemon (HTTP API + health checks). -> The v0.9 re-architecture replaces the daemon with SSH-push (R-001). -> The daemon is repurposed to `drain-and-stop` in v0.11-P05 and deleted -> in v0.11-P14. Flags: `--addr` (default `:8080`), `--pprof` (pprof -> endpoint, default disabled). +> **`orca daemon`** — Run the orca daemon. Replaced by SSH-push +> (R-001). The daemon is repurposed to `drain-and-stop` and the legacy +> plaintext/mTLS mode is deleted (REQ-138). -> **`orca cert`** — Manage orca certificates (CA, server, rotation). -> The v0.9 re-architecture replaces the internal CA with step-ca -> (D-101). Subcommands: `ca-init`, `gen`, `show`, `renew`, -> `fingerprint`. Removed in v0.11. +> **`orca cert`** — Manage orca certificates. Replaced by step-ca +> (D-101). Scheduled for deletion. > **`orca node join` (mTLS path)** — The localhost mTLS join path > (without `--type proxmox`) is deprecated. The v0.9 canonical path is > SSH-push (`--type proxmox`) or local execution (no join needed). > **`orca job run `** — Legacy HCL jobspec. Migrate to `.md` -> (see [docs/jobspec.md](jobspec.md)). The HCL adapter preserves -> `orca job run old-spec.hcl` during the migration window. +> (see [docs/jobspec.md](jobspec.md)). + +> **`orca status`** — v0.1 stub. Use `orca node list` + `orca metrics +> /healthz`. To suppress deprecation warnings during migration, use `--no-deprecation-warnings`: @@ -519,4 +1458,8 @@ orca --no-deprecation-warnings daemon - [docs/ingress.md](ingress.md) — Traefik ingress configuration guide - [docs/namespace.md](namespace.md) — Namespace and path layout - [docs/install.md](install.md) — Installation guide -- [examples/full-stack/](../examples/full-stack/) — Full-stack example with ingress \ No newline at end of file +- [docs/security-runbook.md](security-runbook.md) — Security runbook (seal/unseal, rotation, incident response) +- [docs/webauthn.md](webauthn.md) — WebAuthn / passkey registration +- [docs/threat-model.md](threat-model.md) — STRIDE threat model + zero-trust architecture +- [docs/oidc.md](oidc.md) — OIDC configuration (Dex quickstart, BYO IdP) +- [examples/full-stack/](../examples/full-stack/) — Full-stack example with ingress diff --git a/docs/namespace.md b/docs/namespace.md index 4e59b8e..4faee57 100644 --- a/docs/namespace.md +++ b/docs/namespace.md @@ -8,8 +8,8 @@ directory holds cluster-wide artifacts shared across namespaces. > **v0.9 layout (canonical)**: This document describes the v0.9 > multi-namespace layout. The v0.8 flat layout (`orca.db`, `ca.crt`, -> `server.crt` at the root) is deprecated and will be removed in -> v0.11. See [v0.8 flat layout](#deprecated-v08-flat-layout) below. +> `server.crt` at the root) is deprecated and removed in v0.12 +> (REQ-138). ## Namespace root resolution @@ -51,12 +51,16 @@ $ORCA_HOME/ ├── cluster/ # cluster-wide (NOT a workload namespace) │ ├── ca.crt, ca.key # step-ca root (R-006, D-101) │ ├── 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) │ ├── known_hosts # SSH known_hosts (D-035) │ ├── orca_ssh_key # orca SSH private key (D-037) │ ├── orca_ssh_key.pub # orca SSH public key │ ├── peers// # per-peer directory │ ├── 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 ├── _defaults/ # implicit root namespace (always exists) │ ├── ns.md # namespace frontmatter (kind: Namespace) @@ -80,14 +84,16 @@ $ORCA_HOME/ exists. Every namespace inherits from `_defaults` and cannot opt out (D-185, D-187). - **`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 `db/orca.db` (R-002). No namespace column in SQLite. - **Namespace inheritance**: child namespaces inherit env and constraints from parents (via `ns.md` frontmatter `parents:` field). `_defaults` is always appended last in the inheritance chain. - **`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/`) @@ -134,6 +140,50 @@ orca ns validate prod 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 --parent +``` + +**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 = +``` + +**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. ## `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 in v0.9**: The v0.8 flat layout (`orca.db`, `ca.crt`, -> `ca.key`, `server.crt`, `server.key` at the namespace root) is -> superseded by the v0.9 multi-namespace layout (R-002). The v0.8 -> layout is supported during the dual-write window via -> `internal/certpaths` (a thin shim) and will be removed in v0.11. +> **Removed in v0.12** (REQ-138): The v0.8 flat layout (`orca.db`, +> `ca.crt`, `ca.key`, `server.crt`, `server.key` at the namespace root) +> is superseded by the v0.9 multi-namespace layout (R-002) and the +> dual-write window is closed. 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 per-namespace `db/` (SQLite) to support multi-tenancy (R-002). The -`orca doctor --legacy-paths` command (v0.11-P14c) will detect v0.8 -residue and recommend migration. +`internal/certpaths` shim that supported the dual-write window is +removed in v0.12. ## See also - [Install Guide](install.md) — 1-liner install with `install.sh`. - [Docker Guide](docker.md) — running orca in a container. -- [CLI Reference](cli.md) — `orca ns` subcommands. -- [Jobspec Reference](jobspec.md) — markdown frontmatter schema. \ No newline at end of file +- [CLI Reference](cli.md#orca-ns) — `orca ns` subcommands. +- [Jobspec Reference](jobspec.md) — markdown frontmatter schema. diff --git a/docs/security-runbook.md b/docs/security-runbook.md index c11aab1..8db3f2c 100644 --- a/docs/security-runbook.md +++ b/docs/security-runbook.md @@ -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; - 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 Seal/Unseal (REQ-147, P05) -## 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, -re-encrypts all namespace secrets, re-seals. Atomic + automatic rollback. +### Seal + +```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 -1. Revoke the compromised identity (OIDC user/group or SPIFFE SVID). -2. Rotate the master key (`orca secrets rotate-master`). -3. Review the audit log (`orca doctor audit` verifies the hash chain). -4. If the master key is compromised, all historical secrets are - compromised (no forward secrecy). +1. **Revoke the compromised identity** (OIDC user/group or SPIFFE SVID). +2. **Rotate the master key** (`orca secrets rotate-master`). +3. **Review the audit log** (`orca doctor audit` verifies the hash + chain; `orca audit list` shows entries). +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 -expected allowlist (pct + qm with NOEXEC; apt-get/dpkg excluded). +```bash +orca doctor oidc +``` -## nft Audit - -`orca doctor nft` audits the live nftables ruleset against the emitted one. +Checks the bundled Dex OIDC provider health. Verifies the Dex systemd +unit is running and the `/.well-known/openid-configuration` endpoint +responds. Run after `orca auth init-idp` or after a Dex config change. diff --git a/docs/webauthn.md b/docs/webauthn.md index 8baf3c2..8596a48 100644 --- a/docs/webauthn.md +++ b/docs/webauthn.md @@ -1,27 +1,97 @@ -# WebAuthn / Passkeys (v0.12) +# WebAuthn / Passkeys (v0.13) ## Overview -The bundled Dex uses a custom WebAuthn connector for password-free -authentication. Passkeys are public-key credentials — the private key -never leaves the authenticator (TPM/security key/phone Secure Enclave). +The bundled Dex uses a custom WebAuthn connector (`orca-webauthn-connector`, +REQ-148) for password-free authentication. Passkeys are public-key +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 -`orca auth register` opens the browser to the Dex WebAuthn endpoint. -After the ceremony (biometric/security key), Dex maps the credential -ID to an OIDC `sub`. Credentials stored at -`ClusterDir()/webauthn-credentials.db` (0600, public keys only). +```bash +orca auth register [--no-browser] +``` + +Opens the browser to the Dex WebAuthn registration page at +`https:///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 -The relying-party ID is the cluster's Traefik-served domain -(`--rp-id` on `orca auth init-idp`). HTTPS secure context is provided -by Traefik (step-ca cert, R-017). +The relying-party ID is the cluster's Traefik-served domain, set via +`--rp-id` on `orca auth init-idp` (C-38). The RP ID **must** match the +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 1. `orca init` bootstraps the cluster CA (step-ca, mTLS-only). -2. `orca auth init-idp` deploys Dex behind Traefik (step-ca cert). -3. First operator registers a passkey via the mTLS-authenticated session. -4. Subsequent operators use WebAuthn. +2. `orca auth init-idp --rp-id ` deploys Dex behind + Traefik (step-ca cert) with the WebAuthn connector configured. +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 diff --git a/internal/cli/job.go b/internal/cli/job.go index 2936725..64e0b9b 100644 --- a/internal/cli/job.go +++ b/internal/cli/job.go @@ -47,8 +47,8 @@ var ( ) var jobRunCmd = &cobra.Command{ - Use: "run ", - Short: "Run a job from an HCL spec file", + Use: "run ", + 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.", Args: cobra.ExactArgs(1), RunE: func(cmd *cobra.Command, args []string) error { @@ -216,7 +216,7 @@ func renderJobs(cmd *cobra.Command, jobs []*model.Job) error { return printJSON(jobs) } if len(jobs) == 0 { - fmt.Fprintln(cmd.OutOrStdout(), "No jobs. Use 'orca job run ' to submit one.") + fmt.Fprintln(cmd.OutOrStdout(), "No jobs. Use 'orca job run ' to submit one.") return nil } fmt.Fprintf(cmd.OutOrStdout(), "%-36s %-20s %-12s %-8s\n", "ID", "NAME", "STATUS", "EXIT") diff --git a/internal/cli/status.go b/internal/cli/status.go index 50fb7ef..3a22cb5 100644 --- a/internal/cli/status.go +++ b/internal/cli/status.go @@ -6,9 +6,21 @@ import ( var statusCmd = &cobra.Command{ Use: "status", - Short: "Show orca daemon status", - Long: "Display the current status of the local orca daemon, including version, uptime, and connection info.", + Short: "Show orca daemon status (deprecated)", + 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 { + 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{ "version": version, "daemon": "stopped", @@ -23,7 +35,7 @@ var statusCmd = &cobra.Command{ } printText("orca daemon status\n") 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(" phase: %s\n", "1-cli-skeleton") printText(" milestone: %s\n", "v0.1") diff --git a/internal/proxmox/bootstrap.go b/internal/proxmox/bootstrap.go index fbb7bf1..cd6063e 100644 --- a/internal/proxmox/bootstrap.go +++ b/internal/proxmox/bootstrap.go @@ -3,7 +3,7 @@ // // The bootstrap sequence (run via `orca node join --type proxmox`): // 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 // 4. Create the `orca` Linux system user (config-overridable name) // 5. Create the OrcaOperator PVE role with least-privilege privileges @@ -16,9 +16,9 @@ // 10. Return the node metadata for the caller to persist // // All steps are idempotent (D-036): re-running the bootstrap on an -// already-configured host is a no-op. The password is never persisted -// (D-031) — it is used only for the initial SSH auth and pubkey -// deployment; subsequent orca→Proxmox access uses the deployed SSH key. +// already-configured host is a no-op. Authentication is key-based (R-021) +// (D-031): the orca SSH key is used for the initial SSH auth and +// pubkey deployment; subsequent orca→Proxmox access uses the same key. package proxmox import ( @@ -143,7 +143,7 @@ func BootstrapProxmox(ctx context.Context, opts Options) (*Result, error) { 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 // callback that fails closed on mismatch (AD-028); otherwise use the // TOFU known_hosts capture callback (D-035). The TOFU wrapper fixes diff --git a/scripts/verify-docs.sh b/scripts/verify-docs.sh new file mode 100755 index 0000000..847f8e5 --- /dev/null +++ b/scripts/verify-docs.sh @@ -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 \`` or `## \`orca \` *(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