Files
session-history/ohm/0026/SESSION-0026.8-TRANSCRIPT-2026-05-28T13-46--2026-05-28T14-09.md
T
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

139 lines
6.5 KiB
Markdown
Raw 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.8 — v0.25.0 security hardening config edits
Parent: SESSION-0026.0-TRANSCRIPT-2026-05-28T13-46--INPROGRESS.md
Subsession 0026.8 of OHM session 0026 (REMEDIATION mode). Scope was
narrow and bounded: make exactly two in-repo config edits in the
`rfc-app` repo (branch `feature/v0.25.0-security-hardening`) to close
audit findings M2, L8 (nginx) and L4 (systemd). No git commit, no
push, no VM access, no other files touched. The driver commits and
applies to the VM later.
## What I changed and why
### TASK 1 — nginx security headers (M2 + L8)
File: `deploy/nginx/ohm.wiggleverse.org.conf`
Added to the server block (which is the certbot-promoted HTTPS block on
the VM — see below): `server_tokens off;` (L8) plus five `add_header
... always;` response headers (HSTS, X-Frame-Options DENY,
X-Content-Type-Options nosniff, Referrer-Policy
strict-origin-when-cross-origin, and a Content-Security-Policy) (M2).
All carry `always` so they apply to nginx error responses too.
Important structural note I verified before editing: the in-repo
`.conf` is the **pre-certbot template** and contains only the port-80
`server` block. Per the file's own install comment, RUNBOOK.md, and
DEPLOY-NEW-SESSION-PROMPT.md, `certbot --nginx` rewrites this file on
the VM, promoting this single server block to `listen 443 ssl` and
adding a separate port-80 → 443 redirect block. So headers placed in
this block ride into the HTTPS server block on the VM exactly as the
task intended. I documented that relationship in an inline comment so a
future reader isn't confused about why "the HTTPS headers" live in what
looks like the port-80 block in-repo.
Did NOT touch the existing TLS/proxy/`Cache-Control`/`client_max_body_size`
directives.
### TASK 2 — systemd hardening (L4)
File: `deploy/systemd/rfc-app.service`
Kept the existing NoNewPrivileges / ProtectSystem=strict / ProtectHome
/ PrivateTmp / scoped ReadWritePaths. Added the full defense-in-depth
block under a `# v0.25.0 security hardening (audit 0026 L4)` comment:
empty `CapabilityBoundingSet=` + `AmbientCapabilities=` (service binds
127.0.0.1:8000, needs no caps), PrivateDevices, ProtectKernel{Tunables,
Modules,Logs}, ProtectControlGroups, RestrictAddressFamilies (AF_INET
AF_INET6 AF_UNIX), RestrictNamespaces, LockPersonality,
MemoryDenyWriteExecute, RestrictRealtime, RestrictSUIDSGID,
SystemCallFilter=@system-service, SystemCallErrorNumber=EPERM,
SystemCallArchitectures=native, UMask=0077.
## The CSP I settled on + reasoning
```
default-src 'self';
script-src 'self' https://challenges.cloudflare.com https://*.amplitude.com;
style-src 'self' 'unsafe-inline';
img-src 'self' data: https:;
font-src 'self' data:;
connect-src 'self' https://*.amplitude.com https://challenges.cloudflare.com;
worker-src 'self' blob:;
frame-src https://challenges.cloudflare.com;
frame-ancestors 'none';
base-uri 'self';
object-src 'none'
```
Diffs from the suggested starting policy, all evidence-driven from
reading the frontend:
- **Added `connect-src https://challenges.cloudflare.com`.** Turnstile
(TurnstileWidget.jsx loads `https://challenges.cloudflare.com/turnstile/v0/api.js`)
makes XHR/fetch calls back to that origin during the challenge, not
just an iframe. Without it the challenge can fail.
- **Added `worker-src 'self' blob:`.** This is the load-bearing change.
Amplitude analytics is `@amplitude/unified`, which ships **Session
Replay**, and analytics.js initializes it at **`sampleRate: 1`** (100%
of consenting sessions recorded — see lib/analytics.js lines 219222).
Amplitude Session Replay spins up a Web Worker from a `blob:` URL for
DOM capture/compression. With a strict `default-src 'self'` and no
`worker-src`, `blob:` workers are blocked and session replay breaks
for every consenting user. I could not introspect node_modules to
prove the exact blob usage (deps not installed in this checkout), so
I relied on documented Amplitude Session Replay behavior and added
`blob:` defensively — it's low-risk and prevents a real regression.
- **script-src / connect-src amplitude:** Amplitude AND mermaid are
both **bundled** (`import('@amplitude/unified')`, `import('mermaid')`
— dynamic imports from node_modules, served from `'self'`), so
script-src for them isn't strictly required. I kept
`script-src ... https://*.amplitude.com` defensively (harmless) but
the truly necessary amplitude grant is `connect-src https://*.amplitude.com`
(event ingestion + session replay upload, all under `*.amplitude.com`).
mermaid needs no external origin at all — it renders inline SVG.
- **`style-src 'unsafe-inline'` retained and documented as required:**
the JSX uses inline `style={...}` attributes throughout and mermaid
injects `<style>` at render time. Tightening to nonces/hashes is a
build-side follow-up. **`script-src` deliberately omits
`'unsafe-inline'`** — no inline `<script>` in the app (index.html has
only `<script type="module" src="/src/main.jsx">`), so inline-script
XSS stays blocked.
- `img-src 'self' data: https:` and `font-src 'self' data:` keep
markdown/RFC remote images and data: URIs (and svg/mermaid data:
output) working.
## MemoryDenyWriteExecute judgment call
I included `MemoryDenyWriteExecute=true` **active, not commented out**.
The stack is stock CPython + FastAPI/uvicorn + sqlite3 + bcrypt + httpx
— no JIT, no PyPy, nothing that mmaps W+X pages. MDWX is safe for that
profile. The risk is theoretical (a C-extension doing W+X mmap), so I
judged the security win worth keeping it on. I added an inline comment
flagging it as the single line to comment out if uvicorn fails to come
up on restart. Driver action: **watch the first `systemctl restart
rfc-app` journal** — if uvicorn crashes on boot, comment out the one
`MemoryDenyWriteExecute=true` line and `daemon-reload` + restart;
everything else in the block is restart-safe for this stack.
## Verify
Re-read both files after editing.
- nginx: brace-balanced (1 `server {` + 4 `location {` = 5 opens, 5
closes). Could not run `nginx -t` (no live nginx) — visual sanity
check only; CSP is a single quoted `add_header` value, well-formed.
- systemd: valid `key=value` ini throughout; the two empty-value
capability clears (`CapabilityBoundingSet=`, `AmbientCapabilities=`)
are intentional and valid systemd syntax.
No secret bytes touched or printed. No commit/push/VM action taken.
## Cut state
Both edits complete and verified in-repo on
`feature/v0.25.0-security-hardening`. Handed back to driver for commit
+ VM apply. Open driver watch-item: MDWX on first restart (above).