Email Spam Tester

Email Spam TesterBlog › SPF, DKIM & DMARC Explained

SPF, DKIM & DMARC Explained

What SPF, DKIM and DMARC each actually verify, the failures I keep seeing on live mail traffic, and how to read the three of them in a report.

By Serhii Tanichev9 min read

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.

SPFDKIMDMARC
What it checksWhether the connecting server may send for the bounce domainWhether the signed headers and body arrived unchangedWhether a domain that passed SPF or DKIM matches the From domain
Domain it looks atThe MAIL FROM address, later the Return-PathThe d= in the signatureThe From header
Where it livesA TXT record on the bounce domainA TXT record at selector._domainkeyA TXT record at _dmarc
How it usually breaksToo many DNS lookups, a service left outA key changed on one side only, a footer added on the wayThe platform signs and bounces with its own domain, so nothing aligns
StandardRFC 7208RFC 6376RFC 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.

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.

Try it on your own email

Send one message to a disposable address and read every check in the report, RFC section and all. Free, no signup.

Run a test

More from the blog