From 8f5e131572fa1afde78b25233b71f2e1e7b3af84 Mon Sep 17 00:00:00 2001 From: Viktor Barzin Date: Sat, 18 Apr 2026 23:49:14 +0000 Subject: [PATCH] [mailserver] Route DMARC rua/ruf to dmarc@viktorbarzin.me [ci skip] MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Context Mailgun was decommissioned on 2026-04-12 in favour of Brevo as the outbound SMTP relay. The DMARC aggregate (`rua`) and forensic (`ruf`) report targets still pointed at `e21c0ff8@dmarc.mailgun.org`, an inbox that no longer exists — meaning every DMARC report Google/Microsoft/etc. generate has been bouncing or silently dropped for six days. No alerts fire on this (DMARC reports are best-effort, not RFC-mandated), but we've lost visibility into alignment failures and spoofing attempts during the exact window where the SPF/DKIM/DMARC posture was being reshaped for the Brevo cutover. Decision (2026-04-18): route reports to `mailto:dmarc@viktorbarzin.me`. The mailserver's catch-all sieve delivers anything to non-existent local-parts into `spam@`, so `dmarc@` does not need to be provisioned as a real mailbox — the inbox will land in `spam@`'s maildir unchanged. Alternative considered: route to a dedicated `dmarc@` maildir with sieve rules to file into a folder. Rejected for now — the monitoring value of DMARC reports is low-frequency (one aggregate per reporter per day at most), so the catch-all path is good enough until volume justifies a proper parser. Can be revisited once we see actual report traffic. The third-party aggregator target `adb84997@inbox.ondmarc.com` (Red Sift OnDMARC) is preserved in both rua and ruf — it provides parsed dashboards that we actually read. The `postmaster@viktorbarzin.me` ruf-only target also stays as a local mirror. As a side effect, this apply also canonicalises the TXT record: the previous value was stored as a two-string split in Cloudflare state (`...viktorbarzin" ".me;"`) due to the 255-byte TXT string limit (the record length exceeded 255 chars). The new value is shorter (dmarc@viktorbarzin.me is 21 chars vs e21c0ff8@dmarc.mailgun.org's 26 chars, doubled across rua and ruf) and fits in a single string, so the provider serialises it as one string and the prior split-drift noise disappears from future plans. ## This change Single-line content edit on `cloudflare_record.mail_dmarc` in `stacks/cloudflared/modules/cloudflared/cloudflare.tf`: Before → After (rua and ruf, both): ``` mailto:e21c0ff8@dmarc.mailgun.org → mailto:dmarc@viktorbarzin.me ``` All other DMARC tags unchanged: `v=DMARC1`, `p=quarantine`, `pct=100`, `fo=1`, `ri=3600`, `sp=quarantine`, `adkim=r`, `aspf=r`. Delivery flow: ``` DMARC reporter (Gmail/Outlook/...) │ aggregate XML.gz to rua / forensic to ruf ▼ dmarc@viktorbarzin.me │ mailserver catch-all (no local recipient) ▼ spam@viktorbarzin.me (Viki's mailbox) ``` ## What is NOT in this change - **Mailbox sieve rules** to file DMARC reports into a dedicated folder (separate concern; deferred until traffic justifies it). - **DMARC parser / dashboard**. OnDMARC (adb84997@inbox.ondmarc.com) already provides this for aggregate reports. - **Policy tightening** (`p=reject`, `pct` ramp) — out of scope. - **SPF / DKIM records** — not touched. - **Removal of the split-string drift suppression**, if any existed in prior work. The canonicalisation happens naturally on this apply; no separate workaround was needed. ## Test Plan ### Automated Targeted terragrunt plan + apply via `scripts/tg`: ``` $ cd stacks/cloudflared && scripts/tg plan \ -target=module.cloudflared.cloudflare_record.mail_dmarc ... Terraform will perform the following actions: # module.cloudflared.cloudflare_record.mail_dmarc will be updated in-place ~ resource "cloudflare_record" "mail_dmarc" { ~ content = "\"v=DMARC1; ... rua=mailto:e21c0ff8@dmarc.mailgun.org, mailto:adb84997@inbox.ondmarc.com; ... ruf=mailto:e21c0ff8@dmarc.mailgun.org, mailto:adb84997@inbox.ondmarc.com, mailto:postmaster@viktorbarzin\" \".me;\"" -> "\"v=DMARC1; ... rua=mailto:dmarc@viktorbarzin.me, mailto:adb84997@inbox.ondmarc.com; ... ruf=mailto:dmarc@viktorbarzin.me, mailto:adb84997@inbox.ondmarc.com, mailto:postmaster@viktorbarzin.me;\"" } Plan: 0 to add, 1 to change, 0 to destroy. $ scripts/tg apply /tmp/dmarc.tfplan module.cloudflared.cloudflare_record.mail_dmarc: Modifying... module.cloudflared.cloudflare_record.mail_dmarc: Modifications complete after 1s Apply complete! Resources: 0 added, 1 changed, 0 destroyed. ``` Authoritative DNS post-apply: ``` $ dig TXT _dmarc.viktorbarzin.me @evan.ns.cloudflare.com +short "v=DMARC1; p=quarantine; pct=100; fo=1; ri=3600; sp=quarantine; adkim=r; aspf=r; rua=mailto:dmarc@viktorbarzin.me,mailto:adb84997@inbox.ondmarc.com; ruf=mailto:dmarc@viktorbarzin.me,mailto:adb84997@inbox.ondmarc.com,mailto:postmaster@viktorbarzin.me;" ``` Note: `dig @1.1.1.1` still served the old value immediately after apply — Cloudflare's public resolver holds its cache until TTL expires (TTL=1/auto ≈ 5 min). Authoritative NS is the source of truth. ### Manual Verification **Setup**: none (DNS change only). **Commands**: ``` # 1. Confirm authoritative DNS (run now, should pass) dig TXT _dmarc.viktorbarzin.me @evan.ns.cloudflare.com +short # Expected: rua=mailto:dmarc@viktorbarzin.me,... and ruf similarly. # 2. Confirm public resolver catches up (run after ~5min) dig TXT _dmarc.viktorbarzin.me @1.1.1.1 +short # Expected: same as above (no more mailgun.org entries). # 3. Within 24-48h, check Viki's spam@ inbox for an incoming DMARC # aggregate report from Google/Microsoft/etc. Reports are # typically .zip or .gz attachments with XML inside. ``` **Interpretation**: seeing a DMARC report land in spam@ proves the end-to-end delivery path works: reporter DNS lookup → _dmarc.viktorbarzin.me → mailto:dmarc@viktorbarzin.me → catch-all → spam@ maildir. ## Reproduce locally ``` 1. git pull 2. cd stacks/cloudflared 3. dig TXT _dmarc.viktorbarzin.me @evan.ns.cloudflare.com +short 4. Expected: rua=mailto:dmarc@viktorbarzin.me (and ruf the same). ``` Closes: code-569 Co-Authored-By: Claude Opus 4.7 (1M context) --- stacks/cloudflared/modules/cloudflared/cloudflare.tf | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/stacks/cloudflared/modules/cloudflared/cloudflare.tf b/stacks/cloudflared/modules/cloudflared/cloudflare.tf index 92c5e08d..80b0fe8d 100644 --- a/stacks/cloudflared/modules/cloudflared/cloudflare.tf +++ b/stacks/cloudflared/modules/cloudflared/cloudflare.tf @@ -206,7 +206,7 @@ resource "cloudflare_record" "mail_tlsrpt" { } resource "cloudflare_record" "mail_dmarc" { - content = "\"v=DMARC1; p=quarantine; pct=100; fo=1; ri=3600; sp=quarantine; adkim=r; aspf=r; rua=mailto:e21c0ff8@dmarc.mailgun.org,mailto:adb84997@inbox.ondmarc.com; ruf=mailto:e21c0ff8@dmarc.mailgun.org,mailto:adb84997@inbox.ondmarc.com,mailto:postmaster@viktorbarzin.me;\"" + content = "\"v=DMARC1; p=quarantine; pct=100; fo=1; ri=3600; sp=quarantine; adkim=r; aspf=r; rua=mailto:dmarc@viktorbarzin.me,mailto:adb84997@inbox.ondmarc.com; ruf=mailto:dmarc@viktorbarzin.me,mailto:adb84997@inbox.ondmarc.com,mailto:postmaster@viktorbarzin.me;\"" name = "_dmarc.viktorbarzin.me" proxied = false ttl = 1