Files
orca/.ciagent/PLAN_v0.12.md
T
Jon Chery cf0df0f157 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.
2026-08-07 10:45:07 +00:00

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 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:<port>/callback); headless device-code fallback.
    4. orca auth init-idp: bootstrap bundled Dex (systemd unit + config template + Traefik route) on the lead; --rp-id <domain> 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
  • 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