Almost every ticket I get about authentication starts the same way. "We have SPF, DKIM and DMARC set up." Then I look at the headers of the message that bounced, and two of the three are indeed set up, for a different domain than the one in the From line. Nobody lied. The three records just verify three different things, and the marketing pages that sell them as one bundle never say which.
So here is the version I wish somebody had handed me years ago, written from the receiving side, which is the only side that matters for whether the message lands.
The short version fits in a table. The rest of the post is the long version of each row.
| SPF | DKIM | DMARC | |
|---|---|---|---|
| What it checks | Whether the connecting server may send for the bounce domain | Whether the signed headers and body arrived unchanged | Whether a domain that passed SPF or DKIM matches the From domain |
| Domain it looks at | The MAIL FROM address, later the Return-Path | The d= in the signature | The From header |
| Where it lives | A TXT record on the bounce domain | A TXT record at selector._domainkey | A TXT record at _dmarc |
| How it usually breaks | Too many DNS lookups, a service left out | A key changed on one side only, a footer added on the way | The platform signs and bounces with its own domain, so nothing aligns |
| Standard | RFC 7208 | RFC 6376 | RFC 9989 |
SPF verifies the bounce address, not the sender you see
An email carries two sender addresses. The one in the From header is the one a person reads. The other one is given during the SMTP conversation, in the MAIL FROM command, and it ends up in the Return-Path header of the delivered message, and bounces go there. SPF, defined in RFC 7208, looks at that second address and asks its domain a question in DNS. Is the server that just connected to me allowed to send mail for you?
The answer lives in a TXT record on the domain, and a healthy one is short.
example.com. TXT "v=spf1 ip4:203.0.113.0/24 include:_spf.example-esp.net -all"
The ip4 mechanism names addresses directly. The include pulls in another domain's record, which is how you authorise a sending platform without knowing its addresses. The -all at the end says everything not listed above fails. A ~all says it soft-fails, which most receivers read as a shrug rather than a verdict, and I still see ~all on records that were written in 2014 and never touched again.
There are two things SPF does not do. It says nothing about the From header, so a message with a perfectly passing SPF can show any name it likes to the reader. And it does not survive forwarding, because the forwarding server is now the one connecting, and your record never mentioned it.
One limit trips more senders than any other. A receiver will make at most ten DNS lookups while evaluating your record. include, a, mx, exists and redirect each count, and so does every nested include inside the ones you added. Three platforms, a helpdesk tool, a CRM, and a billing system, each with two or three nested includes of its own, and you are past ten. The result is permerror, and a permanent error is treated by most receivers as if you had no record at all. I once saw a domain lose its SPF for six weeks after somebody added one more include for a survey tool. The check for that is a single number in the report, and it is worth glancing at every time you add a service.
DKIM verifies the message
DKIM, defined in RFC 6376, works differently from that. The sending server takes a hash of the body, picks a list of headers, signs the lot with a private key, and adds the result as a DKIM-Signature header. The receiver fetches the matching public key from DNS and checks the maths. If anything in the signed headers or the body changed on the way, the signature fails.
The signature header names the domain and the key it used.
DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=example.com; s=s1;
h=from:to:subject:date:message-id; bh=47DEQpj8HBSa+/TImW+5JCeuQeRkm5NMpJWZG3hSuFU=;
b=...
The d= tag names the domain that vouches for the message. The s= tag is the selector, and the public key sits at s1._domainkey.example.com as a TXT record.
s1._domainkey.example.com. TXT "v=DKIM1; k=rsa; p=MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA..."
Because the signature travels inside the message, DKIM survives forwarding. A message can pass through three servers and still verify at the end, as long as none of them touched the signed parts. Mailing lists are the classic exception, since they add a footer and rewrite the subject, and that is a problem I will come back to.
Key length matters more than people expect. RFC 8301 requires at least 1024 bits and recommends 2048, and it retired the old SHA-1 variant. Keys of 512 bits still exist in DNS zones out there, generated by some control panel a decade ago, and modern verifiers refuse them outright. The message is signed, the maths checks out, and the receiver still says no.
DKIM has a blind spot of its own, though. A valid signature proves that whoever holds the key for d=example-esp.net signed this message. It does not prove that the From header has anything to do with that domain.
DMARC ties both of them to the From header
This is the record that people mean when they say "authentication", and it is the one that actually looks at the address the reader sees, defined in RFC 9989, which replaced RFC 7489 in May 2026. DMARC takes the domain from the From header and asks two questions. Did SPF pass for a domain that aligns with it? Did DKIM pass with a d= that aligns with it? One yes is enough. Two noes, and the policy you published is applied.
_dmarc.example.com. TXT "v=DMARC1; p=reject; rua=mailto:dmarc-reports@example.com; adkim=r; aspf=r"
The p= tag is the policy. Here none means report to me but deliver as usual, quarantine means put it in spam, reject means refuse it at the door. The rua= address is where receivers send aggregate reports, XML files that arrive roughly daily and tell you which servers sent mail as your domain and whether it aligned. The ruf= tag asks for per-message forensic reports, which most large receivers do not send anymore, so do not build a process around it.
Alignment is the word that does the work. In relaxed mode, the default, mail.example.com aligns with example.com because they share an organisational domain. In strict mode, set with adkim=s or aspf=s, the two have to match exactly. RFC 7489 had a pct= tag for applying the policy to a share of messages; RFC 9989 dropped it, because receivers rarely honoured values other than 0 and 100, and added t=y, a test mode that asks receivers to hold back the policy while reports still arrive. The sp= tag sets a separate policy for subdomains, which is how a strict parent domain can still let a test subdomain send unsigned mail for a while.
Since February 2024, Gmail and Yahoo require SPF or DKIM from everyone, and from bulk senders they require both, a DMARC record as well, even at p=none, and a From domain that aligns with one of the two. That last clause is the one that turned a decade of advice into a hard rule.
The four failures I see most on live traffic
Most of what crosses my desk falls into four shapes, and none of them is exotic.
The ESP signs with its own domain. The platform adds a DKIM signature with d=example-esp.net and uses its own bounce domain in the Return-Path. Both SPF and DKIM pass on their own terms. The From header says news@example.com, nothing aligns, and DMARC fails. With p=none the sender never notices, because the mail still arrives, though it arrives with a worse reputation than it deserves. The day somebody moves the policy to reject the whole list bounces. The fix is a CNAME or two so the platform signs as your domain and bounces to a subdomain of it, and every serious platform documents how.
The include chain outgrows the limit. Covered above, and I repeat it because it is the failure that arrives silently. The record looked fine last quarter.
Policy left at none forever. p=none was meant as a monitoring stage. I find it on domains that published it four years ago, collected reports into a mailbox nobody opens, and moved on. From the receiver's view, a domain that has watched its own traffic for four years and still declines to state a policy is telling them something.
Forwarding. Somebody has their work mail forwarded to a personal account. The forwarding server is not in your SPF, so SPF fails there. DKIM should carry the day, and it does when the message was left alone, but a mailing list that added a footer has broken the signature too, and now both fail on a legitimate message. This is what RFC 8617, ARC, exists for. Each server that handles the message seals what it saw of the authentication results at its hop, so the final receiver can decide to trust the chain instead of the broken signatures. The big receivers honour it, and the small ones mostly do not yet.
Reading the three in a report
The authentication section was the first thing I wrote for the tester, and the way it reads is the way I read raw headers. Start with the Authentication-Results header the receiving server added. It lists an SPF result with the domain it evaluated, a DKIM result with the d= it verified, and a DMARC result with the policy it found.
Then compare the domains rather than the verdicts. SPF pass for bounce.example-esp.net and a From of example.com is a pass that does nothing for you. DKIM pass with d=example.com is the line you want. If DMARC says pass, at least one of the two aligned, and you can stop worrying about this layer. If it says fail while both SPF and DKIM individually say pass, the answer is alignment, every time.
The tester's authentication checks do this comparison for you and put the RFC paragraph next to whatever they flag, so you can argue with the report rather than with me. An earlier post walks through why messages end up in spam in general terms. Authentication is only one of the layers there, but it is the one that fails loudly and gets fixed in an afternoon.
What a healthy setup looks like
Concretely, for a domain that sends its own transactional mail and uses a platform for newsletters.
- An SPF record with your own outbound addresses, one
includeper platform, and-allat the end. Well under ten lookups, checked again each time a service is added. - DKIM keys of 2048 bits on every service that sends as you, each with its own selector, each signing with
d=set to your domain rather than the vendor's. - A DMARC record at
p=quarantineorp=reject, withruapointing at an address that somebody actually reads, or at a service that turns the XML into a dashboard. - A separate subdomain for bulk mail, so that a bad campaign does not drag down the reputation of your invoices and password resets.
- A test send through the platform itself before every big change to DNS. Half of what the checks read is added by the sending platform, and a copy sent from your laptop tells you nothing about it.
The two lines to look for in any report are the DKIM d= and the DMARC result. If the first is your domain and the second is pass, the rest of this article is background reading.