add 0030/SESSION-0030.0-TRANSCRIPT-2026-05-28T16-12--2026-05-28T17-39.md + replace placeholder/variant SESSION-0030.0-TRANSCRIPT-2026-05-28T16-12--INPROGRESS.md
This commit is contained in:
@@ -0,0 +1,211 @@
|
|||||||
|
# Session 0030.0 — Transcript
|
||||||
|
|
||||||
|
> Date: 2026-05-28
|
||||||
|
> Start: 2026-05-28T16-12 (PST implied)
|
||||||
|
> End: 2026-05-28T17-39 (PST implied)
|
||||||
|
> Subject: Security-audit-0026 remediation — flotilla-owned fixes + live GCP/VM hardening
|
||||||
|
> Status: **FINALIZED.**
|
||||||
|
|
||||||
|
> Sanitization note: this transcript is published to the public
|
||||||
|
> `wiggleverse/ohm-session-history`. Because several audit findings were
|
||||||
|
> still **live and unpatched** on production at session end (they belong to
|
||||||
|
> a parallel, not-yet-deployed branch — see "Coordination" below), this
|
||||||
|
> record deliberately references findings by **ID and general category
|
||||||
|
> only** and contains **no exploit sketches or step-by-step reproduction**.
|
||||||
|
> The full-detail audit (`SECURITY-AUDIT-0026-2026-05-28.md`) lives locally
|
||||||
|
> in `~/git/ohm-infra`, which is not a git repository, and was never
|
||||||
|
> published. No secret bytes appear here (§3 invariant 1).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Launch prompt
|
||||||
|
|
||||||
|
> full remediation of security report at ~/git/ohm-infra
|
||||||
|
|
||||||
|
The "security report" is `SECURITY-AUDIT-0026-2026-05-28.md` (produced by
|
||||||
|
the parallel audit session 0026): 1 Critical, 1 High, 5 Medium, 9 Low,
|
||||||
|
6 Info findings spanning three surfaces — the **rfc-app** codebase, the
|
||||||
|
**flotilla** CLI (this repo), and the **live GCP/VM** deployment.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## What this session settled
|
||||||
|
|
||||||
|
Session 0030 took the **flotilla-owned findings** and the **live GCP/VM
|
||||||
|
findings that live in no repo**, applied + verified them, and merged +
|
||||||
|
deployed the flotilla changes. The rfc-app/nginx/systemd findings were
|
||||||
|
left to a parallel in-flight session (see Coordination). Net for OHM:
|
||||||
|
the VM is materially hardened and the operator CLI has three defense-in-
|
||||||
|
depth improvements; the application-layer findings remain pending that
|
||||||
|
other session's release.
|
||||||
|
|
||||||
|
## The "did we leak the audit?" scare (resolved: NO)
|
||||||
|
|
||||||
|
Mid-session the operator flagged that a transcript containing the audit
|
||||||
|
might have been pushed to the public repo before an internet drop. I
|
||||||
|
froze remediation and verified the **entire git history** of
|
||||||
|
`wiggleverse/ohm-session-history` (fresh clone, all branches,
|
||||||
|
`git log -S` for audit-unique strings):
|
||||||
|
|
||||||
|
- Audit session 0026 published **only its 454-byte placeholder** — its
|
||||||
|
full transcript was never pushed (the publish push is atomic; it failed
|
||||||
|
on the internet drop, leaving only the start-gesture placeholder).
|
||||||
|
- **Zero** audit-unique strings appear in any commit, ever.
|
||||||
|
- The audit file itself lives only in local `~/git/ohm-infra` (not a git
|
||||||
|
repo) — never committed anywhere.
|
||||||
|
|
||||||
|
Verdict: nothing leaked. The real containment for the audit's value (an
|
||||||
|
exploit roadmap) is shipping the fixes, which is what the session then
|
||||||
|
continued doing.
|
||||||
|
|
||||||
|
## Coordination — the rfc-app remediation is owned elsewhere
|
||||||
|
|
||||||
|
On inspecting `/Users/benstull/git/rfc-app` I found branch
|
||||||
|
`feature/v0.25.0-security-hardening` already checked out with a large
|
||||||
|
**uncommitted** remediation (a parallel live session, 0027 or 0029).
|
||||||
|
It covers the rfc-app application findings, the nginx server block, and
|
||||||
|
the systemd unit. Editing rfc-app would have clobbered another session's
|
||||||
|
live work, so — with the operator — Session 0030 explicitly scoped to:
|
||||||
|
|
||||||
|
- **flotilla repo** fixes (no overlap), and
|
||||||
|
- **live GCP/VM ops** that exist in no repo.
|
||||||
|
|
||||||
|
The operator chose this division (and chose to perform the live ops this
|
||||||
|
session, and to defer SSH-scoping to a proper IAP rollout).
|
||||||
|
|
||||||
|
## Work performed
|
||||||
|
|
||||||
|
### Flotilla repo (PR #1 — merged)
|
||||||
|
|
||||||
|
Branch `session-0030/flotilla-security-hardening` → **PR #1**, merged to
|
||||||
|
`main` (merge commit `7b32a46`). 161 tests pass.
|
||||||
|
|
||||||
|
- **L1** — the live secret redactor is threaded into `_PhaseRunner` and
|
||||||
|
now scrubs **both** the success and failure phase-`detail` paths before
|
||||||
|
they are persisted to `deploys.phases` / surfaced to the operator
|
||||||
|
(previously only the separate `error_summary` was scrubbed). Test
|
||||||
|
extended to assert no secret survives in any persisted phase detail.
|
||||||
|
(This fix was already present in the working tree at session start;
|
||||||
|
Session 0030 verified, tested, and landed it.)
|
||||||
|
- **I6** — `redact_live` now matches several decodings of the same secret
|
||||||
|
bytes (utf-8/replace, surrogateescape, latin-1) so a non-ASCII secret
|
||||||
|
that surfaced via a different decode path than the needle still
|
||||||
|
redacts. Match only ever redacts more, never less. Residual byte-
|
||||||
|
boundary limitation documented honestly (moot for ASCII/UTF-8 secrets).
|
||||||
|
- **L9** — `ssh.py` no longer leans on `--quiet`'s blanket first-connect
|
||||||
|
host-key accept; it adds `StrictHostKeyChecking=accept-new`, which still
|
||||||
|
connects to genuinely-new hosts non-interactively but **refuses a
|
||||||
|
changed key**. Full first-connect TOFU removal remains the §19.2 IAP
|
||||||
|
item.
|
||||||
|
- **I5** — `overlay set` warns (does not block) when a key name carries an
|
||||||
|
unambiguous secret word, nudging toward `secret bind`/`secret set`.
|
||||||
|
Deliberately does not match generic `*_KEY`/`*_API_KEY`, so public site
|
||||||
|
keys don't train the operator to ignore the warning.
|
||||||
|
|
||||||
|
After merge, a clean idempotent redeploy of the current pin (v0.26.0)
|
||||||
|
exercised the hardened flotilla code end-to-end: **`deploys.id=35`**, all
|
||||||
|
9 phases ok, health verified in 5.0s.
|
||||||
|
|
||||||
|
### Live GCP/VM ops (project `wiggleverse-ohm`, VM `ohm-rfc-app`)
|
||||||
|
|
||||||
|
- **M3** — the SQLite data directory was world-traversable and the DB
|
||||||
|
(plus its WAL/SHM sidecars) world-readable. Tightened the **directory to
|
||||||
|
`750`** (the robust containment — closes world access regardless of
|
||||||
|
per-file mode regression) and the files to **`640`**. Verified to
|
||||||
|
survive both a full VM reboot and a deploy-triggered service restart
|
||||||
|
(sqlite preserves the main DB's mode on its sidecars). Also discovered
|
||||||
|
and **secured** (but did not delete) a set of orphaned `ohm-app.db\r*`
|
||||||
|
files — stale copies left by the historical CRLF path bug; flagged to
|
||||||
|
the operator for deletion after confirmation.
|
||||||
|
- **L5** — deleted the dead `default-allow-rdp` firewall rule (3389 is
|
||||||
|
unused surface on Linux). SSH-from-anywhere left in place by operator
|
||||||
|
decision; the right fix is an IAP rollout (§19.2), not fragile IP
|
||||||
|
scoping.
|
||||||
|
- **L6** — created a dedicated least-privilege runtime service account
|
||||||
|
(`ohm-rfc-app-vm@…`) with logging+monitoring writer only (the app reads
|
||||||
|
its config from `.env` at deploy time, so **no** Secret Manager access
|
||||||
|
is needed on the VM), and swapped the VM off the default compute SA.
|
||||||
|
- **L7** — enabled Shielded-VM **Secure Boot** (boot disk confirmed
|
||||||
|
UEFI-compatible first; vTPM + integrity monitoring were already on).
|
||||||
|
|
||||||
|
L6 + L7 were applied in a single VM stop/start window (~15s of downtime),
|
||||||
|
after confirming no deploy was in flight. All four verified intact after
|
||||||
|
the later deploy.
|
||||||
|
|
||||||
|
### Config verification (audit action item 5)
|
||||||
|
|
||||||
|
Read the live `.env` **variable names** only (no values):
|
||||||
|
- `TURNSTILE_REQUIRED="true"` — confirmed (mitigates the OTC abuse Low).
|
||||||
|
- `GITEA_WEBHOOK_SECRET` — present (Gitea webhook authenticated).
|
||||||
|
- **`WEBHOOK_EMAIL_BOUNCE_SECRET` — absent.** This confirms M5 as a live
|
||||||
|
config gap (the bounce webhook is currently unauthenticated). Handed to
|
||||||
|
the operator with an operator-run secret gesture (never request/echo
|
||||||
|
secret bytes in-conversation — §3 conversation-layer corollary). The
|
||||||
|
operator will bind it and redeploy as a follow-up.
|
||||||
|
|
||||||
|
## Friction / dead-ends (kept honest)
|
||||||
|
|
||||||
|
- The first `chmod 640 …/data/*` failed: having just set the directory to
|
||||||
|
`750`, the non-root SSH login user could no longer list it to expand the
|
||||||
|
glob. Re-ran the file chmod under `sudo sh -c` so root expanded it.
|
||||||
|
- After merging PR #1, `git checkout main && git pull` did **not**
|
||||||
|
fast-forward (local `main` had no upstream tracking), so the working
|
||||||
|
tree briefly reverted to pre-fix code. Recovered with
|
||||||
|
`git fetch && git merge --ff-only origin/main` and set the upstream.
|
||||||
|
No work lost (the fixes were safe in the merge commit).
|
||||||
|
|
||||||
|
## Findings status at session end
|
||||||
|
|
||||||
|
| Owner | Findings | Status |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| **0030 (flotilla)** | L1, I6, L9, I5 | **Done** — merged (PR #1) + deployed |
|
||||||
|
| **0030 (live VM/GCP)** | M3, L5(RDP), L6, L7 | **Done** — applied + verified |
|
||||||
|
| **Operator follow-up** | M5 (config bind), `*.db\r` cleanup, systemd `UMask=0077` | Flagged with gestures |
|
||||||
|
| **Deferred** | L5 (SSH 0.0.0.0/0 → IAP §19.2) | By operator decision |
|
||||||
|
| **Parallel v0.25.0 branch** | C1, H1, M1, M2, M4, L2, L3, L4, L8, I1, I3, I4 | **Pending** — uncommitted, **not yet deployed** (still live on prod) |
|
||||||
|
|
||||||
|
## Cross-references
|
||||||
|
|
||||||
|
- flotilla PR #1 (`wiggleverse/ohm-rfc-app-flotilla`), merge `7b32a46`.
|
||||||
|
- OHM deploy `deploys.id=35`, v0.26.0 (idempotent redeploy exercising the
|
||||||
|
hardened flotilla).
|
||||||
|
- GCP: VM `ohm-rfc-app` / `us-central1-a` / `wiggleverse-ohm`; new SA
|
||||||
|
`ohm-rfc-app-vm@wiggleverse-ohm.iam.gserviceaccount.com`; deleted
|
||||||
|
firewall rule `default-allow-rdp`.
|
||||||
|
- Source audit: `~/git/ohm-infra/SECURITY-AUDIT-0026-2026-05-28.md`
|
||||||
|
(local only — never published).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Operating instructions for the next session
|
||||||
|
|
||||||
|
The application-layer security work is the open thread. Concretely:
|
||||||
|
|
||||||
|
1. **Ship v0.25.0.** The `feature/v0.25.0-security-hardening` branch in
|
||||||
|
rfc-app holds the application + nginx + systemd remediation (C1 XSS
|
||||||
|
sanitizer, the auth-verify rate-limit/lockout, the device-trust lookup
|
||||||
|
fix, security headers, cookie hardening, the bounce-webhook
|
||||||
|
`_required` guard, etc.). It is **uncommitted and undeployed**, which
|
||||||
|
means the audit's **Critical (C1)** and other findings are **still live
|
||||||
|
on production**. This is the highest priority: confirm/finish the
|
||||||
|
branch, commit, release as v0.25.0, bump `ohm-rfc/.rfc-app-version`,
|
||||||
|
and deploy via flotilla. Coordinate with whichever session owns that
|
||||||
|
branch (was 0027/0029) before touching it.
|
||||||
|
2. **M5 config** — the operator intends to bind `WEBHOOK_EMAIL_BOUNCE_SECRET`
|
||||||
|
(Secret Manager + `flotilla secret bind` + provider-side config) and
|
||||||
|
redeploy. The code-side `_required` guard rides along in v0.25.0.
|
||||||
|
3. **systemd `UMask=0077`** — add to rfc-app's `deploy/systemd/rfc-app.service`
|
||||||
|
(belt-and-suspenders on top of the `750` data dir).
|
||||||
|
4. **Orphaned `ohm-app.db\r*` files** on the VM — secured to `640`; delete
|
||||||
|
once confirmed orphaned.
|
||||||
|
5. **L5 SSH / IAP (§19.2)** — the deferred item: IAP-only SSH (firewall +
|
||||||
|
tunnel role + flip flotilla's deploy path to `--tunnel-through-iap`),
|
||||||
|
which also retires the L9 first-connect TOFU residue.
|
||||||
|
|
||||||
|
Suggested next-session launch prompt:
|
||||||
|
|
||||||
|
> Ship rfc-app v0.25.0-security-hardening (finish/commit the existing
|
||||||
|
> branch, release, bump the OHM pin, deploy) — this lands the still-live
|
||||||
|
> audit-0026 application findings incl. the Critical XSS. Coordinate with
|
||||||
|
> the session that owns the branch first. Then help the operator bind
|
||||||
|
> WEBHOOK_EMAIL_BOUNCE_SECRET (M5) and add systemd UMask=0077.
|
||||||
@@ -1,15 +0,0 @@
|
|||||||
# Session 0030.0 — Transcript
|
|
||||||
|
|
||||||
> Date: 2026-05-28
|
|
||||||
> Start: 2026-05-28T16-12 (PST implied)
|
|
||||||
> Status: **PLACEHOLDER — claimed at session start; finalized at session end.**
|
|
||||||
>
|
|
||||||
> This file reserves session ID 0030. The driver replaces this body
|
|
||||||
> with the full transcript before publishing, and renames the file to
|
|
||||||
> its final SESSION-0030.0-TRANSCRIPT-2026-05-28T16-12--<end>.md form.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Launch prompt
|
|
||||||
|
|
||||||
_(launch prompt not captured at claim time)_
|
|
||||||
Reference in New Issue
Block a user