Skip to content

DMARC Report Ingestion

Daily DMARC aggregate (RUA) reports for theaccessible.org are ingested by the dmarc-report Cloudflare Email Worker (workers/dmarc/) instead of landing in larry@’s inbox.

Architecture

Reporter (google.com, yahoo.com, iphmx.com, …)
└─ emails XML report (plain / .gz / .zip attachment)
└─ [email protected] (Cloudflare Email Routing β†’ Worker: dmarc-report)
β”œβ”€ parse attachment β†’ structured JSON β†’ KV (DMARC_KV, 400-day TTL)
β”œβ”€ track known source IPs (KV key: sources)
└─ Resend alert to [email protected] when:
β€’ any record fails DMARC (neither DKIM nor SPF passed), or
β€’ a never-before-seen source IP sends as theaccessible.org

Reports go to theaccessibleorg.com, not theaccessible.org, because theaccessible.org’s MX points at Google Workspace β€” Cloudflare Email Routing cannot run there, so a Cloudflare-routed sibling domain receives the reports.

  • Dashboard: https://dmarc-report.larry-c6c.workers.dev/ (shared-secret login; key in workers/dmarc/.dashboard-key.local, gitignored)
  • JSON API: GET /api/reports?limit=100, GET /api/sources β€” Authorization: Bearer <DASHBOARD_KEY>
  • Health: GET /health (no auth)

KV layout (namespace DMARC_KV, id a68836529ded42fd85ae91eaace6b19f)

KeyValue
report:<zero-padded end ts>:<report id>Parsed DmarcReport JSON (400-day TTL)
sources{ [ip]: { firstSeen, lastSeen, orgs[], messages } }

Keys sort chronologically, so the dashboard lists newest-first without an index. The very first report seeds sources silently (otherwise every IP would alert).

Secrets / vars

  • RESEND_API_KEY (secret) β€” same key as ~/.claude/scripts/send-email.sh
  • DASHBOARD_KEY (secret) β€” dashboard + API auth; local copy in .dashboard-key.local
  • ALERT_EMAIL_FROM / ALERT_EMAIL_TO / PRIMARY_DOMAIN (vars in wrangler.toml)

One-time DNS / routing setup (manual β€” wrangler token lacks zone write)

Status as of 2026-07-11: all three steps DONE and verified (routing tested end-to-end with a live test email; authorization TXT resolves).

  1. βœ… theaccessibleorg.com β†’ Email β†’ Email Routing β†’ Routing rules: custom address [email protected] β†’ action Send to Worker β†’ dmarc-report.
  2. βœ… theaccessibleorg.com β†’ DNS: TXT record β€” name theaccessible.org._report._dmarc (full name theaccessible.org._report._dmarc.theaccessibleorg.com), content v=DMARC1. RFC 7489 Β§7.1 external-destination authorization; without it compliant reporters (Google, Yahoo, Microsoft) verify the record, find nothing, and silently refuse to send theaccessible.org reports to a theaccessibleorg.com address.
  3. βœ… theaccessible.org β†’ DNS: _dmarc TXT record set to
    v=DMARC1; p=quarantine; rua=mailto:[email protected]; pct=100; adkim=s; aspf=r; fo=1

Reporters pick up rua/authorization changes within ~24h (they re-query DNS per report run). Verify step 2 with: dig +short TXT theaccessible.org._report._dmarc.theaccessibleorg.com

Failure behavior

  • Unparseable inbound mail is not bounced; the worker emails an ingest-error alert instead (so a broken reporter can’t silently disappear).
  • A failed Resend call after a successful parse logs but does not bounce β€” the report is already stored.

Deploy / test

Terminal window
cd workers/dmarc
npm test # vitest: parser, zip/gzip decoding, alert logic
npm run typecheck
npm run deploy # wrangler deploy

Backfill/manual insert: build [{key, value}] JSON and npx wrangler kv bulk put file.json --namespace-id a68836529ded42fd85ae91eaace6b19f.