cf0df0f157
---ci--- project: orca phase: 0 milestone: v0.12 status: specify ---/ci--- Threat-model review of entire surface incl OS (25 findings F1..F25). Adopts R-021 (no Orca credentials: human=OIDC, machine=mTLS/SPIFFE). Bundled Dex + WebAuthn (passkeys) as default password-free authenticator. Master key seal-to-OIDC + Shamir 3-of-5 recovery. 30 net-new requirements (REQ-119..REQ-148). 29 phases. Binding conditions C-29..C-38.
163 lines
6.5 KiB
Markdown
163 lines
6.5 KiB
Markdown
# 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.
|