---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.
13 KiB
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, everyscripts/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 |
| 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 registeropen 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-idpas a systemd unit on the lead, fronted by Traefik (R-017, step-ca cert). orca-webauthn-connectoris a custom Dex connector (~300 LoC Go, usinggithub.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, OIDCsubextraction, 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.issuerin 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'ssub+ a fresh 32-byte salt). The sealed blob (salt || ciphertext) is stored atClusterDir()/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'ssub+ 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 addsvalidateNamespaceName.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----passwordbootstrap (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 certif 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-webauthntest helpers. - Bundled Dex is a new systemd unit + Traefik route -- operational
surface growth. Mitigation:
orca doctor oidcchecks 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-rc1if rotation pending,v0.11.29when 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).