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.
This commit is contained in:
Jon Chery
2026-08-07 10:45:07 +00:00
parent da1f93ea77
commit 9bc3462cab
10 changed files with 1457 additions and 27 deletions
+15 -26
View File
@@ -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"
}
+162
View File
@@ -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://<cluster>/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:<port>/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 <domain>`. 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.
+170
View File
@@ -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 |
+183
View File
@@ -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).
+395
View File
@@ -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:<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`
#### 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`
+96
View File
@@ -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).
+55
View File
@@ -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).
+231
View File
@@ -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).
+149
View File
@@ -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)
+1 -1
View File
@@ -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",