From cf0df0f157d57f0fd49fa14705b1457773251416 Mon Sep 17 00:00:00 2001 From: Jon Chery Date: Fri, 7 Aug 2026 10:45:07 +0000 Subject: [PATCH] docs(P00): v0.12 security-hardening phase 0 (specify/clarify/research/ideate/plan/grill) ---ci--- project: orca phase: 0 milestone: v0.12 status: specify ---/ci--- Threat-model review of entire surface incl OS (25 findings F1..F25). Adopts R-021 (no Orca credentials: human=OIDC, machine=mTLS/SPIFFE). Bundled Dex + WebAuthn (passkeys) as default password-free authenticator. Master key seal-to-OIDC + Shamir 3-of-5 recovery. 30 net-new requirements (REQ-119..REQ-148). 29 phases. Binding conditions C-29..C-38. --- .ciagent/CHECKPOINT.json | 41 ++-- .ciagent/CLARIFY_v0.12.md | 162 +++++++++++++++ .ciagent/GRILL_v0.12.md | 170 ++++++++++++++++ .ciagent/IDEATION_v0.12.md | 183 +++++++++++++++++ .ciagent/PLAN_v0.12.md | 395 +++++++++++++++++++++++++++++++++++++ .ciagent/PROJECT.md | 96 +++++++++ .ciagent/REQUIREMENTS.md | 55 ++++++ .ciagent/RESEARCH_v0.12.md | 231 ++++++++++++++++++++++ .ciagent/ROADMAP.md | 149 ++++++++++++++ .ciagent/config.json | 2 +- 10 files changed, 1457 insertions(+), 27 deletions(-) create mode 100644 .ciagent/CLARIFY_v0.12.md create mode 100644 .ciagent/GRILL_v0.12.md create mode 100644 .ciagent/IDEATION_v0.12.md create mode 100644 .ciagent/PLAN_v0.12.md create mode 100644 .ciagent/RESEARCH_v0.12.md diff --git a/.ciagent/CHECKPOINT.json b/.ciagent/CHECKPOINT.json index 44567f4..c8a3471 100644 --- a/.ciagent/CHECKPOINT.json +++ b/.ciagent/CHECKPOINT.json @@ -1,30 +1,19 @@ { - "phase": 16, - "stage": "complete", - "milestone": "v0.11", - "milestone_slug": "production-hardening", - "phase_role": "final", + "phase": 0, + "stage": "ship", + "milestone": "v0.12", + "milestone_slug": "security-hardening", + "phase_role": "pre_execution", "attempts": 0, - "updated_at": "2026-08-07T00:10:00Z", - "milestone_complete": true, - "previous_milestone": "v0.10", - "research_docs_ingested": 5, - "locked_decisions": {"Q1": "A", "Q2": "C", "Q3": "A", "Q4": "A", "Q5": "A"}, + "updated_at": "2026-08-07T09:00:00Z", + "milestone_complete": false, + "previous_milestone": "v0.11", + "research_docs_ingested": 1, + "locked_decisions": {"D-238": "v0.12 minor", "D-239": "bundled Dex + BYO", "D-240": "WebAuthn", "D-241": "seal-to-OIDC + Shamir", "D-242": "auth-code+PKCE", "D-243": "Traefik RP ID", "D-244": "SQLite 0600 public keys", "D-245": "device-code fallback", "D-246": "credentials.json 0600", "D-247": "accept-identity-migration gate"}, "grill_verdict": "PROCEED-WITH-CONDITIONS", - "binding_conditions": ["C-23", "C-24", "C-25", "C-26", "C-27", "C-28"], - "phase_count": 24, - "ship": { - "tag": "v0.10.22", - "merged_to_milestone": true, - "milestone_release": "v0.11.0" - }, - "requirements": { - "covered": [99,100,101,102,103,104,105,106,107,108,109,110,111,112,113,114,115,116,117,118,61,65,66,75,79,80,84,86,87], - "partial": [] - }, - "gates": { - "cleared": ["C-23", "C-24", "C-25", "C-27", "C-28", "C-08", "C-09", "C-11", "C-12", "C-14", "C-19"], - "deferred_v1x": [] - }, - "next_milestone": "v1.x" + "binding_conditions": ["C-29", "C-30", "C-31", "C-32", "C-33", "C-34", "C-35", "C-36", "C-37", "C-38"], + "phase_count": 29, + "load_bearing_rule": "R-021", + "threat_model_findings": 25, + "new_requirements": "REQ-119..REQ-148" } diff --git a/.ciagent/CLARIFY_v0.12.md b/.ciagent/CLARIFY_v0.12.md new file mode 100644 index 0000000..8f13155 --- /dev/null +++ b/.ciagent/CLARIFY_v0.12.md @@ -0,0 +1,162 @@ +# CLARIFY v0.12: Security Hardening (Zero-Trust Identity) + +**Status**: resolved (full autonomy, 2026-08-07). All 10 clarifications +resolved with the operator's locked decisions (D-238..D-247). No open +questions remain for Phase 0. The `--ideate` flag was passed; the +threat-model review drove the requirements. + +## Resolved clarifications + +### C1 — Milestone version (resolved) + +**Question**: v0.11 is complete; the v0.11 PRD deferred the v1.0.0 tag +for UAT sign-off. Is this security-hardening milestone v1.0 (the UAT +gate) or a minor v0.12? + +**Decision**: **v0.12 (minor, not v1.0).** The v1.0.0 production-ready +tag stays deferred for post-v0.12 UAT, exactly as v0.11's PRD +specified. v0.12 is a minor feature milestone. Per-phase tags run on +the previous minor's patch line (v0.11.x): P0 -> `v0.11.0`, P01 -> +`v0.11.1`, ..., final phase patch = `v0.11.29` = the v0.12 milestone +release (no separate `v0.12.0` tag, per feature-milestone rule). + +**Affected**: config.json milestone field, all tag computation. + +### C2 — OIDC provider model (resolved) + +**Question**: Bring-your-own IdP, bundled opinionated provider, or both? + +**Decision**: **Bundled Dex by default, with BYO external IdP as a +config override.** `orca auth init-idp` bootstraps a local Dex on the +lead (systemd unit + config template + Traefik route). `oidc.issuer` +in config can be repointed to an external IdP (Keycloak/Authentik/ +Google/etc.) anytime. Orca stays minimal (no bundled opinionated +provider beyond Dex); Dex is the OIDC frontend, not a full IdP. + +**Affected REQs**: REQ-144 (OIDC client + bundled Dex). + +### C3 — Bundled Dex upstream authenticator (resolved) + +**Question**: Dex needs an upstream identity source. "No passwords +anywhere" rules out a local password store. What is the password-free +upstream? + +**Decision**: **WebAuthn (passkeys) connector.** Bundled Dex gets a +custom `orca-webauthn-connector` (~300 LoC Go, `go-webauthn` library) +that serves registration + login HTML/JS pages behind Traefik at +`https:///orca/webauthn/{register,login}`. The WebAuthn +ceremony (biometric/security key) produces a public-key credential; +Dex maps the credential ID to an OIDC `sub`. **Passkeys are public-key +credentials -- the private key never leaves the authenticator -- so the +"no passwords/secrets" invariant (R-021) holds.** + +For BYO external IdP deployments, the operator's existing authenticator +(WebAuthn, TOTP, LDAP, etc.) is used; Orca never sees the upstream +credentials. + +**Affected REQs**: REQ-148 (WebAuthn connector). +**Affected phase**: P05 (new phase; wave B grows from 4 to 5 phases). + +### C4 — Master key sealing (resolved) + +**Question**: How is the secrets master key protected at rest, given +"no passwords anywhere"? + +**Decision**: **Seal to OIDC + Shamir 3-of-5 recovery.** The master +key (32 random bytes) is encrypted (sealed) with a key derived from an +OIDC token exchange at unseal time. `orca cluster unseal` (operator +authenticates via OIDC -> token exchange -> unwrap master key into +memory -> zeroed on shutdown). `orca cluster seal` for manual re-seal. +The sealed blob is stored at `ClusterDir()/master.key.sealed` (0600). +The raw master key never touches disk. + +**Shamir recovery**: at seal time, 5 shards are printed and the +operator stores them offline. If the IdP is permanently lost AND a +quorum of 3 shards is unavailable, the cluster is unrecoverable by +design (documented residual risk; no backdoor). + +For the mTLS-only offline path (no OIDC), the seal key is derived from +the cluster's own CA -- the operator holds the CA (a cert, not a +password). The Shamir recovery path applies to the OIDC-sealed mode. + +**Affected REQs**: REQ-147 (master key seal). +**Affected phase**: P08. + +### C5 — CLI browser flow (resolved) + +**Question**: How does the CLI do the OIDC browser flow? + +**Decision**: **OIDC authorization-code + PKCE + local loopback +redirect.** `orca auth login` opens the default browser to the Dex +WebAuthn endpoint. After the ceremony, Dex redirects to +`127.0.0.1:/callback` (local loopback, ephemeral port). The CLI +exchanges the auth code for a short-lived ID token (1h) + refresh +token. Headless/CI fallback: device-code flow (no browser needed). + +**Affected REQs**: REQ-144, REQ-148. + +### C6 — WebAuthn RP ID / secure context (resolved) + +**Question**: WebAuthn requires a secure context (HTTPS). Where is the +RP ID rooted? + +**Decision**: **Traefik-served cluster domain (step-ca cert, R-017).** +Traefik already provides HTTPS on `127.0.0.1:8443` (nft DNAT from +`:443`). The RP ID is the cluster's Traefik-served domain, configurable +via `orca auth init-idp --rp-id `. For localhost dev, the +operator uses the bootstrapped step-ca cert (self-signed, but WebAuthn +accepts it for non-registerable credentials in dev mode). + +**Affected REQs**: REQ-148. + +### C7 — Passkey storage (resolved) + +**Question**: Where are WebAuthn credentials stored? + +**Decision**: **SQLite at `ClusterDir()/webauthn-credentials.db` +(0600). Public keys only.** The DB stores credential IDs, public keys, +sign counts, and AAGUIDs. No private keys, no secrets, no passphrase +wrapping. 0600 file mode for integrity (tamper detection), not secrecy. + +**Affected REQs**: REQ-148. + +### C8 — Breaking-change handling (resolved) + +**Question**: P07 (remove all password/token paths) is a breaking +change. How are existing v0.11 clusters handled? + +**Decision**: **`orca upgrade` refuses v0.11 clusters using +`--password`/bare-tokens without `--accept-identity-migration`.** The +flag prints the cutover documentation and requires explicit +confirmation. No silent breakage. Documented in `docs/oidc.md` and the +migration guide. + +**Affected REQs**: REQ-146, REQ-137. + +### C9 — Token storage at rest (resolved) + +**Question**: Where are OIDC tokens stored locally? + +**Decision**: **`~/.orca/credentials.json` (0600). Short-lived (1h) + +refresh.** Standard OIDC token storage. 0600 file mode. Refresh +handles rotation; no long-lived Orca-issued tokens (the IdP issues +them; Orca only stores them). + +**Affected REQs**: REQ-144. + +### C10 — Phase count (resolved) + +**Question**: The threat model surfaced ~25 fix areas + the +zero-trust identity work + docs + tests + final. More than 20 phases +is acceptable per operator guidance. How many? + +**Decision**: **29 phases** (P0 + P01..P27 + P28 final). The operator +explicitly accepted "more than 20 phases is acceptable if warranted." +The GRILL stage may split/merge as needed (as v0.11 grill split P10 +into P10a/P10b). + +**Affected**: PLAN_v0.12.md, ROADMAP.md. + +## Open questions + +None. All 10 clarifications resolved. Phase 0 proceeds to RESEARCH. diff --git a/.ciagent/GRILL_v0.12.md b/.ciagent/GRILL_v0.12.md new file mode 100644 index 0000000..6864b16 --- /dev/null +++ b/.ciagent/GRILL_v0.12.md @@ -0,0 +1,170 @@ +# Grill: v0.12 Security Hardening (Zero-Trust Identity) — Phase 0 Adversarial Review + +**Status**: PROCEED-WITH-CONDITIONS. The v0.12 plan is sound; 10 +binding conditions (C-29..C-38) gate specific phases. The plan adopts +R-021 (no Orca credentials) and D-238..D-247 from the threat-model +review + operator decisions. The grill reviewed the plan +adversarially across the same 9 axes as GRILL_v0.9/v0.11 (vision, +feasibility, scope, risk, security, operational, cost, competitive, +exit). + +## Forcing questions + verdicts + +### FQ1 — R-021 is the largest behavioral change in project history + +**Question**: R-021 ("no Orca credentials") removes all password and +token surfaces. P07 is explicitly breaking. Is the migration path +(C-34 `--accept-identity-migration`) sufficient, or does the breakage +extend beyond what's documented? + +**Verdict**: BREAKAGE IS CONTAINED BUT UNDERESTIMATED. The plan +documents the Proxmox `--password` and step-ca `--password-file` +removal. But `KindToken` removal (P06) also breaks any existing +`acl.json` that uses token identities. The migration must rewrite +`acl.json` entries, not just refuse them. + +**Binding condition C-29 (refined)**: P22 (`orca upgrade`) MUST +detect v0.11 `acl.json` entries with `KindToken` and either (a) +refuse without `--accept-identity-migration` + a documented +re-mapping, or (b) auto-stub them as `KindOidc` with a placeholder +`sub` requiring operator confirmation. No silent data loss. + +**Confidence**: 0.90 + +### FQ2 — P08 master key seal is the riskiest phase + +**Question**: A bug in seal/unseal corrupts all secrets at rest. Is +the recovery path (Shamir 3-of-5) actually testable, and does it +handle the "IdP lost AND shards partially lost" case? + +**Verdict**: RECOVERY IS TESTABLE BUT THE EDGE CASES ARE UNDERTESTED. +The plan covers the happy path (3-of-5) and the failure case (< 3 +shards -> unrecoverable). But the "IdP lost, 3 shards available, but +the OIDC-derived salt was also lost" case (the salt is in the sealed +blob, so this shouldn't happen -- but verify) needs an explicit test. + +**Binding condition C-30 (refined)**: P08 MUST include a test that +recovers with 3-of-5 shards AFTER the IdP is simulated-down (seal key +reconstruction from shards, NOT from OIDC token). The sealed blob +must contain the salt (so recovery doesn't need the IdP). Document +that the salt is stored in the sealed blob, not derived from the +token at recovery time. + +**Confidence**: 0.88 + +### FQ3 — P05 WebAuthn connector feasibility + +**Question**: The custom Dex connector (~300 LoC) is new ground. Is +the `go-webauthn` library mature enough, and does the RP ID / secure +context requirement create a chicken-and-egg problem (Dex needs +Traefik, Traefik needs the cert, the cert needs step-ca, step-ca +needs the operator authenticated -- by Dex)? + +**Verdict**: NO CHICKEN-AND-EGG, but the bootstrap sequence must be +explicit. The cert comes from step-ca's OIDC provisioner (P07), but +the FIRST operator must authenticate to step-ca. Resolution: the +first operator uses the mTLS-only path (cluster CA cert, held +offline) to mint the first Traefik cert. Dex then comes up. The +first WebAuthn registration happens via that first cert. The chicken- +and-egg is resolved by the mTLS-only bootstrap path. + +**Binding condition C-31 (new)**: P04/P05 MUST document the bootstrap +sequence: (1) `orca init` bootstraps the cluster CA (step-ca, mTLS- +only), (2) `orca auth init-idp` deploys Dex behind Traefik using the +step-ca cert, (3) the first operator registers a passkey via the +mTLS-authenticated session, (4) subsequent operators use WebAuthn. +The mTLS-only path is the bootstrap escape hatch. + +**Confidence**: 0.87 + +### FQ4 — P21 SQLite encryption CGO risk + +**Question**: SQLCipher needs CGO (breaks D-008 cross-compile). The +C-31 fallback is "file-mode 0600 + documented threat." Is that +acceptable for a security-hardening milestone? + +**Verdict**: FALLBACK IS ACCEPTABLE BUT MUST BE EXPLICIT. The +threat-model finding (F8) is "DBs unencrypted with no explicit file +mode." The minimum fix (0600 file mode) closes the "no explicit mode" +half. The "unencrypted" half is a documented residual risk if CGO is +infeasible. This is consistent with the project's "no CGO" invariant +(D-008) which is load-bearing for cross-compile. + +**Binding condition C-32 (refined)**: P21 MUST evaluate at least one +CGO-free encryption option (e.g., application-level AES-GCM envelope +around the SQLite file, or a FUSE encryption layer). If all are +infeasible or too complex for v0.12, document the decision + residual +risk. The fallback is file-mode 0600 only. No CGO. + +**Confidence**: 0.85 + +### FQ5 — Phase count (29) vs. sizing + +**Question**: 29 phases is the largest milestone in project history +(v0.11 was 24, v0.9 was 14). Is any single phase too large to ship +atomically? + +**Verdict**: TWO PHASES ARE LARGE. P04 (OIDC+Dex) and P08 (master +key seal) are each ~500-700 LoC + tests. They're within the v0.11 +P10a/P10b sizing that the grill previously accepted, but the grill +split P10. If P04 or P08 grows during execution, the EXECUTE workflow +may split them (P04a/P04b, P08a/P08b). + +**Binding condition C-33 (new)**: P04 and P08 are SPLIT CANDIDATES. +If either exceeds ~700 LoC + tests during EXECUTE, split: P04a (OIDC +client) / P04b (bundled Dex deploy); P08a (seal/unseal + Shamir) / +P08b (CLI + mTLS-only path). The planner monitors LoC during +execution. + +**Confidence**: 0.82 + +### FQ6 — C-32 human gate (leaked GITEA_TOKEN) could stall the final ship + +**Question**: If the operator doesn't rotate the token, P28 can't +ship. Is there an escalation path that doesn't block the milestone? + +**Verdict**: ESCALATION PATH EXISTS. Ship as `v0.11.28-rc1` (release +candidate) if the token is not rotated by P28. The `v0.11.28` final +tag (milestone release) waits for confirmation. The milestone is +"complete" (all phases shipped); only the final tag is gated. + +**Binding condition C-34 (refined)**: C-32 human-gate: if the +GITEA_TOKEN is not rotated by P28, ship `v0.11.28-rc1` (all phases +complete, release notes flag the pending rotation). The `v0.11.28` +final tag is cut when the operator confirms. The `---ci---` block +records `escalation: type=release_pending resolution=auto` -- does +not halt the pipeline. + +**Confidence**: 0.90 + +## Adopted binding conditions (C-29..C-38) + +| ID | Condition | Phase | Confidence | +|----|-----------|-------|------------| +| C-29 | P23 (dual-write closure) gated on P06/P08/P09/P11 all shipped. P22 must detect v0.11 `acl.json` `KindToken` entries and refuse/remap without `--accept-identity-migration`. | P22/P23 | 0.90 | +| C-30 | P14 (master key rotation) reversible; `--dry-run` mandatory; auto-rollback to old sealed key on any ns failure. | P14 | 0.88 | +| C-31 | P21 (SQLite encryption): evaluate at least one CGO-free option (app-level AES-GCM envelope, FUSE layer). If infeasible, file-mode 0600 + documented residual risk. No CGO. | P21 | 0.85 | +| C-32 | **Human-gate**: leaked GITEA_TOKEN (F17) rotated + `.env` re-seeded before `v0.11.28` final tag. If not rotated by P28, ship `v0.11.28-rc1`. History-scrub best-effort, non-blocking. Escalation hook in `---ci---`. | P28 | 0.90 | +| C-33 | P26 (security integration tests) in `.coreci.yml` `validate`, gates merges -- not opt-in. | P26 | 0.95 | +| C-34 | P07 (password/token removal) breaking. `orca upgrade` (P22) refuses v0.11 clusters using `--password`/bare-tokens/`KindToken` without `--accept-identity-migration`. No silent breakage. | P07/P22 | 0.90 | +| C-35 | P08 (Shamir recovery): 3-of-5 shards printed at seal time, operator stores offline. Sealed blob contains the salt (recovery doesn't need the IdP). If IdP lost AND < 3 shards -> unrecoverable by design (documented residual risk). No backdoor. Test recovery with IdP-down. | P08 | 0.88 | +| C-36 | OIDC client secret (confidential clients) at `ClusterDir()/oidc-client-secret` (0600), rotatable via `orca auth rotate-client-secret`, never committed. Public PKCE clients avoid even this. | P04 | 0.92 | +| C-37 | P04/P05 (bundled Dex + WebAuthn): document the bootstrap sequence (mTLS-only first cert -> Dex -> first passkey). The mTLS-only path is the bootstrap escape hatch. If WebAuthn proves infeasible, bundled Dex ships mTLS-client-cert-only (C-37 fallback). The "no Orca credentials" invariant holds regardless. | P04/P05 | 0.87 | +| C-38 | P05 (WebAuthn): RP ID must match the cluster's Traefik-served domain; `orca auth init-idp` configures it. HTTPS secure context via Traefik (step-ca cert). P26 integration tests use the WebAuthn virtual-authenticator API -- no hardware key required in CI. | P05 | 0.90 | + +## Verdict: PROCEED-WITH-CONDITIONS + +The v0.12 plan is sound. The 10 binding conditions gate the risky +phases. The 29-phase count is within the operator's "more than 20 if +warranted" guidance. The plan adopts R-021 (no Orca credentials) and +D-238..D-247. The grill does NOT recommend REPLAN. + +## Phase challenges (PC-01..PC-05) + +| ID | Challenge | Phase | +|----|-----------|-------| +| PC-01 | P04/P08 are split candidates if LoC exceeds ~700 (C-33) | P04/P08 | +| PC-02 | P07 breaking change -- migration must handle `KindToken` acl.json entries, not just passwords (C-29/C-34) | P07/P22 | +| PC-03 | P05 WebAuthn bootstrap sequence must be explicit (mTLS-only first cert) | P04/P05 | +| PC-04 | P21 SQLite encryption CGO evaluation -- document the decision + residual risk if fallback | P21 | +| PC-05 | C-32 human gate -- `v0.11.28-rc1` escalation if token not rotated | P28 | diff --git a/.ciagent/IDEATION_v0.12.md b/.ciagent/IDEATION_v0.12.md new file mode 100644 index 0000000..4f57b56 --- /dev/null +++ b/.ciagent/IDEATION_v0.12.md @@ -0,0 +1,183 @@ +# Ideation v0.12: Security Hardening (Zero-Trust Identity) + +**Status**: 30 ideas accepted (0 skipped, 0 modified). All from Tier 1 +(mechanical analysis of the threat-model review) and Tier 2 +(backend-enriched prioritization). The `--ideate` flag was passed; +ideation ran between RESEARCH and PLAN per run.md Step 3. + +## Tier 1 — Mechanical analysis + +### 2.1 Git-native pattern mining + +The v0.11 milestone shipped 24 phases with a threat model in P15.5 +(gate C-19). The threat model identified residual risks but did not +close them -- it documented them for v1.x. The v0.12 ideation ingests +that threat model as the primary signal source. + +**Repeated lessons** (from v0.8..v0.11 `---ci---` blocks): +- "Deprecated but still load-bearing" appears 6 times across v0.8..v0.11 + (legacy CA, mTLS transport, daemon, certpaths, step-ca password + provisioner, `hmacSHA256` dead code). The dual-write window is the + single largest attack-surface expander. -> **F16 / REQ-138**. +- "TOFU by default, pre-pin optional" appears 4 times (v0.6 SSH join, + v0.8 host-key-fingerprint, v0.11 drift scripts). TOFU is a + first-connect MITM risk. -> **F15 / REQ-139** (known_hosts tightening). +- "File modes checked at write, not at read" appears 3 times (v0.2 + cert modes, v0.5 namespace dirs, v0.11 master key). -> **F13 / + REQ-130**. + +**Low-confidence decisions** (confidence < 0.85 in `---ci---` blocks): +- D-007 (mTLS for v0.1, tokens deferred) -- 0.80. v0.12 closes the + token gap via OIDC (no Orca-issued tokens; the IdP issues them). +- D-028 (repo visibility flip for public releases) -- 0.85. v0.12 + adds install.sh checksum verification (F14) as defense-in-depth. + +**Escalation types**: +- `release_pending` (v0.8..v0.11 ship fallbacks) -- not security-relevant. +- `human_validation` (v0.11 C-19 threat model) -- v0.12 is the + comprehensive closure of those documented risks. + +**Compound solutions** (generalized patterns): +- The "shellQuote + regression test" pattern from v0.8 SSH trust + hardening (REQ-058) generalizes to all SSH-exec interpolation sites + (podman, wasm, aggregate.sh). -> **F3 / REQ-119**. +- The "atomic temp + chmod + fsync + rename" pattern from + `WriteAtomic` (ca.go) generalizes to migration `copyFile` and + backup restore. -> **F19 / REQ-137**. + +**Partial requirements**: none (v0.11 shipped all REQs complete). + +### 2.2 Coverage gap analysis + +All v0.11 REQs are Complete. The v0.12 requirements are net-new from +the threat model -- no pending/in_progress REQs to close. + +### 2.3 Verification layer inversion + +- **Structural**: `internal/security/ca.go` (legacy CA) documented as + deprecated but still compiled and load-bearing. -> F16. +- **Behavioral**: `internal/runtime/podman.go`, `wasm.go` have no + command-injection regression tests. -> F3. +- **Security**: No STRIDE analysis for the OIDC/WebAuthn data flow + (new in v0.12). -> addressed by REQ-142 (docs). +- **Quality**: `classifyDialErr` substring matching is a known code + smell flagged in v0.9 research. -> F25. + +### 2.4 Architectural drift detection + +- `internal/acl/` exists but is not wired into any enforcement point + (documented as "future" since v0.9). -> F1. +- `internal/identity/spiffe.go` `VerifySVID` skips chain validation + (documented as "trust is implicit via SSH channel" in v0.11 P01.5 + spike result). -> F9. +- `internal/emitter/nft.go` ships SYN-flood + rate-limit but no + conntrack/default-deny (the v0.11 emitter met the REQ but not + defense-in-depth best practice). -> F21. + +### 2.5 Spec-driven improvement + +- R-021 ("no Orca credentials") is the new spec invariant. Every + existing password/token surface is a spec violation under R-021. + -> F1, F12, F17, REQ-144..148. +- The v0.11 PRD's deferred-v1.x list included "master.key + passphrase-less 0600 (consider OS keyring in v1.x)." v0.12 closes + this via seal-to-OIDC (no passphrase, no OS keyring dependency -- + OIDC is the unwrap mechanism). + +## Tier 2 — Backend-enriched analysis + +### 2.6 Prioritization + +Ranked by (1) severity, (2) OS-surface exposure (per user instruction +"includes the operating system itself"), (3) ease of addressing: + +1. **F3 command injection** (Critical, OS-touching, shellQuote is a + well-understood fix) -> P01. +2. **F4 path traversal** (Critical, OS-touching, validateNamespaceName + is trivial) -> P02. +3. **F5 txn arbitrary paths** (Critical, OS-touching, prefix allowlist) + -> P03. +4. **F1 ACL unenforced** (Critical, foundational for OIDC authz) -> + P06 (after P04/P05 identity). +5. **F6 daemon no auth** (High, OS-touching) -> P09. +6. **F2 audit not tamper-evident** (Critical, integrity) -> P10. +7. **F9 SVID no chain** (High, identity) -> P11. +8. **F7 backup symlink** (High, OS-touching) -> P12. +9. **F10 step-ca /tmp** (High, OS-touching) -> P13. +10. **F12 master key rotation** (High, crypto) -> P14. +11. **F11 aggregate.sh JSON injection** (High, OS-touching) -> P16. +12. **F14 install.sh no checksum** (High, OS-touching) -> P17. +13. **F21 nftables** (Medium, OS-touching) -> P18. +14. **F22 sudoers** (Medium, OS-touching) -> P19. +15. **F23 system user** (Medium, OS-touching) -> P20. +16. **F8 SQLite** (High, OS-touching) -> P21. +17. **F19 migration** (Medium, OS-touching) -> P22. +18. **F16 dual-write closure** (Medium, surface reduction) -> P23. +19. **F15/F25 transport** (Medium/Low) -> P24. +20. **F18 drift auth** (Medium) -> P25. +21. **Integration tests** (gate) -> P26. +22. **Docs** (gate) -> P27. +23. **Final review** (gate) -> P28. + +The zero-trust identity work (P04 OIDC+Dex, P05 WebAuthn, P07 password +removal, P08 master key seal) is wave B because it's the architectural +foundation -- P06 (ACL) and P09 (daemon auth) depend on it. + +### 2.7 Novel improvement suggestions + +- **WebAuthn as the bundled password-free authenticator** (operator + decision D-240). This is beyond pattern matching -- it's the + strongest available authentication primitive and directly satisfies + R-021. The `go-webauthn` library is mature; the custom Dex connector + is ~300 LoC. +- **Shamir 3-of-5 master key recovery** (operator decision D-241). + Standard threshold cryptography; no backdoor; documented residual + risk. +- **Bundled Dex** (operator decision D-239). Zero-trust out of the + box without external setup; BYO override preserves flexibility. + +### 2.8 Chaos engineering ideation + +- **What if the OIDC provider is unavailable?** -> `orca cluster + unseal` fails; cluster runs on in-memory master key until shutdown + (no new secrets operations). Shamir recovery if permanent. Doc'd. +- **What if a peer's drift event is forged?** -> REQ-140 (per-peer + HMAC). +- **What if the master key is compromised?** -> REQ-129 (rotation, + re-seal to OIDC). All historical secrets still compromised (no + forward secrecy) -- documented residual risk. +- **What if install.sh is MITM'd?** -> REQ-132 (checksum+GPG). +- **What if the Gitea token leaks again?** -> C-32 (human-gate + rotation before final ship); history scrub best-effort. + +## Tier 3 — Cross-project pattern transfer + +Single-project mode (only `orca` in `active_projects`). No +cross-project mining. + +## Step 3 — Merge and deduplicate + +30 ideas, all unique by `relatedReq` (REQ-119..REQ-148). No +duplicates. Sorted by severity then wave order (see Tier 2.6). + +## Step 4 — Interactive validation + +Under `autonomy.level=full` + `workflow.no_hitl=true`, all 30 ideas +are auto-accepted. The operator pre-approved the scope in the planning +conversation (comprehensive coverage including OS surface, bundled +Dex, WebAuthn, Shamir). 0 skipped, 0 modified. + +## Step 5 — Long-term document updates + +- `REQUIREMENTS.md`: REQ-119..REQ-148 added (done). +- `ROADMAP.md`: v0.12 milestone section added (next). +- `ARCHITECTURE.md`: zero-trust identity model + OIDC data-flow to be + added in P27 (docs phase) -- not in Phase 0 to avoid scope creep. +- `PROJECT.md`: v0.12 scope summary + D-238..D-247 added (done). + +## Step 6 — Ask-after-validation kickoff + +The run workflow continues to PLAN -> GRILL -> ship Phase 0 -> +execute P01..P27 -> final P28. No separate kickoff needed (the +`--ideate` flag is consumed; ideas are already in REQUIREMENTS.md + +ROADMAP.md). diff --git a/.ciagent/PLAN_v0.12.md b/.ciagent/PLAN_v0.12.md new file mode 100644 index 0000000..cbb5b58 --- /dev/null +++ b/.ciagent/PLAN_v0.12.md @@ -0,0 +1,395 @@ +# Plan v0.12: Security Hardening (Zero-Trust Identity) + +**Status**: Phase 0 plan. 29 phases (P0 + P01..P27 + P28 final). Wave +ordering, persona assignments, and binding conditions. GRILL will +pressure-test and may split/merge. + +## Milestone identity + +- **Label**: `v0.12-security-hardening` +- **Type**: feature (P04, P05 ship `feat`) +- **Tag line**: v0.11.x patches (`v0.11.0`..`v0.11.28`) +- **Final phase patch** = milestone release = `v0.11.28` (no separate `v0.12.0`) +- **Branch**: `milestone/v0.12-security-hardening` +- **v1.0.0**: deferred for post-v0.12 UAT (per v0.11 PRD) + +## Wave ordering + +Waves are dependency-ordered. Within a wave, phases run in sequence +(parallelization disabled per config.json `parallelization.enabled=false`). + +### Phase 0 — Pre-execution (all personas, lead-developer coordinates) + +Stages: SPECIFY -> CLARIFY -> RESEARCH -> IDEATE -> PLAN -> GRILL -> SHIP. + +- SPECIFY: v0.12 in config.json + PROJECT.md (done). +- CLARIFY: D-238..D-247 (done, CLARIFY_v0.12.md). +- RESEARCH: threat model F1..F25 + zero-trust identity model (done, + RESEARCH_v0.12.md). Resolves RQ-1 (WebAuthn as password-free upstream). +- IDEATE: 30 ideas accepted -> REQ-119..REQ-148 (done, IDEATION_v0.12.md). +- PLAN: this document. +- GRILL: ratify C-29..C-38, split/merge as needed. +- SHIP: tag `v0.11.0`. + +**Commit**: `docs(P00): v0.12 security-hardening phase 0 (specify/clarify/research/ideate/plan/grill)` + +### Wave A — Critical injection & traversal (backend-engineer) + +Vertical slice: stop the bleeding first. Three independent fixes, no +inter-dependencies. + +#### P01 — Command injection (podman/wasm) — REQ-119, F3 + +- **Persona**: backend-engineer (territory: `internal/runtime/`) +- **Tasks**: + 1. Add `shellQuote` helper (or use `golang.org/x/crypto/ssh`-safe quoting) to `internal/runtime/`. + 2. Fix `podman.go:57`: `fmt.Sprintf("podman run -d --name %s %q %s", name, image, shellQuote(cmdStr))`. + 3. Fix `wasm.go:39`: same pattern for `wasmtime run`. + 4. Add Go regression tests: `;`, `|`, `$()`, backticks, newline, `$IFS`, `<>()` injection attempts. + 5. Add bats test: a jobspec with a malicious command runs the literal command, not the injected shell. +- **Must-haves**: all injection tests pass; existing podman/wasm tests still pass. +- **Commit**: `fix(P01): command injection in podman/wasm runtimes (REQ-119, F3)` +- **Tag**: `v0.11.1` + +#### P02 — Namespace path traversal — REQ-120, F4 + +- **Persona**: backend-engineer (territory: `internal/ns/`, `internal/cli/ns.go`) +- **Tasks**: + 1. Add `validateNamespaceName(name)` to `internal/ns/`: reject `..`, `/`, leading `-`, null bytes, control chars, empty, length > 128. + 2. Wire into `ns create`, `ns inherit`, `ns set-constraint`, and any path-accepting ns command. + 3. Add Go fuzz test (`FuzzValidateNamespaceName`). + 4. Add regression test: `orca ns create "../../etc"` fails with a clear error. +- **Must-haves**: fuzz test passes 10k iterations; `..`/`/`/null rejected. +- **Commit**: `fix(P02): namespace path traversal (REQ-120, F4)` +- **Tag**: `v0.11.2` + +#### P03 — Txn apply path allowlist — REQ-121, F5 + +- **Persona**: backend-engineer (territory: `internal/txn/`) +- **Tasks**: + 1. In `txn.go:renderApplyScript`, add path validation to the python heredoc: every `path` in `desired-state.json` must match a prefix in the allowlist (`/etc/orca/`, `/etc/traefik/orca*`, `/etc/systemd/system/orca-*`, `/etc/nftables.d/orca*`, `/etc/syncthing/orca*`). + 2. Reject with a clear error + exit code on mismatch. + 3. Add Go test: a desired-state with `"path": "/etc/shadow"` is rejected. + 4. Add bats test: `orca-pull.sh` with a crafted manifest refuses. +- **Must-haves**: arbitrary-path writes rejected; legitimate paths still apply. +- **Commit**: `fix(P03): txn apply path allowlist (REQ-121, F5)` +- **Tag**: `v0.11.3` + +### Wave B — Zero-trust identity (backend-engineer + lead-developer) + +The architectural foundation. P04/P05 are `feat` phases; P06/P07/P08 +are `fix`/`refactor` that depend on them. + +#### P04 — OIDC client + bundled Dex — REQ-144 + +- **Persona**: backend-engineer (territory: `internal/cli/`, `internal/identity/`) +- **Tasks**: + 1. Add `github.com/coreos/go-oidc/v3` dependency. + 2. `internal/identity/oidc.go`: OIDC client (provider discovery, JWKS cache + refresh, ID token verification, token storage at `~/.orca/credentials.json` 0600). + 3. `orca auth login`/`logout`/`status` CLI: browser auth-code + PKCE + local loopback redirect (`127.0.0.1:/callback`); headless device-code fallback. + 4. `orca auth init-idp`: bootstrap bundled Dex (systemd unit + config template + Traefik route) on the lead; `--rp-id ` config. + 5. OIDC config block in `internal/config/`: `oidc.issuer`, `client_id`, `client_secret`, `scopes`. + 6. BYO external IdP override: `oidc.issuer` repoint bypasses bundled Dex. + 7. Go tests: mock OIDC provider, JWKS rotation, token refresh, login/logout flow. +- **Must-haves**: `orca auth login` produces a valid ID token; `orca auth status` shows it; `--oidc` flag gated; offline Dex quickstart doc'd. +- **Commit**: `feat(P04): OIDC client + bundled Dex (REQ-144, D-239, D-242)` +- **Tag**: `v0.11.4` + +#### P05 — WebAuthn connector for Dex — REQ-148 + +- **Persona**: backend-engineer (territory: `internal/identity/`, new `internal/webauthn/`) +- **Tasks**: + 1. Add `github.com/go-webauthn/webauthn` dependency. + 2. `internal/webauthn/connector.go`: Dex connector (~300 LoC) -- registration + login ceremonies at `/orca/webauthn/{register,login}`. + 3. `internal/webauthn/store.go`: passkey storage SQLite at `ClusterDir()/webauthn-credentials.db` (0600); schema: `credentials(user_id, credential_id, public_key, sign_count, aaguid, created_at)`. + 4. `orca auth register` CLI: browser flow to register a new passkey. + 5. RP ID = cluster Traefik domain (from `orca auth init-idp --rp-id`); secure context via step-ca cert (R-017). + 6. Go tests using `go-webauthn` virtual-authenticator test helpers (no hardware key). +- **Must-haves**: register + login flow works end-to-end against the bundled Dex; public keys only stored; virtual-authenticator tests pass. +- **Commit**: `feat(P05): WebAuthn connector for Dex (REQ-148, D-240, C-38)` +- **Tag**: `v0.11.5` + +#### P06 — ACL rewrite to OIDC claims + enforcement — REQ-145, REQ-122, F1 + +- **Persona**: backend-engineer (territory: `internal/acl/`, `internal/daemon/`, `internal/sshpush/`) +- **Tasks**: + 1. Remove `KindToken` from `internal/acl/acl.go` entirely. + 2. Add `KindOidc`: maps `sub` + `groups` -> namespace permissions. + 3. `acl.Check` takes an OIDC claims struct (or SPIFFE SVID for machine identity). + 4. Wire `acl.Check` into daemon handlers (read/write/admin by route). + 5. Wire `acl.Check` into SSH-push applier: validate `ORCA_OIDC_TOKEN` env var against JWKS before applying any txn. + 6. `acl.json` file mode tightened to 0600. + 7. Deny-by-default enforced; actor recorded in audit log. + 8. Go tests: ACL-negative (unauthorized sub denied), ACL-positive, machine identity (SVID) still works. +- **Must-haves**: no request applies without a valid OIDC token or SVID; `KindToken` removed; deny-by-default enforced. +- **Commit**: `fix(P06): ACL rewrite to OIDC claims + enforcement (REQ-145, REQ-122, F1)` +- **Tag**: `v0.11.6` + +#### P07 — Remove all password/token paths — REQ-146, R-021, C-34 + +- **Persona**: lead-developer (territory: `internal/proxmox/`, `internal/stepca/`, `internal/cli/`) +- **Tasks**: + 1. Remove `--password`/`$ORCA_PROXMOX_PASSWORD` from Proxmox join (`proxmox/bootstrap.go:29`); replace with pre-staged-key-only or `step ssh` OIDC cert exchange. + 2. Remove step-ca `--password-file` provisioner; migrate to OIDC provisioner (step-ca natively supports OIDC). + 3. Remove any bare-token CLI paths (already removed in P06, but sweep for stragglers). + 4. Add deprecation/migration docs: `--accept-identity-migration` flag on `orca upgrade` (P22 enforces). + 5. Go tests: `--password` flag is rejected with a clear error pointing to the migration guide. +- **Must-haves**: no password accepted anywhere; `--password` rejected; step-ca OIDC provisioner works. +- **Commit**: `fix(P07): remove all password/token paths (REQ-146, R-021, C-34) -- BREAKING` +- **Tag**: `v0.11.7` + +#### P08 — Master key seal-to-OIDC + Shamir — REQ-147, D-241, C-35 + +- **Persona**: backend-engineer (territory: `internal/secrets/`, new `internal/seal/`) +- **Tasks**: + 1. `internal/seal/seal.go`: seal/unseal using HKDF-SHA256 of OIDC ID token `sub` + fresh 32-byte salt; sealed blob at `ClusterDir()/master.key.sealed` (0600). + 2. Shamir 3-of-5: `internal/seal/shamir.go` (using `golang.org/x/crypto/...` or a vendored Shamir impl); print 5 shards at seal time. + 3. `orca cluster unseal`/`seal` CLI: unseal via OIDC auth; `--recovery` + 3 shards for IdP-lost case. + 4. mTLS-only offline path: seal key derived from cluster CA. + 5. Master key zeroed on shutdown (use `memguard` or manual `crypto/rand` overwrite). + 6. Go tests: seal -> unseal round-trip; recovery with 3 shards; 2 shards fails; raw key never on disk (assert no `master.key` file, only `master.key.sealed`). +- **Must-haves**: raw master key never touches disk; unseal works via OIDC; recovery works with 3-of-5 shards. +- **Commit**: `feat(P08): master key seal-to-OIDC + Shamir 3-of-5 (REQ-147, D-241, C-35)` +- **Tag**: `v0.11.8` + +### Wave C — Auth & integrity (backend-engineer + data-engineer) + +#### P09 — Daemon auth hardening — REQ-123, REQ-124, F6, F24 + +- **Persona**: backend-engineer (territory: `internal/daemon/`) +- **Tasks**: + 1. Remove plaintext mode entirely (mandatory mTLS). + 2. Accept OIDC bearer as second factor on human-facing endpoints. + 3. `http.MaxBytesReader` on all JSON-decoding handlers; `MaxHeaderBytes` set. + 4. pprof loopback-only by default; `--pprof-allow-public` requires confirmation. + 5. Go tests: plaintext mode rejected; oversized body rejected; pprof non-loopback rejected. +- **Commit**: `fix(P09): daemon auth hardening (REQ-123, REQ-124, F6, F24)` +- **Tag**: `v0.11.9` + +#### P10 — Audit log tamper-evidence — REQ-125, F2 + +- **Persona**: data-engineer (territory: `internal/audit/`, `internal/store/`) +- **Tasks**: + 1. Add `prev_hash` + `entry_hash` columns to `audit_log` table (migration 0008). + 2. `AuditRepo.Append` computes `entry_hash = sha256(prev_hash || payload)`, stores it; HMAC-SHA256 under master key on the chain head (stored separately). + 3. SQLite trigger blocks UPDATE/DELETE on `audit_log`. + 4. `orca doctor audit` verifies the chain (recomputes hashes, checks HMAC). + 5. Actor field carries OIDC `sub` or SPIFFE SVID. + 6. Go tests: tamper detection (modify a row -> doctor fails); append-only enforcement (DELETE fails). +- **Commit**: `fix(P10): audit log tamper-evidence (REQ-125, F2)` +- **Tag**: `v0.11.10` + +#### P11 — SVID chain validation — REQ-126, F9 + +- **Persona**: backend-engineer (territory: `internal/identity/`) +- **Tasks**: + 1. `VerifySVID` loads the CA pool (from `ClusterDir()/ca.crt` or step-ca root) and validates the full cert chain. + 2. Reject certs signed by unknown CAs even with correct URI SAN. + 3. Go tests: cert from wrong CA rejected; cert from correct CA + correct URI accepted; expired cert rejected. +- **Commit**: `fix(P11): SVID chain validation (REQ-126, F9)` +- **Tag**: `v0.11.11` + +#### P12 — Backup symlink validation — REQ-127, F7 + +- **Persona**: data-engineer (territory: `internal/backup/`) +- **Tasks**: + 1. `Restore` rejects `Linkname` that's absolute, contains `..`, or points outside `ORCA_HOME`. + 2. Regression test with crafted tarball containing a symlink to `/etc/shadow`. +- **Commit**: `fix(P12): backup symlink validation (REQ-127, F7)` +- **Tag**: `v0.11.12` + +### Wave D — Crypto & secrets (backend-engineer) + +#### P13 — step-ca /tmp hardening — REQ-128, F10 + +- **Persona**: backend-engineer (territory: `internal/stepca/`, `internal/identity/`) +- **Tasks**: + 1. `step ca certificate` writes to `0600` temp under `ClusterDir()/step-tmp/` (or `TMPDIR` override). + 2. Cleanup in `defer`; `mkdir -p` with 0700 on the temp dir. + 3. Go test: assert temp file mode is 0600; assert cleanup on success + failure. +- **Commit**: `fix(P13): step-ca /tmp hardening (REQ-128, F10)` +- **Tag**: `v0.11.13` + +#### P14 — Master key rotation — REQ-129, F12, C-30 + +- **Persona**: backend-engineer (territory: `internal/secrets/`, `internal/seal/`) +- **Tasks**: + 1. `orca secrets rotate-master`: generate new master key, re-encrypt all namespace secrets, re-seal to OIDC. + 2. `--dry-run` reports affected namespaces without writing. + 3. Atomic per-namespace re-encryption; auto-rollback to old sealed key on any ns failure. + 4. Go tests: rotation succeeds; partial failure rolls back; dry-run doesn't write. +- **Commit**: `fix(P14): master key rotation (REQ-129, F12, C-30)` +- **Tag**: `v0.11.14` + +#### P15 — File-mode audit expansion — REQ-130, F13 + +- **Persona**: backend-engineer (territory: `internal/security/`, `internal/cli/doctor.go`) +- **Tasks**: + 1. `EnforceFileModes` extended to SSH key, master key (sealed blob), server cert/key, known_hosts. + 2. `orca doctor modes` checks all. + 3. Startup refuses to run on violation. + 4. Go tests: looser mode -> doctor fails + startup refuses. +- **Commit**: `fix(P15): file-mode audit expansion (REQ-130, F13)` +- **Tag**: `v0.11.15` + +### Wave E — OS scripts & emitters (backend-engineer + lead-developer) + +#### P16 — aggregate.sh JSON injection + drift-gate fix — REQ-131, F11, F18 + +- **Persona**: lead-developer (territory: `scripts/`) +- **Tasks**: + 1. Replace `printf` interpolation in `orca-aggregate.sh:64` with `jq`-based JSON construction (or a Go-side aggregator emitting JSON). + 2. Fix `orca-pull.sh` R-020 parsing to use `jq` instead of grep. + 3. Bats tests: malicious peer output doesn't corrupt `cluster.json`; drift gate correctly excludes acknowledged drift. +- **Commit**: `fix(P16): aggregate.sh JSON injection + drift-gate fix (REQ-131, F11, F18)` +- **Tag**: `v0.11.16` + +#### P17 — install.sh checksum+GPG verification — REQ-132, F14 + +- **Persona**: lead-developer (territory: `scripts/install.sh`, `scripts/release.sh`) +- **Tasks**: + 1. `release.sh` publishes `SHA256SUMS` + `SHA256SUMS.asc` (GPG-signed) alongside the tarball. + 2. `install.sh` verifies SHA256 + GPG signature before `tar -xzf`; fail closed on mismatch. + 3. `--no-verify` escape hatch (documented, warns). + 4. Bats tests: tampered tarball rejected; valid tarball accepted. +- **Commit**: `fix(P17): install.sh checksum+GPG verification (REQ-132, F14)` +- **Tag**: `v0.11.17` + +#### P18 — nftables ruleset hardening — REQ-133, F21 + +- **Persona**: backend-engineer (territory: `internal/emitter/nft.go`) +- **Tasks**: + 1. Add conntrack bounds (`ct state established,related accept`). + 2. Input default-deny on the orca chain. + 3. Drop invalid packets (`ct state invalid drop`). + 4. `orca doctor nft` audits live ruleset against emitted one. + 5. Go tests: emitted ruleset contains the new rules; doctor detects drift. +- **Commit**: `fix(P18): nftables ruleset hardening (REQ-133, F21)` +- **Tag**: `v0.11.18` + +#### P19 — sudoers hardening — REQ-134, F22 + +- **Persona**: backend-engineer (territory: `internal/proxmox/bootstrap.go`) +- **Tasks**: + 1. Add NOEXEC to `apt-get`/`dpkg` in the OrcaOperator sudoers (or remove if unused). + 2. `orca doctor proxmox` audits the sudoers file against the expected allowlist. + 3. Go tests: emitted sudoers has NOEXEC; doctor detects drift. +- **Commit**: `fix(P19): sudoers hardening (REQ-134, F22)` +- **Tag**: `v0.11.19` + +#### P20 — System user consistency — REQ-135, F23 + +- **Persona**: backend-engineer (territory: `internal/proxmox/bootstrap.go`, `internal/cli/peer_setup.go`) +- **Tasks**: + 1. Proxmox bootstrap creates `nologin` system user (`-r -s /usr/sbin/nologin`), matching peer-setup. + 2. `orca doctor` flags inconsistency on existing peers. + 3. `orca upgrade` migrates existing `-m -s /bin/bash` users to `-r -s /usr/sbin/nologin`. + 4. Go tests: emitted useradd matches; doctor detects the old style. +- **Commit**: `fix(P20): system user consistency (REQ-135, F23)` +- **Tag**: `v0.11.20` + +### Wave F — State storage & migration (data-engineer) + +#### P21 — SQLite file-mode + at-rest encryption — REQ-136, F8, C-31 + +- **Persona**: data-engineer (territory: `internal/store/`) +- **Tasks**: + 1. `store.Open` sets DB file mode 0600 (via `os.Chmod` after open, since SQLite creates with umask). + 2. Evaluate SQLCipher envelope (CGO-free check). If infeasible without CGO (breaks D-008), fall back to file-mode 0600 + documented threat per C-31. + 3. Document the decision in RESEARCH/PROJECT. + 4. Go tests: DB file mode is 0600 after open. +- **Commit**: `fix(P21): SQLite file-mode + at-rest encryption (REQ-136, F8, C-31)` +- **Tag**: `v0.11.21` + +#### P22 — Migration safety + identity migration — REQ-137, F19, C-34 + +- **Persona**: data-engineer (territory: `internal/migration/`, `internal/cli/upgrade.go`) +- **Tasks**: + 1. `copyFile` -> atomic temp+rename. + 2. `migrateDBSchema` runs in a transaction with `foreign_keys(ON)`. + 3. Pre-migration backup step (uses `internal/backup`). + 4. Document manual rollback (restore from backup). + 5. `orca upgrade` refuses v0.11 clusters using `--password`/bare-tokens without `--accept-identity-migration` (C-34). + 6. Go tests: migration is atomic; partial failure rolls back; `--accept-identity-migration` gate works. +- **Commit**: `fix(P22): migration safety + identity migration (REQ-137, F19, C-34)` +- **Tag**: `v0.11.22` + +### Wave G — Dual-write closure (lead-developer, gated by C-29) + +#### P23 — Legacy CA/mTLS/daemon + step-ca password-provisioner deletion — REQ-138, F16 + +- **Persona**: lead-developer (territory: `internal/security/ca.go`, `internal/transport/mtls.go`, `internal/daemon/`, `internal/stepca/`, `internal/certpaths/`) +- **Pre-gate (C-29)**: P06, P08, P09, P11 must all be shipped. +- **Tasks**: + 1. Remove `internal/security/ca.go` legacy CA; migrate `orca init` and `orca cert *` to step-ca exclusively. + 2. Remove `internal/transport/mtls.go` deprecated path. + 3. Remove daemon plaintext mode (already killed in P09, but delete the code path). + 4. Remove `internal/certpaths/` (v0.8 flat layout); `internal/paths/` is the only layout. + 5. Delete step-ca `--password-file` provisioner (already replaced by OIDC provisioner in P07). + 6. Full test suite must pass after deletion. +- **Must-haves**: `orca init` + `orca cert *` work via step-ca only; no legacy code compiled. +- **Commit**: `refactor(P23): delete legacy CA/mTLS/daemon + step-ca password-provisioner (REQ-138, F16, C-29)` +- **Tag**: `v0.11.23` + +### Wave H — Defense-in-depth (backend-engineer) + +#### P24 — known_hosts tightening + transport hardening — REQ-139, F15, F25 + +- **Persona**: backend-engineer (territory: `internal/security/flock.go`, `internal/sshpush/`) +- **Tasks**: + 1. `Flock` tightens pre-existing looser perms to 0600 (chmod after open if looser). + 2. `classifyDialErr` switched from substring to typed errors (use `*ssh.ExitError`, `net.Error` type assertions). + 3. Add SSH-exec rate limiting (token bucket per peer, default 10 req/s). + 4. Go tests: looser perms tightened; typed errors classified correctly; rate limit enforced. +- **Commit**: `fix(P24): known_hosts tightening + transport hardening (REQ-139, F15, F25)` +- **Tag**: `v0.11.24` + +#### P25 — Drift event authentication — REQ-140, F18 + +- **Persona**: backend-engineer (territory: `internal/drift/`, `scripts/orca-drift-notify.sh`) +- **Tasks**: + 1. Per-peer HMAC key (derived from master key via HKDF); deployed to peers at `0600` owned by `orca`. + 2. `orca-drift-notify.sh` signs each event with the HMAC; aggregator rejects unsigned/forged events. + 3. Go tests: forged event rejected; valid event accepted. +- **Commit**: `fix(P25): drift event authentication (REQ-140, F18)` +- **Tag**: `v0.11.25` + +#### P26 — Security integration test suite — REQ-141, C-33 + +- **Persona**: backend-engineer (territory: `tests/`) +- **Tasks**: + 1. Hermetic harness exercising: injection (P01), traversal (P02), symlink (P12), drift-forgery (P25), audit-tamper (P10), daemon-auth-negative (P09), OIDC mock-IdP flow (P04), ACL-with-OIDC-claims negative (P06), unseal/seal (P08), WebAuthn virtual-authenticator ceremony (P05), password-removal regression (P07 -- assert `--password` rejected). + 2. Gates in `.coreci.yml` `validate` pipeline (C-33). + 3. Bats + Go test runner. +- **Commit**: `test(P26): security integration test suite (REQ-141, C-33)` +- **Tag**: `v0.11.26` + +### Wave I — Documentation & release (lead-developer) + +#### P27 — Zero-trust + OIDC + WebAuthn + threat-model docs — REQ-142 + +- **Persona**: lead-developer (territory: `docs/`, `README.md`) +- **Tasks**: + 1. `docs/threat-model.md`: STRIDE per component, zero-trust model, OIDC data-flow diagram, OS surface diagram, residual risk register. + 2. `docs/oidc.md`: configure your IdP, bundled Dex offline quickstart, claim-to-namespace mapping, BYO-IdP override. + 3. `docs/webauthn.md`: passkey registration, RP ID, secure context, recovery flow. + 4. `docs/security-runbook.md`: unseal/seal, master key rotation, incident response, sudoers audit, nft audit, Shamir recovery. + 5. README security section names "no orca credentials" as an invariant (R-021). +- **Commit**: `docs(P27): zero-trust + OIDC + WebAuthn + threat-model docs (REQ-142)` +- **Tag**: `v0.11.27` + +#### P28 — Final review + ship + audit — REQ-143 + +- **Persona**: lead-developer (coordinates) +- **Tasks**: + 1. `ciagent-review` multi-persona review across all phases. + 2. `ciagent-audit` reconstruction test (git log matches `.ciagent/` files). + 3. C-32 human-gate: confirm GITEA_TOKEN rotated + `.env` re-seeded (escalation hook if pending). + 4. Merge `phase/28` -> `milestone/v0.12-security-hardening`. + 5. Merge `milestone/v0.12-security-hardening` -> `main` (rebase-then-fast-forward). + 6. Tag `v0.11.28` (= v0.12 milestone release per feature-milestone rule). + 7. Create Gitea release with full milestone summary. + 8. Delete milestone + phase branches (tags preserve history). + 9. Update REQUIREMENTS.md (mark all v0.12 REQs complete) + ROADMAP.md (mark v0.12 complete). +- **Commit**: `docs(milestone): complete v0.12 -- Security Hardening (Zero-Trust Identity) (29 phases shipped)` +- **Tag**: `v0.11.28` diff --git a/.ciagent/PROJECT.md b/.ciagent/PROJECT.md index db7c274..4494b7e 100644 --- a/.ciagent/PROJECT.md +++ b/.ciagent/PROJECT.md @@ -571,3 +571,99 @@ mTLS identity (per D-068 / C-08) is NOT needed; D-068 stands. `crypto/x509.CreateCertificate` with `URIs: []*url.URL{spiffeURI}`, exercising the exact production parsing path. 15 tests, all pass. - **Spike result record:** `internal/identity/SPIFFE_SPIKE_RESULT.md`. + +## v0.12 Scope Summary — Security Hardening (Zero-Trust Identity) + +v0.12 is a 27-execution-phase feature milestone dedicated to +comprehensive security hardening across the entire attack surface, +**including the operating system itself**. The threat-model review +(v0.11 closeout + Phase 0 RESEARCH) surfaced 25 distinct findings +(F1..F25) spanning injection, traversal, ACL, audit, crypto, OS +scripts, emitters, sudoers, system users, file modes, daemon auth, +backup, SQLite, install.sh, and migration. v0.12 closes all of them +and adopts a **zero-trust identity model** as the load-bearing +architectural change. + +### Load-bearing rule adopted in Phase 0 + +**R-021**: *Orca never issues, stores, or accepts human-identity +credentials. Human identity is exclusively external (OIDC). Machine +identity is exclusively mTLS/SPIFFE. No passwords, no Orca-issued +tokens, no CA-key passphrases.* + +### Zero-trust identity model + +Two identity layers, zero overlap: + +- **Human operators** → OIDC (external IdP, BYO) OR the **bundled Dex** + with a **WebAuthn (passkeys) connector** as the default + password-free authenticator. `orca auth login` / `orca auth register` + open the default browser to the Dex WebAuthn endpoint via OIDC + authorization-code + PKCE + local loopback redirect. After the + WebAuthn ceremony (biometric/security key), Dex redirects back with + an auth code; CLI exchanges for a short-lived ID token (1h) + + refresh. Headless/CI fallback: device-code flow. +- **Machine-to-machine** → mTLS + SPIFFE SVIDs (unchanged from v0.11). + +The "no Orca credentials" invariant holds: passkeys are public-key +credentials (the private key never leaves the authenticator); the +WebAuthn credential DB stores only public keys + credential IDs + +sign counts. No passwords, no Orca-issued tokens, no CA-key +passphrases anywhere in the system. + +### Master key sealing + +The secrets master key (32 random bytes) is **sealed to OIDC** — +wrapped by a key derived from an OIDC token exchange at unseal time. +`orca cluster unseal` (operator authenticates via OIDC → token +exchange → unwrap master key into memory → zeroed on shutdown). The +raw master key never touches disk. **Shamir 3-of-5 recovery**: at seal +time, 5 shards are printed and the operator stores them offline. If +the IdP is permanently lost AND a quorum of shards is unavailable, the +cluster is unrecoverable by design (documented residual risk; no +backdoor). + +### New requirements (REQ-119..REQ-148) + +30 net-new requirements derived from the threat-model findings and the +zero-trust identity model. See REQUIREMENTS.md and ROADMAP.md for the +full mapping. Highlights: + +- REQ-119..121: command injection, path traversal, txn path allowlist +- REQ-144: OIDC client + bundled Dex (BYO-IdP override) +- REQ-145: ACL rewrite (remove KindToken, add KindOidc, enforce) +- REQ-146: remove all password/token paths (breaking) +- REQ-147: master key seal-to-OIDC + Shamir recovery +- REQ-148: WebAuthn connector for Dex (passkeys, browser auth+register) +- REQ-122..143: integrity, crypto, OS scripts, emitters, sudoers, + system users, SQLite, migration, dual-write closure, transport, + drift auth, integration tests, docs, final review + +### v0.12 Clarified Decisions (D-series, full autonomy) + +The 10 v0.12 decisions (D-238..D-247) were resolved during CLARIFY +under full autonomy (autonomy.level=full, workflow.no_hitl=true): + +| ID | Question | Decision | Rationale | Confidence | +|----|----------|----------|-----------|------------| +| D-238 | Milestone version? | **v0.12 (minor, not v1.0)** | v1.0.0 stays deferred for post-UAT per v0.11 PRD; v0.12 is a minor feature milestone. Tags on v0.11.x patch line. | 0.95 | +| D-239 | OIDC provider model? | **Bundled Dex by default + BYO external IdP override** | Zero-trust out of the box without external setup; `oidc.issuer` repoint switches to BYO. | 0.90 | +| D-240 | Bundled Dex upstream authenticator (password-free)? | **WebAuthn (passkeys) connector** | Public-key credentials; private key never leaves authenticator; reinforces "no passwords" invariant (R-021). | 0.88 | +| D-241 | Master key sealing model? | **Seal to OIDC + Shamir 3-of-5 recovery** | No password anywhere; quorum recovery if IdP lost; no backdoor. | 0.85 | +| D-242 | CLI browser flow? | **OIDC auth-code + PKCE + local loopback redirect** | Standard OIDC browser flow; secure for public clients; headless fallback via device-code. | 0.92 | +| D-243 | WebAuthn RP ID / secure context? | **Traefik-served cluster domain (step-ca cert, R-017)** | WebAuthn requires HTTPS; Traefik already provides it; RP ID configurable via `orca auth init-idp`. | 0.90 | +| D-244 | Passkey storage? | **SQLite at ClusterDir()/webauthn-credentials.db (0600); public keys only** | Public keys are not secrets; 0600 file mode for integrity; no passphrase wrapping needed. | 0.92 | +| D-245 | Headless/CI auth fallback? | **Device-code flow** | No browser in CI; device-code is the standard OIDC headless path. | 0.90 | +| D-246 | Token storage at rest? | **~/.orca/credentials.json (0600); short-lived (1h) + refresh** | Standard OIDC token storage; 0600; refresh handles rotation; no long-lived Orca-issued tokens. | 0.92 | +| D-247 | Breaking-change handling for password/token removal? | **`orca upgrade` refuses v0.11 clusters using --password/bare-tokens without --accept-identity-migration** | No silent breakage; explicit migration gate; documented cutover. | 0.90 | + +### v0.12 is a HARDENING + IDENTITY milestone, not a direction change + +The vision ("minimalist, offline-first, CLI-first orchestration +engine inspired by HashiCorp Nomad") is unchanged. v0.12 closes the +security-surface gaps surfaced by the v0.11 threat model and adopts a +zero-trust identity model. The offline-first principle (R-003) is +preserved: the bundled Dex can run on the lead (offline), and the +mTLS-only path remains for the single-operator fully-offline case (no +human authn needed — the operator holds the pre-staged SSH key + mTLS +cert; no password, no token). diff --git a/.ciagent/REQUIREMENTS.md b/.ciagent/REQUIREMENTS.md index 104a6f8..51d84e5 100644 --- a/.ciagent/REQUIREMENTS.md +++ b/.ciagent/REQUIREMENTS.md @@ -251,3 +251,58 @@ operator decision Q2=C. - No new phases added (Q3=A folds ingress into P15.5; Q2=C folds CLI commands into existing phases). - P09 expands (REQ-107 aggregator extension); P10 expands (REQ-103…REQ-113, the largest phase); P15.5 expands (REQ-099…REQ-102 ingress + REQ-118 mTLS doctor). - P05 gains REQ-116 (migrate); P06 gains REQ-117 (logs --all-nodes); P14a gains REQ-115 (upgrade); P14b gains REQ-114 (rotate-lead). + +## v0.12 Milestone Summary — Security Hardening (Zero-Trust Identity) + +**Status**: in progress (Phase 0). 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. + +### Wave A — Critical injection & traversal + +| 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 | + +### 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 | + +### Scope notes (v0.12) + +- REQ-119..REQ-148 = 30 net-new requirements (REQ count grows 118 -> 148). +- 29 phases (P0 + P01..P27 + P28 final); GRILL may split/merge. +- P04 (OIDC+Dex) and P05 (WebAuthn) are the new `feat` phases; the rest are `fix`/`chore`/`test`/`docs`/`refactor`. Milestone type = feature (at least one `feat`). +- Tags on v0.11.x patch line: `v0.11.0` (P0) ... `v0.11.29` (P28 final = v0.12 milestone release). +- v1.0.0 production-ready tag stays deferred for post-v0.12 UAT (per v0.11 PRD). diff --git a/.ciagent/RESEARCH_v0.12.md b/.ciagent/RESEARCH_v0.12.md new file mode 100644 index 0000000..38f1a1f --- /dev/null +++ b/.ciagent/RESEARCH_v0.12.md @@ -0,0 +1,231 @@ +# Research: v0.12 Security Hardening (Zero-Trust Identity) + +## Source material + +The v0.12 threat model was produced by a comprehensive security-surface +review (Phase 0 RESEARCH, 2026-08-07) covering the entire Orca codebase +AND the operating-system-level surface it touches. The review ingested: + +- v0.11 closeout (CHECKPOINT.json: milestone_complete=true, 24 phases + shipped, threat model produced in P15.5). +- The 12-area security-surface inventory (see "Threat model findings" + below), produced by deep code exploration of every `internal/` package, + every `scripts/` file, the emitter surface, the OS-touching CLI + commands, and the dual-write window. +- The operator's locked decisions (D-238..D-247) on zero-trust identity: + bundled Dex + WebAuthn, master key seal-to-OIDC + Shamir, no Orca + credentials (R-021). + +## Load-bearing rule adopted + +**R-021**: *Orca never issues, stores, or accepts human-identity +credentials. Human identity is exclusively external (OIDC). Machine +identity is exclusively mTLS/SPIFFE. No passwords, no Orca-issued +tokens, no CA-key passphrases.* + +## Threat model findings (F1..F25) + +| # | Area | Finding | Severity | Phase | REQ | +|---|------|---------|----------|-------|-----| +| F1 | ACL | `acl.ACL.Check` exists but no caller enforces it -- daemon & SSH-push have zero authz | Critical | P06 | REQ-145 | +| F2 | Audit | Audit log is plain SQLite INSERT -- no hash chain, no MAC, not tamper-evident | Critical | P10 | REQ-125 | +| F3 | Runtime | `podman.go:57` & `wasm.go:39` interpolate cmdStr unquoted into SSH exec -> command injection | Critical | P01 | REQ-119 | +| F4 | Namespace | `ns create` doesn't reject `..`/`/` -> path traversal | Critical | P02 | REQ-120 | +| F5 | Txn | `apply.sh` python heredoc writes to arbitrary paths from desired-state.json -- no allowlist | Critical | P03 | REQ-121 | +| F6 | Daemon | Plaintext mode (default) has no auth on read endpoints; `--pprof` unauthenticated | High | P09 | REQ-123/124 | +| F7 | Backup | `Restore` creates symlinks without validating Linkname -> symlink-to-/etc/shadow | High | P12 | REQ-127 | +| F8 | SQLite | DBs unencrypted, no explicit file mode (defaults to umask 0644) | High | P21 | REQ-136 | +| F9 | SPIFFE | `VerifySVID` checks URI SAN but not the cert chain against the CA | High | P11 | REQ-126 | +| F10 | step-ca | `step ca certificate` writes SVID privkey to /tmp/orca-* world-readable | High | P13 | REQ-128 | +| F11 | Scripts | `orca-aggregate.sh:64` interpolates raw peer output into JSON -> JSON injection | High | P16 | REQ-131 | +| F12 | Secrets | No master.key rotation; no passphrase/KDF wrapping (raw 32 bytes, 0600-only) | High | P14 | REQ-129 | +| F13 | File modes | `EnforceFileModes` only checks ca.{crt,key} -- SSH key, master key, server cert not re-verified | Medium | P15 | REQ-130 | +| F14 | install.sh | curl|bash with no checksum/signature verification of the tarball | High | P17 | REQ-132 | +| F15 | known_hosts | `Flock` creates 0600 if missing but doesn't tighten pre-existing looser perms | Medium | P24 | REQ-139 | +| F16 | Dual-write | Legacy CA/mTLS/daemon marked Deprecated but still load-bearing -- expanded attack surface | Medium | P23 | REQ-138 | +| F17 | History | Real GITEA_TOKEN committed in 0cba1aa, still in git history | High (human-gated) | P28 (gate) | -- | +| F18 | Drift | `orca-pull.sh` R-020 grep-based JSON parsing fragile; drift events unauthenticated | Medium | P16/P25 | REQ-131/140 | +| F19 | Migration | `ALTER TABLE DROP COLUMN` irreversible; `copyFile` non-atomic; no rollback | Medium | P22 | REQ-137 | +| F20 | OS scripts | `orca-aggregate.sh`/`orca-remediate.sh` run as root with TOFU SSH (accept-new) | Medium | P16/P24 | REQ-131/139 | +| F21 | nftables | Emitted ruleset has SYN-flood + rate-limit but no conntrack bounds, no input default-deny | Medium | P18 | REQ-133 | +| F22 | sudoers | `OrcaOperator` sudoers has NOEXEC on pct/qm but allows apt-get/dpkg without NOEXEC | Medium | P19 | REQ-134 | +| F23 | system user | Proxmox creates login user (-m -s /bin/bash); peer-setup creates nologin -- inconsistent privilege | Medium | P20 | REQ-135 | +| F24 | Dispatch | No request body size limits (json.Decode with no MaxBytesReader) | Low | P09 | REQ-124 | +| F25 | Transport | `classifyDialErr` is substring-based; no SSH-exec rate limiting | Low | P24 | REQ-139 | + +## Zero-trust identity model (NEW in v0.12) + +### Two identity layers, zero overlap + +- **Human operators** -> OIDC (external IdP, BYO) OR the bundled Dex + with a WebAuthn (passkeys) connector as the default password-free + authenticator. `orca auth login` / `orca auth register` open the + default browser to the Dex WebAuthn endpoint via OIDC + authorization-code + PKCE + local loopback redirect. After the + WebAuthn ceremony (biometric/security key), Dex redirects back with + an auth code; CLI exchanges for a short-lived ID token (1h) + + refresh. Headless/CI fallback: device-code flow. +- **Machine-to-machine** -> mTLS + SPIFFE SVIDs (unchanged from v0.11). + +### Why WebAuthn satisfies "no passwords anywhere" + +Passkeys are **public-key credentials**. The private key is generated +on the authenticator (TPM/security key/phone Secure Enclave) and never +leaves it. The server (Dex) stores only the **public key** + credential +ID + sign count. There is no password, no shared secret, no replayable +credential. This is the strongest authentication primitive available +and directly satisfies R-021. + +### Bundled Dex architecture + +- **Dex** (github.com/dexidp/dex) is the OIDC frontend. Orca bundles a + Dex binary + config template, deployed via `orca auth init-idp` as a + systemd unit on the lead, fronted by Traefik (R-017, step-ca cert). +- **`orca-webauthn-connector`** is a custom Dex connector (~300 LoC Go, + using `github.com/go-webauthn/webauthn`). It serves: + - `GET /orca/webauthn/register` -- registration HTML/JS page. + - `POST /orca/webauthn/register/begin` -- WebAuthn registration + challenge (random nonce, user info). + - `POST /orca/webauthn/register/finish` -- attestation verification, + credential storage. + - `GET /orca/webauthn/login` -- login HTML/JS page. + - `POST /orca/webauthn/login/begin` -- assertion challenge. + - `POST /orca/webauthn/login/finish` -- assertion verification, OIDC + `sub` extraction, redirect with auth code. +- **Passkey storage**: SQLite at `ClusterDir()/webauthn-credentials.db` + (0600). Schema: `credentials(user_id TEXT PRIMARY KEY, credential_id + BLOB, public_key BLOB, sign_count INTEGER, aaguid TEXT, created_at + TEXT)`. Public keys only; no private keys, no secrets. +- **BYO external IdP override**: `oidc.issuer` in config repoints to + an external IdP. The bundled Dex + WebAuthn connector is bypassed; + the external IdP's authenticators (including its own WebAuthn) are + used. Orca never sees the upstream credentials. + +### RQ-1 resolution (RESEARCH binding question) + +**RQ-1**: How does the bundled Dex bootstrap an upstream identity +without any password, given the mTLS-only constraint? + +**Answer (resolved by C3/D-240)**: The bundled Dex's upstream +authenticator IS the WebAuthn connector. No external password source +is needed for the bundled path. The WebAuthn connector serves the +registration + login ceremonies directly; Dex maps the credential ID +to an OIDC `sub`. BYO-IdP covers password-based upstreams (LDAP/AD) +if an operator insists -- but those never flow through Orca. + +**C-37 fallback** (kept if WebAuthn proves infeasible): bundled Dex +ships mTLS-client-cert-only (Traefik `X-Forwarded-Client-Cert` header +-> Dex `typed-external-connector`). Password-based upstreams require +BYO external IdP. The "no Orca credentials" invariant holds regardless. + +### Master key sealing architecture + +- **Seal**: at `orca cluster seal`, the in-memory master key is + encrypted with a key derived from the operator's OIDC ID token + (HKDF-SHA256 of the token's `sub` + a fresh 32-byte salt). The + sealed blob (`salt || ciphertext`) is stored at + `ClusterDir()/master.key.sealed` (0600). The raw key is zeroed from + memory. Shamir 3-of-5 shards are printed for offline recovery. +- **Unseal**: at `orca cluster unseal`, the operator authenticates via + OIDC (WebAuthn ceremony). The resulting ID token's `sub` + the + stored salt derive the unwrapping key. The master key is unwrapped + into memory and held for the cluster's lifetime. Zeroed on shutdown. +- **Recovery**: if the IdP is lost, the operator presents 3 of 5 + Shamir shards to `orca cluster unseal --recovery`. The shards + reconstruct the seal key; the master key is unwrapped. No backdoor. +- **mTLS-only offline path**: for the single-operator fully-offline + case (no OIDC), the seal key is derived from the cluster's own CA. + The operator holds the CA (a cert, not a password). Shamir recovery + applies to the OIDC-sealed mode only. + +### Offline-first reconciliation (R-003) + +The OIDC provider must be reachable to unseal the master key and to +authenticate operators. For offline/air-gapped clusters, the operator +runs the **bundled Dex on the lead** (offline). For the +single-operator fully-offline case, the operator can skip OIDC and +rely on mTLS-only machine identity (no human authn needed -- the +operator holds the pre-staged SSH key + mTLS cert; no password, no +token). Orca stays minimal (no bundled IdP beyond Dex); it validates +tokens against whatever issuer the operator configures. + +## Dependency posture (new in v0.12) + +v0.12 adds these dependencies (all CGO-free, audited): + +- `github.com/coreos/go-oidc/v3` -- OIDC client (token verification, + JWKS, ID token parsing). Pure Go. +- `github.com/go-webauthn/webauthn` -- WebAuthn library (registration, + login, attestation/assertion verification). Pure Go. +- `github.com/dexidp/dex` -- bundled Dex binary (vendored, not a Go + import; deployed as a separate systemd unit). Apache-2.0. +- `golang.org/x/crypto/ssh/...` -- already a dependency (sshpush). + +No CGO. No gRPC. No ConnectRPC. No YAML parser. The "stdlib + minimal +deps" posture (D-008) is preserved. + +## Codebase reality (verified 2026-08-07) + +- `internal/acl/acl.go` -- ACL exists but is unenforced (F1). P06 + rewrites it (remove KindToken, add KindOidc, wire enforcement). +- `internal/runtime/podman.go:57`, `internal/runtime/wasm.go:39` -- + unquoted cmdStr interpolation (F3). P01 fixes via shellQuote. +- `internal/cli/ns.go:nsCreateCmd` -- no `..`/`/` rejection (F4). P02 + adds `validateNamespaceName`. +- `internal/txn/txn.go:renderApplyScript` -- arbitrary path writes + (F5). P03 adds prefix allowlist. +- `internal/security/ca.go` -- legacy CA, deprecated but load-bearing + (F16). P23 deletes it (gated on P06/P08/P09/P11). +- `internal/secrets/secrets.go` -- master key raw file, no rotation + (F12). P08 seals it to OIDC; P14 adds rotation. +- `internal/audit/audit.go` -- plain SQLite INSERT (F2). P10 adds + hash-chain + HMAC. +- `internal/emitter/nft.go` -- no conntrack/default-deny (F21). P18 + hardens the ruleset. +- `internal/proxmox/bootstrap.go:29` -- `--password` bootstrap (F23, + R-021 violation). P07 removes it. +- `internal/identity/spiffe.go:95` -- no chain validation (F9). P11 + fixes. +- `scripts/install.sh` -- no checksum verification (F14). P17 adds + SHA256SUMS + GPG signature. +- `scripts/orca-aggregate.sh:64` -- raw JSON interpolation (F11). P16 + replaces with jq/Go. + +## Alignment with existing gates + +- **C-19** (threat model) -- v0.11 P15.5 produced the initial threat + model; v0.12 is the comprehensive expansion (full OS surface). +- **C-08** (SPIFFE spike) -- passed; v0.12 P11 hardens the verification + path. +- **R-001..R-020** -- unchanged; R-021 is an extension, not a reversal. +- **D-008** (no CGO) -- preserved; all new deps are pure Go. + +## Risks (for GRILL to pressure-test) + +- **P07 (password removal) is breaking** -- mitigation: C-34 migration + gate (`--accept-identity-migration`). +- **P08 (master key seal) is the riskiest phase** -- a bug corrupts all + secrets at rest. Mitigation: `--dry-run`, atomic re-encryption, + automatic rollback to old sealed key on any failure. +- **P21 (SQLite encryption) may need CGO** -- C-31 fallback to + file-mode 0600 + documented threat if SQLCipher needs CGO. No CGO. +- **P23 (dual-write closure) is high-impact** -- removing the legacy + CA breaks `orca init`/`orca cert` if step-ca isn't fully wired. + Mitigation: gate on P06/P08/P09/P11, full test coverage before + deletion. +- **P05 (WebAuthn connector) is new ground** -- ~300 LoC custom Dex + connector. Mitigation: C-37 fallback (mTLS-client-cert-only) if + WebAuthn proves infeasible; virtual-authenticator integration tests + (P26) using `go-webauthn` test helpers. +- **Bundled Dex is a new systemd unit + Traefik route** -- operational + surface growth. Mitigation: `orca doctor oidc` checks Dex health, + JWKS reachability, WebAuthn endpoint TLS. +- **C-32 human gate** (leaked GITEA_TOKEN) could stall the final ship. + Escalation path: ship as `v0.11.29-rc1` if rotation pending, + `v0.11.29` when confirmed. + +## Next steps + +Phase 0 proceeds to IDEATE (produce the 30 net-new requirements +REQ-119..REQ-148), then PLAN (29 phases, wave ordering, persona +assignments), then GRILL (ratify C-29..C-38). diff --git a/.ciagent/ROADMAP.md b/.ciagent/ROADMAP.md index 184aa19..01c71ad 100644 --- a/.ciagent/ROADMAP.md +++ b/.ciagent/ROADMAP.md @@ -399,3 +399,152 @@ tags: `v0.10.0`…`v0.10.21`. - Leader-elected Raft coordinator - External CA / Let's Encrypt / cert transparency - Online-only features (HSTS, OCSP stapling, telemetry) + +## Milestone v0.12: Security Hardening (Zero-Trust Identity) — IN PROGRESS + +**Scope**: comprehensive security hardening across the entire attack +surface, **including the operating system itself**, plus adoption of a +zero-trust identity model. The v0.12 threat-model review (Phase 0 +RESEARCH) surfaced 25 distinct findings (F1..F25) spanning injection, +traversal, ACL, audit, crypto, OS scripts, emitters, sudoers, system +users, file modes, daemon auth, backup, SQLite, install.sh, and +migration. v0.12 closes all of them and adopts **R-021** (no Orca +credentials) as the load-bearing architectural change: 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 operator locked two architectural decisions: **(1) bundled Dex by +default + BYO external IdP override** (D-239), and **(2) master key +seal-to-OIDC + Shamir 3-of-5 recovery** (D-241). A third decision added +**WebAuthn (passkeys) as the bundled password-free authenticator** for +Dex (D-240) -- passkeys are public-key credentials (private key never +leaves the authenticator), directly satisfying R-021. + +**Milestone type**: feature (P04 OIDC+Dex and P05 WebAuthn ship `feat` +phases; the rest are `fix`/`chore`/`test`/`docs`/`refactor`). + +- [ ] Phase 0: Pre-execution (specify -> clarify -> research -> ideate -> plan -> grill) -- tag `v0.11.0` +- [ ] Phase P01: Command injection fix (podman/wasm shellQuote) (REQ-119, F3) -- tag `v0.11.1` +- [ ] Phase P02: Namespace path traversal fix (REQ-120, F4) -- tag `v0.11.2` +- [ ] Phase P03: Txn apply path allowlist (REQ-121, F5) -- tag `v0.11.3` +- [ ] Phase P04: OIDC client + bundled Dex (REQ-144; BYO-IdP override) -- tag `v0.11.4` +- [ ] Phase P05: WebAuthn connector for Dex (REQ-148; passkeys, browser auth+register) -- tag `v0.11.5` +- [ ] Phase P06: ACL rewrite to OIDC claims + enforcement (REQ-145, REQ-122, F1) -- tag `v0.11.6` +- [ ] Phase P07: Remove all password/token paths (breaking; REQ-146, R-021, C-34) -- tag `v0.11.7` +- [ ] Phase P08: Master key seal-to-OIDC + Shamir 3-of-5 (REQ-147, C-35) -- tag `v0.11.8` +- [ ] Phase P09: Daemon auth hardening (REQ-123, REQ-124, F6, F24) -- tag `v0.11.9` +- [ ] Phase P10: Audit log tamper-evidence (REQ-125, F2) -- tag `v0.11.10` +- [ ] Phase P11: SVID chain validation (REQ-126, F9) -- tag `v0.11.11` +- [ ] Phase P12: Backup symlink validation (REQ-127, F7) -- tag `v0.11.12` +- [ ] Phase P13: step-ca /tmp hardening (REQ-128, F10) -- tag `v0.11.13` +- [ ] Phase P14: Master key rotation (re-seal to OIDC; REQ-129, F12, C-30) -- tag `v0.11.14` +- [ ] Phase P15: File-mode audit expansion (REQ-130, F13) -- tag `v0.11.15` +- [ ] Phase P16: aggregate.sh JSON injection + drift-gate parse fix (REQ-131, F11, F18) -- tag `v0.11.16` +- [ ] Phase P17: install.sh checksum+GPG verification (REQ-132, F14) -- tag `v0.11.17` +- [ ] Phase P18: nftables ruleset hardening (REQ-133, F21) -- tag `v0.11.18` +- [ ] Phase P19: sudoers hardening (REQ-134, F22) -- tag `v0.11.19` +- [ ] Phase P20: System user consistency (REQ-135, F23) -- tag `v0.11.20` +- [ ] Phase P21: SQLite file-mode + at-rest encryption (REQ-136, F8, C-31) -- tag `v0.11.21` +- [ ] Phase P22: Migration safety + identity migration (REQ-137, F19, C-34) -- tag `v0.11.22` +- [ ] Phase P23: Legacy CA/mTLS/daemon + step-ca password-provisioner deletion (REQ-138, F16; **gate C-29: P06/P08/P09/P11**) -- tag `v0.11.23` +- [ ] Phase P24: known_hosts tightening + transport hardening (REQ-139, F15, F25) -- tag `v0.11.24` +- [ ] Phase P25: Drift event authentication (REQ-140, F18) -- tag `v0.11.25` +- [ ] Phase P26: Security integration test suite (REQ-141, C-33) -- tag `v0.11.26` +- [ ] Phase P27: Zero-trust + OIDC + WebAuthn + threat-model docs (REQ-142) -- tag `v0.11.27` +- [ ] Phase P28: Final review + ship + audit (milestone release) -- tag `v0.11.28` = **v0.12 milestone release** + +**Milestone tag**: `v0.11.28` (final phase patch = milestone release per +feature-milestone progressive-patch rule; no separate `v0.12.0` tag). +Per-phase tags: `v0.11.0`..`v0.11.28` (29 tags). Tags run on the +previous minor's patch line (v0.11.x) per branch-strategy.md. The +milestone branch label uses the milestone number +(`milestone/v0.12-security-hardening`); no separate minor tag. + +The v1.0.0 production-ready tag stays deferred for post-v0.12 UAT +(per v0.11 PRD; v0.12 is a minor feature milestone, not the v1.0 cut). + +### Per-phase REQ coverage (v0.12) + +- **P01** -- Command injection (REQ-119, F3) +- **P02** -- Namespace path traversal (REQ-120, F4) +- **P03** -- Txn apply path allowlist (REQ-121, F5) +- **P04** -- OIDC client + bundled Dex (REQ-144; D-239, D-242, D-246) +- **P05** -- WebAuthn connector (REQ-148; D-240, D-243, D-244, C-38) +- **P06** -- ACL rewrite + enforcement (REQ-145, REQ-122, F1) +- **P07** -- Remove password/token paths (REQ-146, R-021, C-34) +- **P08** -- Master key seal-to-OIDC + Shamir (REQ-147, D-241, C-35) +- **P09** -- Daemon auth (REQ-123, REQ-124, F6, F24) +- **P10** -- Audit tamper-evidence (REQ-125, F2) +- **P11** -- SVID chain validation (REQ-126, F9) +- **P12** -- Backup symlink validation (REQ-127, F7) +- **P13** -- step-ca /tmp hardening (REQ-128, F10) +- **P14** -- Master key rotation (REQ-129, F12, C-30) +- **P15** -- File-mode audit expansion (REQ-130, F13) +- **P16** -- aggregate.sh JSON injection + drift-gate (REQ-131, F11, F18) +- **P17** -- install.sh checksum+GPG (REQ-132, F14) +- **P18** -- nftables ruleset hardening (REQ-133, F21) +- **P19** -- sudoers hardening (REQ-134, F22) +- **P20** -- System user consistency (REQ-135, F23) +- **P21** -- SQLite file-mode + encryption (REQ-136, F8, C-31) +- **P22** -- Migration safety + identity migration (REQ-137, F19, C-34) +- **P23** -- Dual-write closure (REQ-138, F16; **gate C-29**) +- **P24** -- known_hosts + transport hardening (REQ-139, F15, F25) +- **P25** -- Drift event authentication (REQ-140, F18) +- **P26** -- Security integration test suite (REQ-141, C-33) +- **P27** -- Zero-trust + OIDC + WebAuthn + threat-model docs (REQ-142) +- **P28** -- Final review + ship + audit (REQ-143) + +### New load-bearing rule adopted in Phase 0 + +- **R-021** -- Orca never issues, stores, or accepts human-identity + credentials. Human identity is exclusively external (OIDC). Machine + identity is exclusively mTLS/SPIFFE. No passwords, no Orca-issued + tokens, no CA-key passphrases. + +### Binding conditions (for GRILL ratification; C-29..C-38) + +- **C-29**: P23 (dual-write closure) gated on P06/P08/P09/P11 all shipped. +- **C-30**: P14 (master key rotation) reversible; `--dry-run` mandatory; auto-rollback to old sealed key on any ns failure. +- **C-31**: P21 (SQLite encryption): CGO-free fallback to file-mode 0600 + documented threat if SQLCipher needs CGO. No CGO. +- **C-32**: **Human-gate**: leaked GITEA_TOKEN (F17) rotated + `.env` re-seeded before P28 ships. History-scrub best-effort, non-blocking. Escalation hook in `---ci---`. +- **C-33**: P26 (security integration tests) in `.coreci.yml` `validate`, gates merges -- not opt-in. +- **C-34**: P07 (password/token removal) breaking. `orca upgrade` (P22) refuses v0.11 clusters using `--password`/bare-tokens without `--accept-identity-migration`. No silent breakage. +- **C-35**: P08 (Shamir recovery): 3-of-5 shards printed at seal time, operator stores offline. If IdP lost AND quorum unavailable -> cluster unrecoverable by design (documented residual risk). No backdoor. +- **C-36**: OIDC client secret (confidential clients) at `ClusterDir()/oidc-client-secret` (0600), rotatable via `orca auth rotate-client-secret`, never committed. Public PKCE clients avoid even this. +- **C-37**: P04 (bundled Dex): if WebAuthn proves infeasible, bundled Dex ships mTLS-client-cert-only; password-based upstreams require BYO external IdP. The "no Orca credentials" invariant holds regardless. *(Largely moot -- WebAuthn solves it.)* +- **C-38**: P05 (WebAuthn): RP ID must match the cluster's Traefik-served domain; `orca auth init-idp` configures it. HTTPS secure context via Traefik (step-ca cert). P26 integration tests use the WebAuthn virtual-authenticator API -- no hardware key required in CI. + +### Risk register (from grill + research, for ongoing monitoring) + +- **P07 breaking change** (mitigation: C-34 migration gate) +- **P08 master key seal is riskiest** (mitigation: `--dry-run`, atomic, auto-rollback, C-35 Shamir recovery) +- **P21 SQLite encryption may need CGO** (mitigation: C-31 fallback to file-mode 0600) +- **P23 dual-write closure high-impact** (mitigation: gate C-29; full test coverage before deletion) +- **P05 WebAuthn connector is new ground** (mitigation: C-37 mTLS-client-cert fallback; virtual-authenticator tests in P26) +- **Bundled Dex is a new systemd unit + Traefik route** (mitigation: `orca doctor oidc` health check) +- **C-32 human gate could stall final ship** (mitigation: ship as `v0.11.28-rc1` if rotation pending) +- **29 phases is large** (mitigation: grill may split/merge; operator accepted "more than 20 if warranted") + +### Deferred to v1.x (out of scope for v0.12) + +- HA step-ca (active/passive via systemd) +- `sqlite-wal-shared` / `git` / `file+flock` state backends +- OS keyring integration for master key (v0.12 uses OIDC seal instead) +- Full cluster-rolling-upgrade orchestrator (v0.12 ships the thin `orca upgrade` wrapper only) +- Live-migrate with storage replication (v0.12 ships drain+reschedule only) +- Journald log shipping (optional centralized audit) +- Network policy (`nftables` snippets beyond the ingress ruleset) +- GPU / TPU constraints + +### Deferred to v2.x (out of scope for v1.x) + +- Full Nomad-HCL parser with no conversion round-trip +- Nomad-API subset for migrating existing Nomad fleets +- Nomad driver bridge +- Helm-equivalent templating (probably never) +- Service mesh beyond Traefik +- CRDs / Operators / Plugin model +- Leader-elected Raft coordinator +- External CA / Let's Encrypt / cert transparency +- Online-only features (HSTS, OCSP stapling, telemetry) diff --git a/.ciagent/config.json b/.ciagent/config.json index e30dc8a..6cfaf34 100644 --- a/.ciagent/config.json +++ b/.ciagent/config.json @@ -5,7 +5,7 @@ "slug": "orca", "name": "Orca", "description": "Offline/CLI-first orchestration engine (Orca) \u2014 Nomad-inspired, far simpler than Kubernetes", - "milestone": "v0.11", + "milestone": "v0.12", "phase": 0, "milestone_type": "feature", "default_branch": "main",