notspoofed.com SPF · DKIM · DMARC

SPF passed but DKIM failed

By · Published

You opened a delivered message, looked at the source, and found this:

Find your selector. The DKIM selector finder tests the selectors every major provider publishes for a domain, follows CNAME delegation to whoever holds the key, and reports key size, testing mode and revoked keys.

Authentication-Results: mx.google.com;
       dkim=fail header.i=@example.com header.s=s1 header.b=Ht9kQ2mR;
       spf=pass (google.com: domain of bounce@example.com designates
         198.51.100.20 as permitted sender) smtp.mailfrom=bounce@example.com;
       dmarc=pass (p=NONE sp=NONE dis=NONE) header.from=example.com

One passed, one failed, and the natural reading — “the two of them disagree, so one of them is lying” — is wrong. SPF and DKIM are not two opinions about the same question. They check different things, and a pass from one carries no information at all about the other.

What each result actually proves

SPF checked the connection. It took the envelope sender — the MAIL FROM address in the SMTP conversation, which your recipient never sees — and asked whether the IP that connected was listed in that domain’s SPF record. A pass means one thing: this machine was allowed to send for that envelope domain. It says nothing about who wrote the message, nothing about whether the content arrived intact, and nothing about the address in the From: header.

DKIM checked the message. The signer hashed the body, hashed a named list of header fields, and signed the result with a private key. A verifier fetches the public key from DNS and recomputes both hashes. A failure means the bytes it hashed are not the bytes that were signed, or that it could not get a usable key.

So spf=pass with dkim=fail is not a contradiction. It is the completely ordinary situation where the message travelled by an authorised path and then something changed it — or where your key is not where the verifier looked.

Why DMARC can still pass

DMARC does not require both. RFC 9989, the 2026 revision that replaced RFC 7489, puts it plainly: if one or more of the authenticated identifiers aligns with the Author Domain, the message passes DMARC.

The Author Domain is the domain in the From: header — the one your recipient actually reads. That is the whole point of the mechanism. SPF authenticates the envelope sender, DKIM authenticates the d= value of the signature, and DMARC’s job is to insist that at least one of those matches the visible one.

Alignment has two modes, set in your DMARC record:

That gives you two very different situations wearing the same dkim=fail:

What you see What it means
spf=pass aligned, dkim=fail, dmarc=pass Delivery is fine today. You are running on one leg, and it is the leg that breaks when anyone forwards your mail.
spf=pass for a vendor domain, dkim=fail, dmarc=fail Nothing authenticated your From domain. This is being rejected at p=reject.

If yours is the second row, the SPF half of the problem is alignment rather than DKIM — why DMARC fails when SPF passes covers that half in full. The rest of this page is about the DKIM half, which you need fixed either way.

Reading which identifier failed

Authentication-Results is defined by RFC 8601, and the useful part is that each method reports its own identifier. For DKIM those are header.d (the signing domain), header.s (the selector), header.i (the signing identity) and header.b (the first bytes of the signature, which lets you tell two signatures apart). For SPF it is smtp.mailfrom.

Microsoft writes the same information differently:

authentication-results: spf=pass (sender IP is 198.51.100.20)
 smtp.mailfrom=example.com; dkim=fail (body hash did not verify)
 header.d=example.com; dmarc=pass action=none header.from=example.com;
 compauth=pass reason=100

The parenthetical is where the diagnosis lives, and the exact wording varies by receiver. The distinctions that matter:

Result What the verifier is telling you
dkim=fail (body hash did not verify) The key was fine. The body changed after signing.
dkim=fail (signature did not verify) The key was fine. A signed header changed.
dkim=permerror It could not use your key: no such selector, malformed record, or revoked.
dkim=temperror DNS did not answer. Retryable, not your configuration.
dkim=none There was no signature to check at all.
dkim=neutral The verifier declined to reach a verdict — Gmail uses this for a bad body hash.

Two more things worth noticing before you go hunting. A message can carry more than one DKIM-Signature, and a receiver reports a result for each — your ESP’s signature plus your own is normal, and one pass alongside one fail is fine as long as the aligned one passed. And you must read this from a message you received, at a mailbox on another provider. A copy from your Sent folder has no Authentication-Results header at all, because that verdict is written by the receiving server.

Paste the block into the header analyzer if you would rather not do the comparison by eye — it pulls out d=, s= and the alignment verdict. It runs in your browser, so nothing is uploaded.

The causes, in the order they are worth checking

Something rewrote the message in transit

This is the common one, and the tell is body hash did not verify. DKIM signs the body byte for byte; the relaxed canonicalisation defined in RFC 6376 forgives trailing whitespace and collapses runs of spaces, and that is the entire allowance. Anything else breaks it:

If the appliance is yours and sits after signing, either move signing to the last hop outbound or stop it modifying the body.

A mailing list or a forwarder

The same effect, deliberately. Lists add a [listname] subject tag, append an unsubscribe footer, and insert List-Id and friends. Any of those breaks the signature if it touches something the signature covers.

Header additions break it for a reason people find surprising: RFC 6376 lets a signer name a header field in h= even when that field does not exist, treating it as an empty string. Signers do this deliberately — it is called over-signing — precisely so that adding one later invalidates the signature. If your ESP over-signs Sender: or List-Unsubscribe: and a list adds one, DKIM fails and nothing is misconfigured.

There is no fix for this on your end, because it is not your bug. Modern lists mitigate it with ARC, which records the authentication verdict at each hop so a downstream receiver can see the message passed before the list touched it. ARC is Experimental, and the large receivers honour it from senders they already trust. What you can do is make sure the message was signed correctly when it left you — check a direct delivery, not the list copy.

The selector is not in DNS, or was rotated out

dkim=permerror with wording about a missing key. RFC 6376 is specific here: if the key record does not exist, the verifier must immediately return PERMFAIL. Look at the s= value in the failing signature and query it:

dig +short TXT s1._domainkey.example.com

Nothing back means the signer is signing with a selector you never published, or one you published and later deleted, or a CNAME to the provider that no longer resolves. Providers rotate keys and expect the CNAME to follow. See finding your DKIM selector if you do not know which selectors you should have.

The key is revoked

A record that exists but reads v=DKIM1; p= is a revoked key — RFC 6376 requires a PERMFAIL, and it makes no distinction between a revoked key and a deleted one. This happens when a rotation was half-finished, or when a wildcard *._domainkey record with an empty p= is answering for a selector you forgot to publish explicitly.

The record is there but unusable

Worth ruling out while you have dig open:

A body length limit

If your signatures carry an l= tag, only that many octets of the body are signed and everything past it is unverified. RFC 6376 explicitly warns that verifiers may treat a message with content beyond the signed length with suspicion and fail it as unsigned content. If you are setting l= to survive footer appliances, stop — you are trading a working signature for one receivers are entitled to reject.

What to do next

Send one message from the affected system to a mailbox at a different provider — directly, not via a list — and read the Authentication-Results in the delivered copy.

If it says permerror, the fix is DNS: publish the selector the s= tag names, or repair the CNAME, and re-test in a few minutes.

If it says body hash did not verify on a direct send, an appliance in your own outbound path is modifying signed mail; move the signing step after it.

If a direct send passes and only list traffic fails, your DKIM is correct and you should leave it alone — publish p=quarantine rather than p=reject while you watch the aggregate reports, so list mail is not destroyed while ARC does its work.

Run the domain through the checker to confirm which selectors resolve before you change anything else.