SPF vs DKIM vs DMARC: Why Two Passes Can Still Fail
SPF, DKIM, and DMARC are not three grades for the same test. SPF checks whether the sending server may use the SMTP envelope domain. DKIM verifies a signature made by the domain in d=. DMARC asks whether at least one of those authenticated domains aligns with the domain a person sees in From:.
That last comparison explains the result that confuses people most: SPF can pass, DKIM can pass, and DMARC can still fail.
Start with the result that looks contradictory
Here is a hypothetical but standards-valid header trace. The .example names keep the domains fictional:
From: Billing <billing@brand.example>
Return-Path: <bounce@sender.example>
Authentication-Results: mx.receiver.example;
spf=pass smtp.mailfrom=sender.example;
dkim=pass header.d=sender.example header.s=mail;
dmarc=fail header.from=brand.example
The visible sender is brand.example. Both passing checks authenticated sender.example. Those are different organizational domains, so neither result supports the visible identity.
I start with the domain after header.from=, then compare the SPF and DKIM domains with it. Reading the three pass or fail words without their identities loses the part that matters.
SPF passed the envelope sender
SPF checks the SMTP MAIL FROM identity, or the HELO identity for a null reverse path. It asks whether the connecting IP is authorized by that domain's SPF policy.
In the trace, smtp.mailfrom=sender.example passed. That says nothing yet about billing@brand.example, because the visible From: header is a separate identity.
This separation is normal. Transactional email services often use a bounce domain in the SMTP envelope while showing your brand domain to the recipient. The setup becomes a DMARC problem only when the authenticated domain does not align with the visible one.
DKIM passed a signature from another domain
DKIM attaches a cryptographic signature and identifies its signing domain with d=. A receiver retrieves the public key from the selector under _domainkey and verifies the signed header fields and body hash.
Here, header.d=sender.example passed. The signature survived, but the signer still belongs to sender.example. RFC 6376 limits DKIM's mandatory identity output to that signing domain; extra meaning about the author or safety of the message cannot be assumed from the signature alone.
Well, the email now has a visible author, an envelope sender, and a signing domain (one identity would have been too relaxing). DMARC exists to connect the authenticated identities to the one the reader sees.
DMARC failed the alignment comparison
DMARC takes the author domain from the RFC 5322 From: field. In this example that is brand.example. It then checks the domains authenticated by SPF and DKIM.
RFC 9989 calls this identifier alignment. Relaxed alignment, the default, accepts domains with the same organizational domain. Strict alignment requires an exact match.
sender.example and brand.example satisfy neither mode. DMARC therefore fails even though both underlying mechanisms passed their own jobs.
The strongest fair objection is that two cryptographic or DNS-backed passes should count for something. They do, but any domain owner can authorize its own servers and sign its own mail. Without alignment, an attacker could authenticate attacker.example and place brand.example in the visible From:. DMARC passes when at least one authenticated identifier aligns, not when a message merely collects pass results.
A corrected trace makes the relationship visible
Configure the sending service to use an aligned custom envelope domain and an aligned DKIM signing domain. An exact-domain example looks like this:
From: Billing <billing@brand.example>
Return-Path: <bounce@brand.example>
Authentication-Results: mx.receiver.example;
spf=pass smtp.mailfrom=brand.example;
dkim=pass header.d=brand.example header.s=mail;
dmarc=pass header.from=brand.example
Now both authenticated identities align with header.from=brand.example. DMARC needs only one aligned pass, but configuring both gives legitimate mail another path to pass when forwarding or message modification breaks one mechanism.
This article owns the header-reading problem. If you are changing an existing policy record, the RFC 9989 migration guide covers the current tags, reporting changes, and enforcement rollout.
Check DNS without confusing configuration with evidence
Once you know the three domains and the DKIM selector, inspect the records that should support them:
dig +short TXT brand.example
dig +short TXT mail._domainkey.brand.example
dig +short TXT _dmarc.brand.example
The first query can return the SPF policy for the envelope domain. The second looks up the DKIM key for selector mail. The third returns the DMARC policy for the visible author domain.
A record's presence is configuration evidence, not proof that a particular message passed. The receiving system performs the checks using the connection, message, and DNS state it observed, then records the outcome in Authentication-Results.
There is one security catch. Authentication-Results can itself be forged before a message enters your mail system. RFC 8601 requires receivers to manage these headers across their trust boundary. For incident analysis or automated filtering, trust the result inserted by your own receiving service, not an arbitrary older copy lower in the header block.
Authentication stops at domain authorization
A DMARC pass does not prove that a message is harmless. A compromised account, an abusive but properly authenticated domain, or a convincing look-alike domain can all pass authentication.
It also does not verify the recipient address. SPF, DKIM, and DMARC protect sending-domain identities. Email verification evaluates whether a signup address is structured, routed, risky, deliverable, or inconclusive, while confirmation tests current inbox access.
The validation, verification, and confirmation guide keeps those recipient-side checks separate. If you need them in an application, the emailverifier.dev API reference documents the status, action, signals, and suggestion fields.
If header.from does not align with either passed identity, fix the sender configuration before tightening DMARC policy.