notspoofed.com SPF · DKIM · DMARC

DKIM passed but SPF failed

By · Published

This is the mirror image of the usual complaint, and it worries people more than it should:

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=pass header.i=@example.com header.s=s1 header.b=Ht9kQ2mR;
       spf=fail (google.com: domain of ceo@example.com does not designate
         203.0.113.44 as permitted sender) smtp.mailfrom=ceo@example.com;
       dmarc=pass (p=REJECT sp=REJECT dis=NONE) header.from=example.com

SPF failed. DMARC passed anyway. The message was delivered. Before you go editing DNS, work out whether anything is actually wrong — for a large share of the people who search this, nothing is.

What each result actually proves

DKIM proved the message. A verifier fetched your public key from DNS, recomputed the hash of the body and of the signed headers, and got a match. That is a cryptographic statement about content: this message left example.com intact and has not been altered in any way the signature covers.

SPF proved nothing here, and that is a narrower failure than it sounds. SPF asks a single question — was the connecting IP listed in the SPF record of the envelope sender domain? A fail means this particular IP, at this particular hop, was not authorised. It is a statement about a path, not about a message. A message that changed hands twice gets checked against the second hop, and your record has no idea that hop exists.

The consequence people miss: SPF results are not portable. Your DKIM signature is still valid three forwards later. Your SPF result was decided at the first receiver and is worthless to anyone downstream.

Why DMARC passed

Because it only needs one. RFC 9989 — the 2026 revision that replaced RFC 7489 — says that if one or more of the authenticated identifiers aligns with the Author Domain, the message passes the DMARC check.

The Author Domain is the domain in the From: header. That is the identity a human reads, and it is the only one DMARC cares about matching. Alignment comes in two modes:

So dkim=pass header.d=example.com with header.from=example.com is an aligned pass, and DMARC is satisfied. The SPF result is not consulted further. This is exactly the design intent: DKIM is the mechanism that survives relaying, and SPF is the one that does not.

One detail that catches people out on bounces and out-of-office replies: RFC 9989 relies solely on the MAIL FROM identity for SPF. A null reverse-path — which RFC 7208 turns into postmaster@ the HELO name — can produce a perfectly good SPF pass that DMARC cannot use, because it aligns with nothing. On those messages DKIM is not the better option, it is the only one.

Reading which identifier failed, and how

Every SPF result is a different diagnosis. The values come from RFC 8601, and treating them as interchangeable is what sends people down the wrong path:

Result What it means Your record is
spf=fail Record read, IP not authorised, record ends -all Working
spf=softfail Same, but the record ends ~all Working
spf=neutral The record ends ?all, or has no all at all Saying nothing
spf=none No SPF record exists for the envelope domain Missing
spf=permerror The record could not be evaluated Broken
spf=temperror DNS did not answer in time Fine; transient

fail and softfail mean SPF worked and gave a verdict. Only permerror and none mean you have something to fix in DNS. See ~all versus -all for why the first two matter less than the arguments about them suggest.

Read the smtp.mailfrom value next, because it is frequently not the domain you assumed:

spf=fail smtp.mailfrom=bounces@sendingplatform.net

That is a different domain’s record failing, not yours. And read the Received: chain from the bottom up — if there is a hop between your server and the final receiver, you are looking at forwarded mail and the SPF failure is structural. Some receivers also stamp a Received-SPF: header at the hop where they made the decision, which tells you exactly which IP and which identity were checked.

Use a message you received at a mailbox on another provider. A copy from your Sent folder carries no Authentication-Results, because the receiving server writes it. Paste the block into the header analyzer to have the comparison done for you — it runs in your browser and uploads nothing.

The causes, in the order they are worth checking

Forwarding, which is not a fault

A .forward rule, a university alumni address, a role account that redirects, a mailing list. The forwarder keeps your envelope sender and connects from its own IP. Your record does not list it, so SPF fails. RFC 7208 describes this directly in its discussion of mediators, and offers no fix that lives in your record — because there isn’t one.

Some forwarders implement SRS, rewriting the envelope sender into their own domain so SPF passes for them. That fixes the forwarder’s deliverability, not your DMARC: the rewritten domain is theirs, so it does not align with your From header, and DMARC is still riding entirely on DKIM. SRS is a convention rather than a standard, and plenty of forwarders do not implement it.

If DKIM passed, this case needs nothing from you. Note it and move on.

A sender genuinely missing from the record

The failure you do want to act on. Look at the IP in the parenthetical and work out what it is:

An include that stopped resolving

This one produces permerror, and the mechanism is worth knowing. RFC 7208 defines include: as a recursive evaluation, and its result table is explicit: if the referenced domain has no SPF record, the include returns permerror — and that permerror propagates up and takes down your entire record. One decommissioned vendor whose SPF record was deleted can therefore break authentication for every message you send, with no other symptom.

Check every include: in your record still returns something.

Over the ten-lookup limit

Also permerror, also total. Every include, a, mx, ptr, exists and redirect counts, and the count is cumulative across the whole nested tree — RFC 7208 requires it to be tracked as one global limit, not per evaluation. Go over and receivers treat the record as unusable. Related and easier to miss: void lookups, terms whose DNS query returns nothing, are limited to two by the same section.

SPF: too many DNS lookups has the counting method and the three ways back under.

Two SPF records, or one bad character

If more than one TXT record at your domain begins v=spf1, RFC 7208 requires a permerror — the receiver is not allowed to pick one. This happens routinely during provider migrations, when a second record is added rather than the existing one edited. Syntax faults do the same: a stray semicolon, include= instead of include:, or a record split across two quoted strings that reassembles with a space in the middle.

The envelope sender is not the domain you think

SPF is looked up at exactly the envelope domain, with no inheritance — a subdomain does not fall back to its parent’s record. If your platform sends with an envelope of bounce.example.com and only example.com has a record, the result is none, not a pass borrowed from the apex.

What to do next

Take the smtp.mailfrom value and the IP out of the failing header and answer one question: is this a hop you control?

If it is not — a forwarder, a list, a redirect one of your recipients set up — do nothing to your SPF record. DKIM is already carrying the message through DMARC, which is the outcome the design intends.

If it is, and the result was fail or softfail, add that sender to your record using the provider’s include: rather than a hardcoded IP, then re-check your lookup count before you publish.

If the result was permerror, stop treating this as an SPF failure — your record is switched off entirely and every message you send is relying on DKIM alone. Query each include: for a live record, delete the ones that return nothing, and confirm there is exactly one v=spf1 TXT record on the name.

Run the domain through the checker to see the record as receivers evaluate it, lookup count included, before you edit anything.