Files
orca/.ciagent/GRILL_v0.12.md
T
Jon Chery cf0df0f157 docs(P00): v0.12 security-hardening phase 0 (specify/clarify/research/ideate/plan/grill)
---ci---
project: orca
phase: 0
milestone: v0.12
status: specify
---/ci---

Threat-model review of entire surface incl OS (25 findings F1..F25).
Adopts R-021 (no Orca credentials: human=OIDC, machine=mTLS/SPIFFE).
Bundled Dex + WebAuthn (passkeys) as default password-free authenticator.
Master key seal-to-OIDC + Shamir 3-of-5 recovery.
30 net-new requirements (REQ-119..REQ-148). 29 phases. Binding conditions C-29..C-38.
2026-08-07 10:45:07 +00:00

171 lines
9.3 KiB
Markdown

# 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 |