Backfills transcripts that existed only locally in ohm-infra: - 0019.1/.2/.3 - UX-polish wave subagent transcripts (driver 0019.0 never finalized) - 0026.1-.9 - security-audit-0026 subagent transcripts (driver abandoned/closed-out by 0068; audit drove published 0030 remediation) - 0073.0 - PPE/progressive-delivery + engineering-handbook session, originally drafted as a duplicate 0046.0; reassigned next free number (0072 taken by a concurrent session) sessions.json: add 0019/0073 titles, update 0026 title. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
6.6 KiB
SESSION-0026.5 — flotilla operator-CLI security audit (READ-ONLY subsession)
- Parent: SESSION-0026.0-TRANSCRIPT-2026-05-28T13-46--INPROGRESS.md
- Role: subsession 0026.5 of OHM security-audit driver session 0026
- Surface:
/Users/benstull/projects/wiggleverse/ohm-rfc-app-flotilla(the flotilla operator CLI) - Scope: READ-ONLY. No edits, no deploys, no secret rotation, no git mutation. Only file written = this transcript.
- Focus: §3 invariant 1 ("no secret bytes in the build pipeline / state / log /
stdout"), overlay-vs-secret separation, SSH command safety,
secret setstdin-only.
Pre-state
Branch main, clean except untracked .claude/. Audited the canonical tree under
ohm_rfc_app_flotilla/ (NOT the .claude/worktrees/ copies, which a naive find
surfaced first — pivoted to the real package dir). Read SPEC.md §1–§13 + CLAUDE.md.
Turn-by-turn arc
- Read SPEC.md (§3 invariants, §6 overlay, §7 secrets, §8 deploy gesture, §10 log) and the binding CLAUDE.md "never ask for secret bytes" / two-layer rules.
- Listed canonical source; read secrets.py, ssh.py, deploy.py in one batch.
- Read deploy_log.py, db.py, registry.py, plan.py, pin.py.
- Grepped cli.py for stdin/tty/secret/overlay output paths; read the overlay +
secret CLI verbs in full; read
secret set(TTY guard + stdin). - Checked migrations for any
secret_valuecolumn (none — confirmed) and skimmed test_secrets.py + test_deploy.py for hardcoded/echoed real secrets (none; all stub bytes likeb"NEVER-LEAK-..."). - Adversarial pass on candidate findings:
- KILLED "snapshot hash leaks overlay values" — hash is sha256, one-way; values not recoverable. Not a leak.
- KILLED "SSH command injection via target_version" — pin.py validates strict
SemVer regex
^\d+\.\d+\.\d+$before it becomesv<ver>; all VM coords and vite values go throughshlex.quote. No injection. - KILLED "secret could be set as overlay" as a code bug — downgraded to informational: overlay/secret are distinct tables + distinct CLI paths; the non-secret-overlay boundary is operator discipline per CLAUDE.md two-layer rule, and there's no automated key-name guard, but that's a documented design boundary, not a defect. Noted F3.
- CONFIRMED the phase-detail redaction gap (F1) by tracing append_phase →
persisted
phases[].detailvs. the later error_summary-only redaction. - Held F2 (gcloud
--quiethost-key TOFU) as needs-verification.
Cut state — findings
-
F1 (MEDIUM, high confidence): Per-phase
detailis persisted to thedeploys.phasesJSON column UNREDACTED. deploy.py_PhaseRunner.runwritesdetail=e.detail(e.g. phase-3f"git fetch/checkout failed: {r.stderr...[:240]}") viadeploy_log.append_phaseat the moment of failure. Theredact_livescrub runs LATER and ONLY on the separateerror_summarystring (_finalize_failure). A secret that surfaces in a command's stderr lands unredacted in flotilla state. Violates §3 invariant 1 / §10.2. The existing test (test_deploy.py:248) checks onlyoutcome.error_summary, never the persistedphases[].detail— so the gap is untested. Remediation: redact phase detail atappend_phasetime (pass the live_ResolvedSecretsinto_PhaseRunner), or redact in_phaseNbefore raising_PhaseFailure. -
F2 (LOW, needs-verification): ssh.py invokes
gcloud compute ssh ... --quiet.--quietauto-accepts prompts including first-connect host-key confirmation (TOFU silently accepted). gcloud still managesgoogle_compute_known_hostsand pulls host keys from instance metadata when present, so steady-state MITM exposure is limited; residual risk is first-connect. Verify exact gcloud host-key behavior before treating as actionable. Remediation if confirmed: rely on GCP guest-attr host keys / IAP (the §19.2 "harden SSH via IAP" candidate already tracks this). -
F3 (INFORMATIONAL): No code guard prevents an operator from placing a sensitive value into the non-secret overlay (e.g. as a
VITE_*key, which deploy.py phase-5 interpolates into the build command AND bundle-embeds). This is the documented two-layer boundary (operator discipline), not a defect — flagged so the driver knows the enforcement is human, not mechanical. -
F4 (LOW, edge):
redact()/redact_live()match needles viabytes.decode("utf-8", errors="replace"). A secret containing non-UTF-8 bytes would decode to a different string than what landed in (replace-decoded) stderr, so redaction could miss it. OHM secrets are random ASCII/UTF-8 tokens, so low real risk; noted for completeness.
What's solid (controls correctly enforcing §3 invariant 1)
- No
secret_valuecolumn anywhere; migration 002secret_refsholds only project/id/version refs. DB stores references, not bytes. secret setis stdin-only with an explicitsys.stdin.isatty()TTY refusal (cli.py) — matches CLAUDE.md exactly; bytes never enter shell history/args.read_secretreturns bytes that the deploy/plan paths use transiently;_ResolvedSecretsholds them in bytearrays scrubbed-to-zero on scope exit (incl. exception unwind), env_body bytearray zeroed in afinally..envwritten atomically over SSH stdin (never local disk, never a command arg):umask 077+cat > tmp+mvassudo -u <user>, mode 0600, owner = service user. Verified env body travels viainput=(stdin), not the command string (test_deploy.py:166 asserts bytes are in stdin, not in any recorded command).- Snapshot hash and plan render use ONLY ref strings, never bytes.
- error_summary redaction path IS applied (the F1 gap is the phase detail, a separate persisted field).
- All SSH-interpolated values
shlex.quoted; pin is strict-SemVer-validated. secret list/overlay showrender[secret: <project>/<id>@<version>], never resolved bytes.- Tests use only stub bytes (
b"NEVER-LEAK-...",b"abc123"); no real secret hardcoded or echoed.
Driver needs to know
- F1 is the one worth fixing: it's a real (if narrow — requires a secret to appear in remote stderr) §3-invariant-1 leak into flotilla state, and it's untested.
- F2 ties into the already-tracked §19.2 "harden SSH via IAP" item.
§19.2 candidates surfaced
- Mechanical overlay-key denylist / secret-key collision guard (relates F3) — make the two-layer rule enforced, not just disciplined.
- Redaction hardening: redact at every state-write boundary (phase detail included), not only at finalize (relates F1, F4).
No secret bytes appear anywhere in this transcript.