SPF, DKIM and DMARC: Diagnosing Why Legitimate Mail Is Being Rejected
SPF, DKIM and DMARC fail in distinct and identifiable ways, and the header of a rejected message usually names which. This guide covers reading those headers, the alignment rule that catches everyone out, and the limits that silently break a correct-looking SPF record.
Read the Authentication-Results header first
Every receiving system that performs these checks records the outcome in a header. It is the fastest route to a diagnosis and it removes all guesswork about which mechanism failed.
Authentication-Results: mx.google.com;
spf=fail (google.com: domain of bounce@marketing.example.com does not
designate 198.51.100.7 as permitted sender) smtp.mailfrom=marketing.example.com;
dkim=pass header.i=@example.com header.s=selector1;
dmarc=fail (p=REJECT sp=REJECT dis=REJECT) header.from=example.com
Three separate verdicts, and they can disagree. Here SPF failed, DKIM passed, and DMARC still failed — which seems contradictory until alignment is understood.
In Microsoft 365, the equivalent summary appears in Authentication-Results plus a composite header:
X-Forefront-Antispam-Report: CIP:198.51.100.7;CTRY:US;SFV:SPM; SCL:5;SRV:;IPV:NLI;H:mail.example.com;DIR:INB;
Obtain the full headers before theorising: in Outlook, File → Properties → Internet headers; in Gmail, Show original.
SPF: the ten-lookup limit breaks correct records
SPF publishes which hosts may send for a domain, and it validates the envelope sender (MAIL FROM), not the visible From: address. This distinction is the root of much confusion.
dig +short TXT example.com | grep spf # "v=spf1 include:spf.protection.outlook.com include:_spf.google.com # include:sendgrid.net include:mail.zendesk.com ~all"
Each include, a, mx, ptr and exists consumes DNS lookups, and the specification caps the total at ten. Crucially, includes nest: include:spf.protection.outlook.com may itself perform several. Exceed ten and the result is permerror, which most receivers treat as a failure — the record looks perfect and does not work.
# count the real total, including nested includes dig +short TXT spf.protection.outlook.com dig +short TXT _spf.google.com
Remedies, in order of preference: remove services no longer used; move sources onto subdomains with their own records (mail.example.com); use SPF flattening only with automation, since flattened records break silently when a provider changes its IP ranges.
The qualifier at the end sets the policy for everything not listed:
| Qualifier | Result | Typical handling |
|---|---|---|
-all | fail (hard) | Reject |
~all | softfail | Accept, mark suspicious |
?all | neutral | No opinion — effectively no protection |
+all | pass everything | Never use; authorises the entire internet |
Publish exactly one SPF record. Two TXT records beginning v=spf1 produce permerror under the specification — a frequent outcome when a second mail provider is onboarded by a different team.
DKIM: selectors, key length and body hash
DKIM signs headers and body with a private key; the receiver fetches the public key from DNS at <selector>._domainkey.<domain>. The selector is named in the signature itself:
DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=example.com; s=selector1; h=from:to:subject:date; bh=47DEQpj8HBSa...; b=Xk3p...
dig +short TXT selector1._domainkey.example.com # "v=DKIM1; k=rsa; p=MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8A..."
Distinguish the two hashes, because they fail for different reasons:
- bh= is the body hash. A
bodyhash did not verifyerror means the body changed in transit — a mailing list footer, a scanning gateway rewriting links, or a disclaimer appliance. - b= is the signature over the headers. Failure here means a signed header was altered or the key does not match.
Use relaxed canonicalisation (c=relaxed/relaxed) unless you have a specific reason not to; strict canonicalisation fails on trivial whitespace changes that intermediate systems make routinely.
In Microsoft 365, DKIM uses two selectors with CNAMEs so keys can be rotated without a DNS change:
dig +short CNAME selector1._domainkey.example.com dig +short CNAME selector2._domainkey.example.com # selector1-example-com._domainkey.tenant.onmicrosoft.com
Get-DkimSigningConfig -Identity example.com | Format-List Rotate-DkimSigningConfig -Identity example.com -KeySize 2048
Use 2048-bit keys. 1024-bit keys are still accepted but are increasingly treated as weak, and some receivers now discount them.
DMARC alignment — the rule that explains the contradictions
DMARC does not simply ask whether SPF or DKIM passed. It asks whether either passed for a domain that matches the From: header domain. That is alignment, and it is where otherwise-correct configurations fail.
dig +short TXT _dmarc.example.com # "v=DMARC1; p=reject; rua=mailto:dmarc@example.com; pct=100; adkim=s; aspf=s"
| Tag | Meaning |
|---|---|
p= | Policy: none, quarantine or reject |
sp= | Policy for subdomains |
adkim= | DKIM alignment: r relaxed (default) or s strict |
aspf= | SPF alignment: r relaxed or s strict |
pct= | Percentage of mail the policy applies to |
rua= | Address for aggregate XML reports |
Relaxed alignment accepts an organisational-domain match: mail.example.com aligns with example.com. Strict alignment requires an exact match. Setting aspf=s while a bulk provider sends with an envelope domain of bounce.provider.net guarantees DMARC failure however valid the SPF record is.
Work through the earlier example with alignment in mind:
- SPF checked
marketing.example.com(envelope) — failed. - DKIM passed for
d=example.com. - From: header is
example.com, so DKIM is aligned and DMARC should pass.
If DMARC nonetheless reports failure, the usual cause is that the DKIM signature broke in transit while the header still records the attempt — check for bodyhash did not verify rather than assuming the policy is wrong.
Forwarding and mailing lists break SPF by design
When a message is forwarded, the forwarding server becomes the sender. SPF then evaluates that server against the original domain's record and fails — correctly, by the specification's own logic. Nothing is misconfigured.
This is the principal reason to rely on DKIM for DMARC alignment: the signature travels with the message and survives forwarding, provided the body is not altered.
- Simple forwarding — SPF fails, DKIM survives, DMARC passes on DKIM alignment.
- Mailing lists — subject prefixes and footers are added, breaking the body hash, so DKIM fails too. Well-behaved lists rewrite the From: header to their own domain to avoid this.
- Sender Rewriting Scheme (SRS) — rewrites the envelope sender so SPF passes at the next hop; supported by Microsoft 365 for mail forwarded out of the service.
Before moving to p=reject, confirm from aggregate reports that legitimate forwarded mail still aligns on DKIM. Deploying reject while list traffic depends on SPF is the classic way to lose real mail.
Deploy DMARC in stages
Never publish p=reject first. The aggregate reports are the point of the exercise — they reveal legitimate senders nobody remembered.
- Monitor.
v=DMARC1; p=none; rua=mailto:dmarc@example.com. Collect for at least two to four weeks. - Read the reports. They are XML; use a parser or a reporting service. Identify every source sending as your domain.
- Fix or remove each source. Add legitimate ones to SPF, enable DKIM signing, decommission the rest.
- Quarantine gradually.
p=quarantine; pct=10, then raise the percentage as reports stay clean. - Reject.
p=reject; pct=100, withsp=rejectso subdomains are covered.
# publish a null MX and DMARC reject on domains that never send mail dig +short MX parked.example.com # 0 . dig +short TXT _dmarc.parked.example.com # "v=DMARC1; p=reject;"
Non-sending domains are a favourite target for spoofing precisely because they are forgotten. A reject policy and a null MX cost nothing and remove the opportunity.
Checklist
- Read
Authentication-Resultsbefore changing any DNS record. - Count SPF DNS lookups including nested includes; over ten is
permerror. - Confirm exactly one
v=spf1TXT record exists. - Check the DKIM selector named in the signature actually resolves.
- Distinguish
bh=failures (body altered) fromb=failures (headers or key). - Verify DMARC alignment, not just pass — compare the From: domain with the SPF and DKIM domains.
- Expect SPF to fail on forwarded mail; rely on DKIM alignment there.
- Move through none → quarantine → reject on evidence from aggregate reports.
pass and DMARC still fails. Watch the ten-lookup SPF limit, and prefer DKIM, which survives forwarding.