Files
orca/.ciagent/RESEARCH_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

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, 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
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).