Files
session-history/ohm/0026/SESSION-0026.6-TRANSCRIPT-2026-05-28T13-46--2026-05-28T13-52.md
Ben Stull 9e6756f678 ohm: migrate orphaned subagent transcripts (0019.x, 0026.x) + renumber dup 0046.0 -> 0073
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>
2026-06-05 03:28:59 -07:00

73 lines
11 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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). `.env` was 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 unit `ohm-rfc-app.service` active (restarted ~1m41s before sampling — i.e. 0022's deploy had just landed); legacy `ohm-app.service` no longer present.
## Turn-by-turn arc
1. 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-level `0027` placeholder (`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.)
2. Read `ssh.py` (gcloud compute ssh wrapper, no keys held, OS Login) + `registry.py` (deployment schema). `flotilla deployment show ohm-rfc-app` gave VM name/zone/user/install dir/unit.
3. `gcloud compute instances list` + `describe`: default compute SA, default GCE scope set (NO cloud-platform), `enable-oslogin` instance metadata set, Shielded (vTPM + integrity ON, **Secure Boot OFF**), tags http-server/https-server.
4. `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.)
5. `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.
6. `gcloud secrets list`: 7 `ohm-rfc-app-*` named secrets + 4 hash-named (likely Certbot/other). Per-secret `get-iam-policy` for 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).
7. `gcloud compute addresses list`: static IP `ohm-app-ip` 136.116.40.66 IN_USE; USERS self-link still references `.../instances/ohm-app` (stale label after rename; IP correctly attached). Cosmetic.
8. 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`.
9. SSH batch #2 (`systemctl show` effective props, service user, app/.env/DB perms, `ss -tlnp`): see findings. `.env` 600 owner-only (solid); SQLite DB **644 world-readable**; CapabilityBoundingSet = full default set; several Protect*/Restrict*/SystemCallFilter unset.
10. SSH batch #3 (`nginx -T` greps, 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 security `add_header` except Cache-Control. server_tokens commented out.
11. 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`.
12. 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).
13. 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.db` mode `644` owner `ohm-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-rename `ohm-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-Control` on 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.1` leaked; `server_tokens off;` is commented out. Remediation: enable `server_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-ip` address USERS self-link references old `ohm-app` instance; DB file `ohm-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
- `.env` 600 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 `0027` INPROGRESS 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 check` verb could codify the systemd-unit + nginx-header + DB-perms assertions as a post-deploy audit (generic across deployments). The hardening expectations (F1F4, F8) are deployment-shape-agnostic and would live in flotilla-core, with per-deployment exceptions in §13.