tenant email spec: DMARC reports go to cribado.report via CARLOS_DMARC_RUA
Paul's ruling 2026-08-19, superseding the earlier "no rua in v1". The address is configuration, never a constant, so a deployment that sets nothing publishes no rua and degrades to a missing report rather than a broken record. Records RFC 7489 §7.1 external-destination verification as a precondition: cribado.report must publish *._report._dmarc.cribado.report before rua is switched on, or conforming reporters discard every report while the DNS record looks correct.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
1 file changed,
+36
−17
docs/superpowers/specs/2026-08-19-tenant-email-design.md+36 −17
diff --git a/docs/superpowers/specs/2026-08-19-tenant-email-design.md b/docs/superpowers/specs/2026-08-19-tenant-email-design.md| index 268134014..8e214ff46 100644 |
| --- a/docs/superpowers/specs/2026-08-19-tenant-email-design.md |
| +++ b/docs/superpowers/specs/2026-08-19-tenant-email-design.md |
| @@ -296,7 +296,7 @@ action, which is what makes `carlos email enable` a one-command story: |
| | `<token>._domainkey.<host>` ×3 | CNAME | `<token>.dkim.amazonses.com` | |
| | `mail-eu-west-1.<host>` | MX | `10 feedback-smtp.eu-west-1.amazonses.com` | |
| | `mail-eu-west-1.<host>` | TXT | `v=spf1 include:amazonses.com ~all` | |
| -| `_dmarc.<host>` | TXT | `v=DMARC1; p=none; adkim=s; aspf=s` | |
| +| `_dmarc.<host>` | TXT | `v=DMARC1; p=none; adkim=s; aspf=s; rua=mailto:$CARLOS_DMARC_RUA` | |
| **Custom domain** — CARLOS creates the identity and *emits* the same record |
| set; the customer publishes it; CARLOS polls until green. |
| @@ -327,17 +327,37 @@ to change later: for a custom domain it lives in the **customer's** zone, |
| so getting it wrong means asking every customer to republish DNS. It is |
| region-qualified on day one for that reason alone. |
| -**`p=none` and no `rua`.** `p=none` because a tenant whose first send is |
| -rejected by its own DMARC record learns nothing useful. No `rua` because |
| -aggregate reports need a mailbox to arrive at, and inbound mail is out of |
| -scope (§1) — publishing a reporting address CARLOS cannot receive at would |
| -be a lie in DNS, and a lie every receiver on earth would act on. The |
| -consequence is real and worth naming: **without `rua` there are no |
| -aggregate reports, so nothing tells us whether tightening the policy is |
| -safe.** Arranging a reporting destination is therefore a precondition of |
| -the tightening follow-up, not an afterthought to it (§13). A custom |
| -domain's policy is the customer's to set; CARLOS proposes and does not |
| -impose. |
| +**`p=none` to start, with `rua` pointing at an external processor.** |
| +`p=none` because a tenant whose first send is rejected by its own DMARC |
| +record learns nothing useful; aggregate reports are what eventually justify |
| +tightening it (§13). |
| + |
| +Reports go to **cribado.report** (Paul, 2026-08-19). The address is |
| +**configuration, never a constant**: `CARLOS_DMARC_RUA` holds the mailto |
| +target, so the reporting destination can change without a release and a |
| +deployment that sets nothing simply publishes no `rua` — the degradation is |
| +a missing report, not a broken record. |
| + |
| +**External-destination verification is a precondition, and it is the part |
| +that is easy to miss.** RFC 7489 §7.1: because `cribado.report` is a |
| +different organizational domain from the domain being reported on, it must |
| +itself publish |
| + |
| +``` |
| +<reported-domain>._report._dmarc.cribado.report TXT "v=DMARC1" |
| +``` |
| + |
| +or conforming reporters discard the report rather than send it. That record |
| +is keyed by **each reported domain**, so with one identity per app it does |
| +not scale as individual records — `cribado.report` MUST carry the wildcard |
| +form `*._report._dmarc.cribado.report`. **Confirm the wildcard is published |
| +before `rua` is switched on**; until it is, publishing `rua` produces a |
| +DNS record that looks correct and silently yields no reports at all, which |
| +is worse than publishing none. The same wildcard covers custom domains, so |
| +this is confirmed once and never again. |
| + |
| +A custom domain's policy remains the customer's to set; CARLOS proposes |
| +and does not impose. |
| ### The seam |
| @@ -498,11 +518,10 @@ before. |
| - **Confirm the SES region for the estate** before writing the AWS |
| implementation. `eu-west-1` is assumed throughout, matching `objstore`. |
| - **Verify SES tenant attribution over SMTP** against live AWS docs (§3). |
| -- **A DMARC reporting destination, then a tighter policy.** v1 publishes |
| - no `rua` (§7), so there is no evidence on which to tighten `p=none`. |
| - Arranging somewhere aggregate reports can land is the first half of this |
| - follow-up; moving the platform domain to `quarantine` is the second, and |
| - it must not happen first. |
| +- **Tighten the platform domain's DMARC policy past `p=none`** once |
| + cribado.report's aggregate reports show it is safe. Blocked on the |
| + external-destination wildcard in §7 actually being in place — without it |
| + there are no reports, and tightening on no evidence is guessing. |
| - **Tito's estate.** Whether the titogo account gets its own SES estate or |
| reaches carloku's (§2). |
| - **Per-instance credentials.** §1's contract is worded per-credential so |