cf0df0f157
---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.
22 KiB
22 KiB
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 separatev0.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:
- Add
shellQuotehelper (or usegolang.org/x/crypto/ssh-safe quoting) tointernal/runtime/. - Fix
podman.go:57:fmt.Sprintf("podman run -d --name %s %q %s", name, image, shellQuote(cmdStr)). - Fix
wasm.go:39: same pattern forwasmtime run. - Add Go regression tests:
;,|,$(), backticks, newline,$IFS,<>()injection attempts. - Add bats test: a jobspec with a malicious command runs the literal command, not the injected shell.
- Add
- 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:
- Add
validateNamespaceName(name)tointernal/ns/: reject..,/, leading-, null bytes, control chars, empty, length > 128. - Wire into
ns create,ns inherit,ns set-constraint, and any path-accepting ns command. - Add Go fuzz test (
FuzzValidateNamespaceName). - Add regression test:
orca ns create "../../etc"fails with a clear error.
- Add
- 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:
- In
txn.go:renderApplyScript, add path validation to the python heredoc: everypathindesired-state.jsonmust match a prefix in the allowlist (/etc/orca/,/etc/traefik/orca*,/etc/systemd/system/orca-*,/etc/nftables.d/orca*,/etc/syncthing/orca*). - Reject with a clear error + exit code on mismatch.
- Add Go test: a desired-state with
"path": "/etc/shadow"is rejected. - Add bats test:
orca-pull.shwith a crafted manifest refuses.
- In
- 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:
- Add
github.com/coreos/go-oidc/v3dependency. internal/identity/oidc.go: OIDC client (provider discovery, JWKS cache + refresh, ID token verification, token storage at~/.orca/credentials.json0600).orca auth login/logout/statusCLI: browser auth-code + PKCE + local loopback redirect (127.0.0.1:<port>/callback); headless device-code fallback.orca auth init-idp: bootstrap bundled Dex (systemd unit + config template + Traefik route) on the lead;--rp-id <domain>config.- OIDC config block in
internal/config/:oidc.issuer,client_id,client_secret,scopes. - BYO external IdP override:
oidc.issuerrepoint bypasses bundled Dex. - Go tests: mock OIDC provider, JWKS rotation, token refresh, login/logout flow.
- Add
- Must-haves:
orca auth loginproduces a valid ID token;orca auth statusshows it;--oidcflag 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/, newinternal/webauthn/) - Tasks:
- Add
github.com/go-webauthn/webauthndependency. internal/webauthn/connector.go: Dex connector (~300 LoC) -- registration + login ceremonies at/orca/webauthn/{register,login}.internal/webauthn/store.go: passkey storage SQLite atClusterDir()/webauthn-credentials.db(0600); schema:credentials(user_id, credential_id, public_key, sign_count, aaguid, created_at).orca auth registerCLI: browser flow to register a new passkey.- RP ID = cluster Traefik domain (from
orca auth init-idp --rp-id); secure context via step-ca cert (R-017). - Go tests using
go-webauthnvirtual-authenticator test helpers (no hardware key).
- Add
- 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:
- Remove
KindTokenfrominternal/acl/acl.goentirely. - Add
KindOidc: mapssub+groups-> namespace permissions. acl.Checktakes an OIDC claims struct (or SPIFFE SVID for machine identity).- Wire
acl.Checkinto daemon handlers (read/write/admin by route). - Wire
acl.Checkinto SSH-push applier: validateORCA_OIDC_TOKENenv var against JWKS before applying any txn. acl.jsonfile mode tightened to 0600.- Deny-by-default enforced; actor recorded in audit log.
- Go tests: ACL-negative (unauthorized sub denied), ACL-positive, machine identity (SVID) still works.
- Remove
- Must-haves: no request applies without a valid OIDC token or SVID;
KindTokenremoved; 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:
- Remove
--password/$ORCA_PROXMOX_PASSWORDfrom Proxmox join (proxmox/bootstrap.go:29); replace with pre-staged-key-only orstep sshOIDC cert exchange. - Remove step-ca
--password-fileprovisioner; migrate to OIDC provisioner (step-ca natively supports OIDC). - Remove any bare-token CLI paths (already removed in P06, but sweep for stragglers).
- Add deprecation/migration docs:
--accept-identity-migrationflag onorca upgrade(P22 enforces). - Go tests:
--passwordflag is rejected with a clear error pointing to the migration guide.
- Remove
- Must-haves: no password accepted anywhere;
--passwordrejected; 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/, newinternal/seal/) - Tasks:
internal/seal/seal.go: seal/unseal using HKDF-SHA256 of OIDC ID tokensub+ fresh 32-byte salt; sealed blob atClusterDir()/master.key.sealed(0600).- Shamir 3-of-5:
internal/seal/shamir.go(usinggolang.org/x/crypto/...or a vendored Shamir impl); print 5 shards at seal time. orca cluster unseal/sealCLI: unseal via OIDC auth;--recovery+ 3 shards for IdP-lost case.- mTLS-only offline path: seal key derived from cluster CA.
- Master key zeroed on shutdown (use
memguardor manualcrypto/randoverwrite). - Go tests: seal -> unseal round-trip; recovery with 3 shards; 2 shards fails; raw key never on disk (assert no
master.keyfile, onlymaster.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:
- Remove plaintext mode entirely (mandatory mTLS).
- Accept OIDC bearer as second factor on human-facing endpoints.
http.MaxBytesReaderon all JSON-decoding handlers;MaxHeaderBytesset.- pprof loopback-only by default;
--pprof-allow-publicrequires confirmation. - 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:
- Add
prev_hash+entry_hashcolumns toaudit_logtable (migration 0008). AuditRepo.Appendcomputesentry_hash = sha256(prev_hash || payload), stores it; HMAC-SHA256 under master key on the chain head (stored separately).- SQLite trigger blocks UPDATE/DELETE on
audit_log. orca doctor auditverifies the chain (recomputes hashes, checks HMAC).- Actor field carries OIDC
subor SPIFFE SVID. - Go tests: tamper detection (modify a row -> doctor fails); append-only enforcement (DELETE fails).
- Add
- 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:
VerifySVIDloads the CA pool (fromClusterDir()/ca.crtor step-ca root) and validates the full cert chain.- Reject certs signed by unknown CAs even with correct URI SAN.
- 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:
RestorerejectsLinknamethat's absolute, contains.., or points outsideORCA_HOME.- 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:
step ca certificatewrites to0600temp underClusterDir()/step-tmp/(orTMPDIRoverride).- Cleanup in
defer;mkdir -pwith 0700 on the temp dir. - 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:
orca secrets rotate-master: generate new master key, re-encrypt all namespace secrets, re-seal to OIDC.--dry-runreports affected namespaces without writing.- Atomic per-namespace re-encryption; auto-rollback to old sealed key on any ns failure.
- 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:
EnforceFileModesextended to SSH key, master key (sealed blob), server cert/key, known_hosts.orca doctor modeschecks all.- Startup refuses to run on violation.
- 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:
- Replace
printfinterpolation inorca-aggregate.sh:64withjq-based JSON construction (or a Go-side aggregator emitting JSON). - Fix
orca-pull.shR-020 parsing to usejqinstead of grep. - Bats tests: malicious peer output doesn't corrupt
cluster.json; drift gate correctly excludes acknowledged drift.
- Replace
- 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:
release.shpublishesSHA256SUMS+SHA256SUMS.asc(GPG-signed) alongside the tarball.install.shverifies SHA256 + GPG signature beforetar -xzf; fail closed on mismatch.--no-verifyescape hatch (documented, warns).- 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:
- Add conntrack bounds (
ct state established,related accept). - Input default-deny on the orca chain.
- Drop invalid packets (
ct state invalid drop). orca doctor nftaudits live ruleset against emitted one.- Go tests: emitted ruleset contains the new rules; doctor detects drift.
- Add conntrack bounds (
- 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:
- Add NOEXEC to
apt-get/dpkgin the OrcaOperator sudoers (or remove if unused). orca doctor proxmoxaudits the sudoers file against the expected allowlist.- Go tests: emitted sudoers has NOEXEC; doctor detects drift.
- Add NOEXEC to
- 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:
- Proxmox bootstrap creates
nologinsystem user (-r -s /usr/sbin/nologin), matching peer-setup. orca doctorflags inconsistency on existing peers.orca upgrademigrates existing-m -s /bin/bashusers to-r -s /usr/sbin/nologin.- Go tests: emitted useradd matches; doctor detects the old style.
- Proxmox bootstrap creates
- 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:
store.Opensets DB file mode 0600 (viaos.Chmodafter open, since SQLite creates with umask).- Evaluate SQLCipher envelope (CGO-free check). If infeasible without CGO (breaks D-008), fall back to file-mode 0600 + documented threat per C-31.
- Document the decision in RESEARCH/PROJECT.
- 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:
copyFile-> atomic temp+rename.migrateDBSchemaruns in a transaction withforeign_keys(ON).- Pre-migration backup step (uses
internal/backup). - Document manual rollback (restore from backup).
orca upgraderefuses v0.11 clusters using--password/bare-tokens without--accept-identity-migration(C-34).- Go tests: migration is atomic; partial failure rolls back;
--accept-identity-migrationgate 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:
- Remove
internal/security/ca.golegacy CA; migrateorca initandorca cert *to step-ca exclusively. - Remove
internal/transport/mtls.godeprecated path. - Remove daemon plaintext mode (already killed in P09, but delete the code path).
- Remove
internal/certpaths/(v0.8 flat layout);internal/paths/is the only layout. - Delete step-ca
--password-fileprovisioner (already replaced by OIDC provisioner in P07). - Full test suite must pass after deletion.
- Remove
- 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:
Flocktightens pre-existing looser perms to 0600 (chmod after open if looser).classifyDialErrswitched from substring to typed errors (use*ssh.ExitError,net.Errortype assertions).- Add SSH-exec rate limiting (token bucket per peer, default 10 req/s).
- 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:
- Per-peer HMAC key (derived from master key via HKDF); deployed to peers at
0600owned byorca. orca-drift-notify.shsigns each event with the HMAC; aggregator rejects unsigned/forged events.- Go tests: forged event rejected; valid event accepted.
- Per-peer HMAC key (derived from master key via HKDF); deployed to peers at
- 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:
- 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
--passwordrejected). - Gates in
.coreci.ymlvalidatepipeline (C-33). - Bats + Go test runner.
- 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
- 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:
docs/threat-model.md: STRIDE per component, zero-trust model, OIDC data-flow diagram, OS surface diagram, residual risk register.docs/oidc.md: configure your IdP, bundled Dex offline quickstart, claim-to-namespace mapping, BYO-IdP override.docs/webauthn.md: passkey registration, RP ID, secure context, recovery flow.docs/security-runbook.md: unseal/seal, master key rotation, incident response, sudoers audit, nft audit, Shamir recovery.- 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:
ciagent-reviewmulti-persona review across all phases.ciagent-auditreconstruction test (git log matches.ciagent/files).- C-32 human-gate: confirm GITEA_TOKEN rotated +
.envre-seeded (escalation hook if pending). - Merge
phase/28->milestone/v0.12-security-hardening. - Merge
milestone/v0.12-security-hardening->main(rebase-then-fast-forward). - Tag
v0.11.28(= v0.12 milestone release per feature-milestone rule). - Create Gitea release with full milestone summary.
- Delete milestone + phase branches (tags preserve history).
- 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