add SESSION-M.3-TRANSCRIPT-2026-05-28T05-30--2026-05-28T05-58.md
This commit is contained in:
@@ -0,0 +1,311 @@
|
||||
# Session M.3 — Transcript
|
||||
|
||||
> Parent: SESSION-M-TRANSCRIPT-2026-05-28T05-26--<end>.md
|
||||
>
|
||||
> Date: 2026-05-28 (PST)
|
||||
> Start: 2026-05-28T05-30 PST
|
||||
> End: 2026-05-28T05-58 PST
|
||||
> Goal: Inventory OHM's email + Gitea-webhook posture (roadmap items
|
||||
> #18 + #20, bundled) and draft a hardening runbook plus a
|
||||
> framework-side template-hygiene proposal. Read-only only —
|
||||
> no DNS edits, no SMTP-provider changes, no rfc-app code,
|
||||
> no secret rotations, no test emails.
|
||||
>
|
||||
> Outcome: **Two docs written and left on disk for the driver at
|
||||
> `~/git/ohm-infra/EMAIL-AND-WEBHOOK-HARDENING-RUNBOOK.md` and
|
||||
> `~/git/ohm-infra/RFC-APP-EMAIL-HYGIENE-PROPOSAL.md`. Five §19.2
|
||||
> candidates surfaced. Three high-impact deployment-side gestures
|
||||
> and three high-impact framework-side changes prioritized for the
|
||||
> operator. Two surprising findings: SMTP_HOST is missing from the
|
||||
> live flotilla overlay (gap to disambiguate), and a stale Gitea
|
||||
> webhook on `wiggleverse/meta` still points at the deprovisioned
|
||||
> `http://rfc.wiggleverse.org`.**
|
||||
|
||||
---
|
||||
|
||||
## Pre-session state
|
||||
|
||||
- Parent: Session M driver, dispatched M.3 with scope = inventory
|
||||
for items #18 + #20.
|
||||
- `~/git/ohm-infra/` is a plain directory (not a git repo) — driver
|
||||
noted this; M.3 leaves docs on disk for the driver to handle
|
||||
publishing.
|
||||
- `rfc-app` at v0.17.0 on disk; live OHM running v0.17.0 per
|
||||
`flotilla pin check ohm-rfc-app` (deploys.id=21).
|
||||
- Hard constraints from dispatch: no operator-led gestures, no
|
||||
secret bytes in transcript, read-only inventory only.
|
||||
|
||||
---
|
||||
|
||||
## Turn-by-turn arc
|
||||
|
||||
### Arc 1 — DNS inventory + roadmap read
|
||||
|
||||
Ran `dig +short TXT wiggleverse.org`, `dig +short TXT
|
||||
_dmarc.wiggleverse.org`, `dig +short MX wiggleverse.org`, and
|
||||
probed common DKIM selectors (`default`, `mail`, `s1`, `s2`,
|
||||
`selector1`, `k1`, `mandrill`, `smtp`, `google`, etc.).
|
||||
|
||||
Findings:
|
||||
|
||||
- **SPF:** `"v=spf1 include:_spf.google.com ~all"` (soft-fail).
|
||||
- **DMARC:** `"v=DMARC1; p=none; rua=mailto:ben@wiggleverse.org"`
|
||||
(monitoring-only, no enforcement, no `pct=`, no `sp=`).
|
||||
- **DKIM:** present at `google._domainkey.wiggleverse.org` — a
|
||||
2048-bit RSA Workspace key. All other probed selectors empty.
|
||||
- **MX:** `aspmx.l.google.com` family — Google Workspace mailbox.
|
||||
|
||||
Read roadmap items #18 (l.362-403) and #20 (l.458-547) from
|
||||
`~/projects/wiggleverse/ohm-rfc/ROADMAP.md`. Both confirm they
|
||||
should ship together because the same DNS edits serve both. #20
|
||||
names BIMI as a follow-on after DMARC reaches `p=reject`. #18 names
|
||||
"refresh webhook-shared-secret + replay-resistance + ingress
|
||||
restriction" as framework-side work.
|
||||
|
||||
### Arc 2 — Flotilla overlay + secret inventory
|
||||
|
||||
```
|
||||
.venv/bin/ohm-rfc-app-flotilla overlay show ohm-rfc-app
|
||||
.venv/bin/ohm-rfc-app-flotilla secret list ohm-rfc-app
|
||||
```
|
||||
|
||||
Overlay reveals:
|
||||
|
||||
- `SMTP_PORT=587`, `SMTP_STARTTLS=1`, `EMAIL_FROM=notifications@wiggleverse.org`,
|
||||
`EMAIL_FROM_NAME=Wiggleverse` — present.
|
||||
- `SMTP_HOST` — **missing**. `SMTP_USER` — **missing**.
|
||||
- Secret refs include `GITEA_WEBHOOK_SECRET` bound to
|
||||
`wiggleverse-ohm/ohm-rfc-app-gitea-webhook-secret@latest`.
|
||||
|
||||
Greps:
|
||||
|
||||
- `~/projects/wiggleverse/ohm-rfc-app-flotilla/scripts/register-ohm.sh:40`
|
||||
sets `SMTP_HOST=smtp-relay.gmail.com` — but the live overlay
|
||||
doesn't have it. Either the bootstrap script wasn't re-run after
|
||||
a reshape, or SMTP env lives out-of-band on the VM (e.g.,
|
||||
systemd-unit override). This is a finding for the runbook —
|
||||
operator must disambiguate via `gcloud ssh` + `systemctl show
|
||||
ohm-app.service`.
|
||||
|
||||
Confirmed provider = **Google Workspace SMTP relay** (the bootstrap
|
||||
script + the `google._domainkey` DKIM + the MX records align).
|
||||
|
||||
### Arc 3 — Framework email-helper read
|
||||
|
||||
Read all of:
|
||||
|
||||
- `~/git/rfc-app/backend/app/email.py` — §15.4 notification mailer
|
||||
(`_deliver`, `_send_one`, `_send_bundle`, `make_unsubscribe_url`).
|
||||
- `~/git/rfc-app/backend/app/email_otc.py` — v0.7.0 OTC helper.
|
||||
- `~/git/rfc-app/backend/app/email_invite.py` — v0.17.0 admin-create
|
||||
invite helper.
|
||||
|
||||
Built header-presence matrix across all four kinds (OTC / invite /
|
||||
watcher-single / watcher-bundle):
|
||||
|
||||
- `Date`, `Message-ID`, `Auto-Submitted`, `List-Unsubscribe`,
|
||||
`List-Unsubscribe-Post`, `multipart/alternative` — **all
|
||||
absent** on all four paths. Google's SMTP relay adds `Date` /
|
||||
`Message-ID` during ingestion, but the framework doesn't control
|
||||
the format.
|
||||
- Plain-text bodies only. No HTML variant anywhere.
|
||||
- `_deliver` (`email.py:308`) embeds unsubscribe URL in body footer
|
||||
but doesn't set the header.
|
||||
|
||||
Stdlib spot-check: `python3 -c "from email.message import
|
||||
EmailMessage; …"` confirms `set_content` doesn't auto-populate
|
||||
`Date` or `Message-ID`. Inspected `smtplib.SMTP.send_message`
|
||||
source — also doesn't set them.
|
||||
|
||||
### Arc 4 — Gitea-webhook inventory
|
||||
|
||||
Found the handler at `~/git/rfc-app/backend/app/webhooks.py` (78
|
||||
lines). HMAC-SHA-256 over the raw body, `hmac.compare_digest` for
|
||||
constant-time compare. **But:** `webhooks.py:43` gates verification
|
||||
behind `if config.webhook_secret:` — empty secret = no auth. Per
|
||||
`config.py:75`, `webhook_secret` is `_optional`. Insecure default.
|
||||
|
||||
Pulled `gitea-bot-token` from GCP Secret Manager into a shell var
|
||||
(`GITEA_TOKEN=$(gcloud secrets versions access latest …)`), used
|
||||
it for read-only `curl` calls against the Gitea API, immediately
|
||||
`unset` after each call. Never echoed the value to stdout or any
|
||||
file.
|
||||
|
||||
Hook inventory across the wiggleverse org:
|
||||
|
||||
| Repo | Hook | Notes |
|
||||
|---|---|---|
|
||||
| `wiggleverse/ohm-meta` | `https://ohm.wiggleverse.org/api/webhooks/gitea` | active, 12 events — correct |
|
||||
| `wiggleverse/meta` | `http://rfc.wiggleverse.org/api/webhooks/gitea` | **active, plain HTTP, deprovisioned domain** |
|
||||
| `wiggleverse/rfc-0001-human` | (none) | the only published RFC has no hook |
|
||||
| `ben.stull/ohm-rfc` | (bot 403) | can't inventory, operator self-checks |
|
||||
|
||||
The org-level hooks endpoint
|
||||
(`/api/v1/orgs/wiggleverse/hooks`) returned `[]` — no org-wide
|
||||
hooks, all are repo-level.
|
||||
|
||||
Gitea's API exposes hook `config` keys = `['url', 'content_type']`
|
||||
— it does NOT return the configured `secret` field, so I can't
|
||||
confirm from the API side that the secret matches what OHM has.
|
||||
But OHM's cache is current and webhook receiver verifies, so the
|
||||
secrets must align (otherwise everything would 401).
|
||||
|
||||
### Arc 5 — Bounce-handling + observability gap
|
||||
|
||||
Read `~/git/rfc-app/backend/app/api_notifications.py:471-502` —
|
||||
`/api/webhooks/email-bounce` exists, gated on optional
|
||||
`WEBHOOK_EMAIL_BOUNCE_SECRET` per SPEC.md §3667 Slice 8 settlement.
|
||||
But **no provider is wired to POST here** — Google Workspace SMTP
|
||||
relay doesn't natively webhook bounces (it bounces back to the
|
||||
sender mailbox). So today: hard bounces silently retry every
|
||||
`flush_pending` pass.
|
||||
|
||||
All four send helpers catch `Exception`, log via `log.exception`,
|
||||
return `False`. No DB write of the failure, no admin notification.
|
||||
The operator has zero visibility into send failures without
|
||||
grepping VM logs.
|
||||
|
||||
### Arc 6 — Draft the two docs
|
||||
|
||||
Wrote `EMAIL-AND-WEBHOOK-HARDENING-RUNBOOK.md` (~700 lines) with
|
||||
sections: current posture / hardened shape / operator-execution
|
||||
sequence / framework-vs-deployment split / §19.2 candidates.
|
||||
|
||||
Wrote `RFC-APP-EMAIL-HYGIENE-PROPOSAL.md` (~280 lines) with the
|
||||
framework-side minor proposal: `build_envelope` shared helper,
|
||||
webhook handler tightening (mandatory secret), `outbound_emails`
|
||||
audit table, bounce-correlation, suggested 5-slice shipping plan.
|
||||
|
||||
Cross-referenced both docs at each end. Both name files + line
|
||||
numbers for every framework citation.
|
||||
|
||||
### Arc 7 — Transcript
|
||||
|
||||
This file. ohm-infra is not a git repo (driver's check confirmed
|
||||
pre-session), so no commit; driver decides whether to add a remote
|
||||
or just publish the transcripts via `publish-transcript.sh`.
|
||||
|
||||
---
|
||||
|
||||
## Cut state (end of subsession)
|
||||
|
||||
| Surface | State |
|
||||
|---|---|
|
||||
| `~/git/ohm-infra/EMAIL-AND-WEBHOOK-HARDENING-RUNBOOK.md` | written, ~21 KB |
|
||||
| `~/git/ohm-infra/RFC-APP-EMAIL-HYGIENE-PROPOSAL.md` | written, ~9 KB |
|
||||
| `~/git/ohm-infra/SESSION-M.3-TRANSCRIPT-…md` | this file |
|
||||
| OHM live | unchanged (v0.17.0, deploys.id=21) |
|
||||
| DNS / Gitea / SMTP provider | unchanged (read-only inventory only) |
|
||||
| flotilla overlay / secrets | unchanged (read-only `show` + `list` only) |
|
||||
|
||||
---
|
||||
|
||||
## §19.2 candidates surfaced
|
||||
|
||||
1. **rfc-app: `GITEA_WEBHOOK_SECRET` should be required, not optional.**
|
||||
Today the framework silently accepts unsigned POSTs when the env
|
||||
var is empty. Recommend `_required` with documented
|
||||
`RFC_APP_INSECURE_WEBHOOKS=1` dev-bypass.
|
||||
|
||||
2. **rfc-app: outbound-email observability seam.** Add
|
||||
`outbound_emails` table + write on every send. Today failures
|
||||
log-and-forget; the operator has no audit trail.
|
||||
|
||||
3. **flotilla: `overlay show` doesn't surface "expected but missing"
|
||||
keys.** The SMTP_HOST gap is invisible without grepping
|
||||
register-ohm.sh + framework source side-by-side. A per-deployment
|
||||
"required keys" manifest + `flotilla overlay validate` verb would
|
||||
catch this class of regression.
|
||||
|
||||
4. **flotilla: lacks a webhook-inventory verb.** Listing / diffing
|
||||
the webhooks on the org's Gitea repos against the deployment's
|
||||
expected set is a recurring operator gesture. A `flotilla webhook
|
||||
list / check <deployment>` verb would formalize it. Generic
|
||||
enough for a future `flotilla-core` extraction.
|
||||
|
||||
5. **rfc-app: multiple From-identity references should derive from
|
||||
one source.** `EMAIL_FROM` is read in `email.py:82`,
|
||||
`email_otc.py` via cfg, `email_invite.py` via cfg, plus implied
|
||||
by DMARC alignment + Workspace allowed-senders. A spec section
|
||||
binding all surfaces to one source would prevent skew.
|
||||
|
||||
---
|
||||
|
||||
## What lands on the driver's plate
|
||||
|
||||
1. **Decide where to put the two docs.** `~/git/ohm-infra/` is not
|
||||
a git repo. Options: (a) leave as plain files, publish only via
|
||||
`publish-transcript.sh` (which today only handles transcripts);
|
||||
(b) git-init `~/git/ohm-infra/` and add a Gitea remote so
|
||||
ohm-infra becomes a versioned doc repo; (c) move the two docs
|
||||
into `~/projects/wiggleverse/ohm-rfc/` under a `docs/` subdir
|
||||
since they're OHM-deployment-specific. Driver decides.
|
||||
|
||||
2. **Schedule the disambiguation gesture.** The SMTP_HOST gap is
|
||||
the most urgent finding — if the framework is actually falling
|
||||
to stdout-fallback, OHM's emails (OTC, invites, notifications,
|
||||
beta-access) aren't leaving the VM. Operator runs `gcloud ssh
|
||||
ohm-app` + `systemctl show ohm-app.service | grep
|
||||
Environment` next session.
|
||||
|
||||
3. **Schedule the stale-hook deletion.** Five-second gesture; the
|
||||
hook on `wiggleverse/meta` pointing at deprovisioned
|
||||
`http://rfc.wiggleverse.org/api/webhooks/gitea` should go.
|
||||
|
||||
4. **Decide on the From-identity question.** `ohm@wiggleverse.org`
|
||||
(roadmap's suggestion) or keep `notifications@wiggleverse.org`?
|
||||
Pinning this is a prerequisite for the framework-side proposal's
|
||||
`Reply-To` decision.
|
||||
|
||||
5. **Slot the rfc-app email-hygiene minor.** Per the proposal, v0.18.0
|
||||
or v0.19.0; 5 slices; mostly backward-compatible. Operator
|
||||
decides whether to bundle with another minor (e.g., #19's
|
||||
CONTRIBUTING.md docs) or ship solo.
|
||||
|
||||
6. **Confirm `ben.stull/ohm-rfc`'s hooks.** Bot lacks admin so M.3
|
||||
couldn't inventory. Operator self-checks via Gitea web UI.
|
||||
|
||||
---
|
||||
|
||||
## What the driver needs to know (report-back summary)
|
||||
|
||||
**DNS posture (literal values):**
|
||||
- SPF: `v=spf1 include:_spf.google.com ~all` (soft-fail).
|
||||
- DMARC: `v=DMARC1; p=none; rua=mailto:ben@wiggleverse.org`
|
||||
(monitoring-only).
|
||||
- DKIM: present at `google._domainkey.wiggleverse.org` (2048-bit
|
||||
Workspace key).
|
||||
|
||||
**SMTP provider:** Google Workspace SMTP relay
|
||||
(`smtp-relay.gmail.com`) — inferred from bootstrap script + DKIM
|
||||
+ MX. **But SMTP_HOST is missing from the live flotilla overlay**
|
||||
— either the bootstrap was reshaped and these were dropped, or
|
||||
they're set on the VM out-of-band. Operator must disambiguate.
|
||||
|
||||
**Top 3 deployment-side hardening gestures (priority order):**
|
||||
1. Delete stale `wiggleverse/meta` hook (5 seconds, removes
|
||||
plain-HTTP-to-deprovisioned-domain liability).
|
||||
2. Disambiguate SMTP_HOST gap (5 minutes, confirms email actually
|
||||
leaves the VM).
|
||||
3. DMARC Phase A: add `pct=100; sp=quarantine; adkim=s; aspf=s` to
|
||||
the existing `p=none` record; wait 1 week; flip to `p=quarantine`.
|
||||
|
||||
**Top 3 framework-side changes (rfc-app minor, target v0.18.0):**
|
||||
1. `build_envelope` shared helper adds `Date`, `Message-ID`,
|
||||
`Auto-Submitted`, `List-Unsubscribe`, `List-Unsubscribe-Post`,
|
||||
and `multipart/alternative` to all four email paths.
|
||||
2. Make `GITEA_WEBHOOK_SECRET` required (not silently optional).
|
||||
3. `outbound_emails` audit table + bounce-correlation hook.
|
||||
|
||||
**Webhook posture:** signature verification is correct
|
||||
(`hmac.compare_digest`, HMAC-SHA-256 over raw body) **when the
|
||||
secret is set**. On OHM the secret is bound. Two open issues:
|
||||
(a) framework's silent-accept-when-unset insecure default; (b)
|
||||
stale `wiggleverse/meta` hook on a deprovisioned domain.
|
||||
|
||||
**§19.2 candidates surfaced:** 5 (listed above).
|
||||
|
||||
**Doc paths:**
|
||||
- `/Users/benstull/git/ohm-infra/EMAIL-AND-WEBHOOK-HARDENING-RUNBOOK.md`
|
||||
- `/Users/benstull/git/ohm-infra/RFC-APP-EMAIL-HYGIENE-PROPOSAL.md`
|
||||
- `/Users/benstull/git/ohm-infra/SESSION-M.3-TRANSCRIPT-2026-05-28T05-30--2026-05-28T05-58.md`
|
||||
Reference in New Issue
Block a user