9e6756f678
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>
11 KiB
11 KiB
SESSION-0026.6 TRANSCRIPT — OHM security audit subsession (systemd / nginx / TLS / GCP IAM / firewall / secrets)
- Subsession: 0026.6 of OHM security-audit driver session 0026
- Parent transcript: SESSION-0026.0-TRANSCRIPT-2026-05-28T13-46--INPROGRESS.md
- Start: 2026-05-28T13-46 (America/Los_Angeles) End: 2026-05-28T13-52
- Mode: READ-ONLY audit of LIVE infrastructure (project
wiggleverse-ohm, host ohm.wiggleverse.org) - Mandate surface: systemd unit hardening, service user, file perms, nginx/TLS/headers, GCP IAM + firewall + secrets + VM SA/scopes + static IP, session-history secret-byte spot-check.
- No secret bytes appear anywhere in this transcript (§3 invariant 1 / conversation corollary).
.envwas read for variable NAMES + perms/owner only; values never echoed.
Pre-state
- gcloud authed (active accounts incl. ben.stull@wiggleverse.org), project
wiggleverse-ohm. - Coordination: sessions 0022.0 and 0026.0 marked ACTIVE/INPROGRESS in ohm-session-history. 0022.0 was mid VM-rename (ohm-app -> ohm-rfc-app) + deploy. I observed only; did not interfere.
- VM rename observed COMPLETE by sample time: instance
ohm-rfc-app(us-central1-a, RUNNING, NAT 136.116.40.66); systemd unitohm-rfc-app.serviceactive (restarted ~1m41s before sampling — i.e. 0022's deploy had just landed); legacyohm-app.serviceno longer present.
Turn-by-turn arc
- Claimed a session ID via
claim-session-id.sh --start 2026-05-28T13-46. NOTE/ARTIFACT: as a subsession (0026.6) I should not have run the top-level claim — it computed + PUSHED a top-level0027placeholder (SESSION-0027.0-...--INPROGRESS.md) to the shared repo. This is a stray placeholder that needs retraction/finalization by the driver. (Read-only-infra mandate honored; this is the session-history repo, not infra. I did not push further changes to retract it to avoid racing 0022.0/0026.0.) - Read
ssh.py(gcloud compute ssh wrapper, no keys held, OS Login) +registry.py(deployment schema).flotilla deployment show ohm-rfc-appgave VM name/zone/user/install dir/unit. gcloud compute instances list+describe: default compute SA, default GCE scope set (NO cloud-platform),enable-oslogininstance metadata set, Shielded (vTPM + integrity ON, Secure Boot OFF), tags http-server/https-server.gcloud projects get-iam-policy: only ben.stull = owner + secretmanager.admin + secretAccessor; service agents only otherwise. No Editor sprawl, no default-SA project role, no SA user-managed keys, no extra humans. (Refuted the standard "default SA has Editor" finding — it has no project role here.)gcloud compute firewall-rules list: default-allow-ssh (22) and default-allow-rdp (3389) open to 0.0.0.0/0; http/https to world (expected); internal 10.128.0.0/9.gcloud secrets list: 7ohm-rfc-app-*named secrets + 4 hash-named (likely Certbot/other). Per-secretget-iam-policyfor several = EMPTY (no per-secret bindings) -> secret access is project-IAM-only (ben.stull). VM default SA has NO secretAccessor (consistent with flotilla deploy-time resolution via operator ADC).gcloud compute addresses list: static IPohm-app-ip136.116.40.66 IN_USE; USERS self-link still references.../instances/ohm-app(stale label after rename; IP correctly attached). Cosmetic.- SSH read-only batch #1 (systemctl cat/status, unit file perms): unit hardening baseline present; backend
uvicorn --host 127.0.0.1 --port 8000 --proxy-headers --forwarded-allow-ips 127.0.0.1. - SSH batch #2 (
systemctl showeffective props, service user, app/.env/DB perms,ss -tlnp): see findings..env600 owner-only (solid); SQLite DB 644 world-readable; CapabilityBoundingSet = full default set; several Protect*/Restrict*/SystemCallFilter unset. - SSH batch #3 (
nginx -Tgreps, site files): proxy_pass localhost, client_max_body_size 4M, certbot TLS1.2/1.3 + modern ciphers in server block; nginx.conf http-block line 34 lists TLSv1/1.1 (latent). No securityadd_headerexcept Cache-Control. server_tokens commented out. - Local TLS/header checks (openssl s_client, curl -sI): LE E7 ECDSA cert valid 2026-05-26..08-24, HTTP->HTTPS 301. Adversarially verified TLS1.0 and TLS1.1 are REJECTED (alert 70), TLS1.2 OK -> KILLED the "TLS1.0/1.1 enabled" finding as a live exposure (server-block override wins over http-block line 34; line 34 downgraded to latent/informational). Confirmed NO security headers (no HSTS/CSP/X-Frame-Options/nosniff/Referrer-Policy) on / and /api/health. Server version banner leaks
nginx/1.22.1. - SSH batch #4 (sshd -T, nginx 443 default, unattended-upgrades, fail2ban): sshd permitrootlogin no / passwordauth no / pubkey-only -> heavily mitigates world-open :22 (DOWNGRADED that finding). unattended-upgrades enabled. fail2ban inactive (low value given no password auth).
- Cloned ohm-session-history (30 transcripts), grepped for sk-ant/AKIA/private-key/ghp_/xox + secret-name=longvalue assignments: only
FILL_FROM_*placeholders matched -> session-history secret-byte spot-check CLEAN.
Cut state — findings
(severity / confidence after self-refutation)
- F1 — SQLite DB world-readable.
/opt/ohm-rfc-app/backend/data/ohm-app.dbmode644ownerohm-rfc-app. Any local user/process can read all app data (users, OAuth identities, sessions, content). Remediation:chmod 640(or 600) the DB + ensure umask in deploy. MEDIUM, HIGH conf. (mitigated: single-tenant host, few local users.) Filename still pre-renameohm-app.db— cosmetic. - F2 — No HTTP security headers. No HSTS, CSP, X-Frame-Options, X-Content-Type-Options, Referrer-Policy on ohm.wiggleverse.org (verified via curl on / and /api/health; nginx has only
add_header Cache-Controlon static). Clickjacking + SSL-strip-on-first-visit + MIME-sniff exposure for an app doing OAuth/auth. Remediation: add HSTS (with preload once confident), X-Frame-Options DENY / CSP frame-ancestors, nosniff, Referrer-Policy in the ohm server block. MEDIUM(-HIGH), HIGH conf. - F3 — systemd CapabilityBoundingSet not restricted. Effective bounding set = full default (cap_sys_admin, cap_sys_module, cap_bpf, cap_sys_ptrace, etc.). Unit sets no
CapabilityBoundingSet=. Real impact LOW (process runs non-root uid 999 + NoNewPrivileges=true, so no effective caps), but violates least-privilege. Remediation:CapabilityBoundingSet=(empty) — service binds 8000 so needs none. LOW-MEDIUM, HIGH conf. - F4 — systemd deeper sandbox knobs unset. PrivateDevices=no, ProtectKernelTunables=no, ProtectKernelModules=no, ProtectControlGroups=no, RestrictAddressFamilies unset, SystemCallFilter unset, LockPersonality=no, MemoryDenyWriteExecute=no, RestrictNamespaces=no. (Unit comment self-describes as "modest defaults.") Defense-in-depth gaps. Remediation: add the above (RestrictAddressFamilies=AF_INET AF_INET6 AF_UNIX; SystemCallFilter=@system-service; etc.). LOW, HIGH conf.
- F5 — Firewall: SSH(22) + RDP(3389) open to 0.0.0.0/0. DOWNGRADED: SSH is well-mitigated (OS Login enabled instance-level + sshd passwordauth=no, permitrootlogin=no, pubkey-only) so it requires a valid GCP IAM identity; remaining risk is brute-force noise / SSH 0-day surface. RDP rule is pure dead surface on a Linux host (nothing listens on 3389). Remediation: delete default-allow-rdp; restrict :22 to IAP range (35.235.240.0/20) or known admin IPs, prefer --tunnel-through-iap. LOW (SSH) / LOW (RDP), HIGH conf.
- F6 — VM uses default compute service account.
727327350842-compute@developer.gserviceaccount.com. Mitigated: it holds NO project IAM role and NO secretAccessor, and scopes are the default set (no cloud-platform). Best practice is a dedicated minimal SA, but blast radius today is small. LOW, HIGH conf. - F7 — Shielded VM Secure Boot OFF. vTPM + integrity monitoring ON, Secure Boot OFF. Remediation: enable Secure Boot (requires stop/start; coordinate). LOW, MED conf.
- F8 — nginx version banner disclosure.
Server: nginx/1.22.1leaked;server_tokens off;is commented out. Remediation: enableserver_tokens off. LOW, HIGH conf. - F9 — nginx.conf http-block lists TLSv1/TLSv1.1 (line 34). LATENT only — overridden by the certbot server-block (TLS1.0/1.1 verified rejected live). Risk: a future vhost without the certbot include would inherit weak protocols. Remediation: fix the http-block default to TLSv1.2 TLSv1.3. INFORMATIONAL/LOW, HIGH conf.
- F10 — Static IP / DB stale naming.
ohm-app-ipaddress USERS self-link references oldohm-appinstance; DB fileohm-app.db. Cosmetic post-rename drift, no security impact. INFO.
Killed / refuted
- "TLS 1.0/1.1 enabled" — refuted: live handshake rejects both (alert 70).
- "Default SA has project Editor" — refuted: default SA has no project-level role at all.
- "Backend :8000 exposed" — refuted: binds 127.0.0.1 only; firewall has no :8000 rule.
- "World-open SSH is high" — downgraded: OS Login + pubkey-only + no root login.
- "Secrets readable by VM/others" — refuted: per-secret IAM empty, project-IAM grants accessor only to ben.stull.
What's solid
.env600 owner-only; service runs as dedicated nologin uid 999 with no extra groups, owns only its app dir.- Backend bound to 127.0.0.1:8000, nginx proxy_pass localhost, --forwarded-allow-ips 127.0.0.1, client_max_body_size 4M.
- TLS: LE ECDSA cert valid, TLS1.2/1.3 only (verified), modern ciphers, session tickets off, HTTP->HTTPS 301.
- sshd: pubkey-only, no root login, no password/empty-password auth; OS Login on.
- IAM minimal (single owner, no SA keys, no editor sprawl); VM SA has no project role + no secretAccessor; default (not cloud-platform) scopes.
- unattended-upgrades enabled.
- ohm-session-history spot-check: no secret bytes across 30 transcripts (only FILL_FROM_* placeholders).
- systemd baseline hardening present (NoNewPrivileges, ProtectSystem=strict, ProtectHome, PrivateTmp, ReadWritePaths scoped to data dir).
Driver-needs-to-know
- Stray
0027INPROGRESS placeholder was pushed to ohm-session-history by my claim-script call (I should not have claimed as a subsession). Driver should retract/finalize it. - Top remediations by value: F1 (chmod DB 640) and F2 (security headers) first; both low-effort, real defense-in-depth wins. Then F3/F4 (systemd caps + sandbox) as a single unit edit, F5 (drop RDP rule, restrict/IAP SSH).
- 0022's deploy had just landed at sample time; everything observed is post-rename steady state. No deploy was in progress during my SSH samples; no locks touched.
§19.2 candidates (flotilla-core extraction)
- A
flotilla harden checkverb could codify the systemd-unit + nginx-header + DB-perms assertions as a post-deploy audit (generic across deployments). The hardening expectations (F1–F4, F8) are deployment-shape-agnostic and would live in flotilla-core, with per-deployment exceptions in §13.