Your DMARC Failure Count Is Wrong
TLDR
- Took two production domains from
p=noneto enforcement - one all the way top=reject- across roughly 30 sending services, without losing legitimate mail - The dashboard failure counts were wrong the whole time. Deduplicating by Message-ID cut the "failing" number by more than half - most of it was the same email counted twice
- Our own remediation drove the SPF record to 9 of the 10 DNS lookups the RFC allows. One more include would have broken authentication for the entire domain
- A vendor on a shared IP pool told us they could not do DKIM. A one-field change to a bounce address made their mail pass anyway
- Spoofing volume dropped 41% within a week of raising enforcement to 50%
- Everything reusable is open source: dmarc-provenance
30 Sending services | 9/10 SPF lookups used | 41% Spoof volume drop | 0 Legit mail lost
The problem
A domain that has existed for twenty years sends email from places nobody remembers. Marketing platforms, billing vendors, report schedulers, scan-to-email copiers, a survey tool someone bought in 2019. Without DMARC enforcement, anyone on the internet can also send as that domain, and receivers have no instruction to stop them.
Turning on enforcement is a two-word DNS change. The actual work is proving that every legitimate sender authenticates before you flip it, because the first thing enforcement breaks is whatever you forgot existed.
The toolstack
| Tool | Role |
|---|---|
Microsoft Defender Advanced Hunting (EmailEvents) | Per-message auth verdicts for mail that touches your tenant, 30-day window |
| Aggregate DMARC reports (rua) | The world's receivers' view - the only lens for mail that never touches your tenant |
| Exchange message trace | 90-day lookback when 30 days is not enough |
| DNS + Terraform | Where every fix lands |
The mental model that matters: your tenant is per-message ground truth for mail that reaches you. Aggregate reports are the only way to see everything else. You need both, and they disagree constantly.
The query that matters
Defender logs one row per copy of an email. Relayed and forwarded copies fail authentication even when the original passed, because the relay host is not in the sender's SPF record and the message got modified in transit.
Count rows and a healthy sender looks broken. I made this exact mistake twice - declared an outage, traced the messages, and found every "lost" email sitting in the recipient's inbox, delivered seconds before its failing echo was logged.
The fix is grouping by Message-ID and only counting emails where no copy passed anywhere:
EmailEvents
| where Timestamp > ago(30d)
| where EmailDirection == "Inbound"
| where SenderFromDomain endswith "yourdomain.com"
| extend dmarc = tostring(parse_json(AuthenticationDetails).DMARC)
| summarize anypass = max(iff(dmarc =~ "pass", 1, 0)),
anyfail = max(iff(dmarc =~ "fail", 1, 0)),
env = min(SenderMailFromDomain)
by InternetMessageId
| where anyfail > 0 and anypass == 0
| summarize failures = count() by env
| order by failures desc
On our data this cut the failure count by more than half. The repo has a standalone version (dedupe.py) that runs against any CSV export.
Naming the sender behind the platform
Reports tell you "SendGrid is failing." That is useless when three different vendors send through SendGrid and you have a contract with none of them.
The account is in the metadata. VERP bounce addresses embed an account ID (bounces+1234567-...). Received headers sometimes carry it base64-encoded. Click-tracking hostnames and custom reverse DNS are per-customer. Fingerprinting this way revealed one vendor quietly running two accounts on the same platform - one authenticated years ago, one never - which explained why a fix that was "already done" kept failing.
The vendor who could not do DKIM
One platform runs customers on a shared IP pool. Shared pool means shared DKIM - they physically cannot sign as your domain. Their support said so, correctly, and the conversation stalled for a week.
DMARC does not need both mechanisms. It needs one, aligned. We pointed the vendor's bounce address at a subdomain we control, published their SPF there, and alignment carried the mail: 0% to 99.7% passing with a single configuration field. No DKIM involved.
Caveat that matters later: SPF alignment dies on forwarding. Fine at quarantine, risky at reject. Know which policy you are under before you rely on it.
The lookup budget nobody watches
SPF allows ten DNS lookups per evaluation. Past ten, receivers get a permanent error and treat it as failure - for the whole domain, every message.
Here is the trap: we did this to ourselves. Every include in that record was one of my own fixes - a survey vendor here, a payroll platform there, each individually correct, each merged through review. Nobody tracks the running total, because no single change looks dangerous. When I finally counted, we were at nine of ten. The next routine vendor onboarding, done the usual way with an include:, would have silently broken authentication for everything at once.
The repo's spf_lookups.py walks the include tree and tells you your number in ten seconds. Run it before every SPF change, not after. The policy that follows: new senders get DKIM, not another include.
The ramp
p=quarantine with a pct= sampling rate, stepped 5 → 25 → 50 → 100 over four weeks, each step gated on one question: is anything legitimate in the deduplicated failing bucket?
The percentage only affects mail that already fails, so raising it cannot break a passing sender. What the slow ramp buys is time for aggregate reports - which lag a day or two - to surface senders your tenant never sees. Ours surfaced several, including a billing system that only ever emailed external recipients.
What enforcement is actually for
At the end, what remained failing was not vendors. In one week we counted spoofed mail against 505 different employee names - fake voicemails, fake DocuSign requests, wire transfer confirmations, invoice fraud aimed at the payment approval chain. All of it now quarantined everywhere, not just inside our walls. A week after the 50% step, campaign volume dropped 41%. Attackers measure deliverability too.
Know what DMARC does not stop: lookalike domains and display-name impersonation pass their own authentication perfectly. Pair enforcement with impersonation protection and out-of-band verification for payment changes, or the most expensive attacks walk straight past your new policy.
What I would do differently
- Deduplicate from day one. Build it into the first query, not after the second false alarm
- Inventory subdomains at the start. They inherit nothing and attackers find them first
- Audit allow rules before enforcing. Ours had exception lists that silently masked failures for months
- Ask every vendor for their account identifier up front, not "are you set up"
- Write a removal criterion into every exception you create, or they become permanent
Resources
The deduplicator, the SPF budget tool, the hunting queries, and the full methodology are on GitHub: github.com/erv718/dmarc-provenance