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:
+15
-26
@@ -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"
|
||||
}
|
||||
|
||||
@@ -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.
|
||||
@@ -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 |
|
||||
@@ -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).
|
||||
@@ -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`
|
||||
@@ -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).
|
||||
|
||||
@@ -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).
|
||||
|
||||
@@ -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).
|
||||
@@ -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)
|
||||
|
||||
@@ -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",
|
||||
|
||||
Reference in New Issue
Block a user