SPF, DKIM and DMARC explained once and for all
The exact DNS records, the order in which recipients check them, the errors that render them useless, and how to read a DMARC report.
UptimeMag editorial team · 12 October 2026 · 5 min read

The quickest verification
Before touching DNS, open the email that ended up in spam (or the one that never arrived) and look at the full headers: in Gmail it's "Show original", in Outlook it's "Message headers". Search for the line starting with Authentication-Results: it reports three results, spf=, dkim= and dmarc=, each with pass, fail, softfail or none. This is the quickest way to find out which of the three protocols is failing without guessing. If the message comes from outside, the recipient can always check the headers from another mail client to verify authentication.
Who checks what, and in what order
The receiving server evaluates SPF first (is the sender's IP address authorised?) and then DKIM (does the cryptographic signature in the header match the public key published in DNS?). DMARC comes last: it doesn't verify anything on its own, but checks that at least one of the two results is "aligned" with the domain shown in the From field, and decides what to do based on the published policy.
The SPF record: syntax and limits
An SPF record is a TXT record in your main zone's DNS. Here's a real example of a domain using Google Workspace plus its own IP, read with dig +short txt yourdomain: "v=spf1 ip4:128.173.235.0/27 ip6:2001:468:c80:a102:1:1000:1024:0/120 ip6:2607:b400:000a:0280::/64 mx a include:_spf.google.com ~all". If mail goes through Microsoft 365/Exchange instead, the typical record is "v=spf1 include:spf.protection.outlook.com ~all".
The pieces that matter:
- mechanism: mechanism = ( all / include / a / mx / ptr / ip4 / ip6 / exists )
- qualifier before the mechanism: qualifier = "+" / "-" / "?" / "
" (no sign equals "+", which is pass; "-" is fail; "" is softfail, used during testing)
A limit to remember: RFC 7208 requires that SPF validators limit to ten the terms that generate a DNS query during the check; the mechanisms involved are include, a, mx, ptr and exists, plus the redirect modifier. Exceeding it doesn't give a warning: it gives a permerror, and according to RFC 7208 if there are syntax errors in the record, check_host() returns "permerror" immediately without further interpretation.
The most common errors, by frequency
- Too many
includedirectives: each service added over time (CRM, billing, newsletter, old provider never removed) adds a lookup. Beyond ten, SPF breaks for all messages, not just those from the excess service. Typical cause of permerror, along with duplicate mechanisms or empty lookups. - Two SPF records on the same domain: a
dig txtreturning two lines both starting withv=spf1is always an error. The domain must have only one: if multiple sources are needed, add them asincludestatements within the same record, not as separate records. - Forgetting
-allor leaving+all: without a restrictive qualifier at the end, SPF authorises anyone, making the check pointless.
The DKIM record
DKIM signs the message body and some headers with a private key; the recipient retrieves the public key from a TXT published on selector._domainkey.yourdomain, where the selector is the name chosen by the sending software (e.g. google, mail, default). The reference standard is RFC 6376. The typical value of the TXT is v=DKIM1; k=rsa; p= followed by the public key in base64. Two practical causes of failure: the selector in DNS doesn't match the one written in the message's DKIM-Signature header, or the key was regenerated on the sending server but not updated in DNS.
The DMARC record: tag syntax
The DMARC record goes on _dmarc.yourdomain as a TXT record. The basic syntax, according to Microsoft documentation: Hostname: _dmarc, TXT value: v=DMARC1; policy DMARC; percentage of mail subject to the policy; address for aggregate reports; address for forensic reports.
| Tag | Meaning | Values |
|---|---|---|
| p= | policy for the main domain | none, quarantine, reject |
| sp= | separate policy for subdomains | none, quarantine, reject |
| pct= | percentage of messages to which the policy applies | 0-100, default 100 |
| rua= | address for XML aggregate reports | mailto: |
| ruf= | address for forensic reports (message by message) | mailto: |
| adkim= / aspf= | strictness of DKIM/SPF alignment | s (strict) or r (relaxed) |
The meaning of the three policies, from RFC 7489: with quarantine the domain owner asks that mail failing DMARC be treated as suspicious by mail receivers: in practice it ends up in spam or is examined more closely; with reject the owner asks that mail receivers reject mail failing the DMARC check, preferably during the SMTP transaction itself. The pct tag indicates the percentage of the domain owner's message flow to which the DMARC policy should be applied, useful for gradually raising enforcement.
The most widespread error: DMARC stuck on p=none forever
Many publish the record, receive the reports, and stop there. p=none only serves to collect data: it doesn't block or quarantine anything. The correct path starts with a minimal record like v=DMARC1; p=none; rua=mailto:address@yourdomain, designed to provide operational visibility without impact, and moves towards enforcement only when every legitimate source is known and SPF or DKIM pass in alignment with the domain visible in the From field. Anyone staying in none for months has only installed a monitoring system, not a protection.
How to read an aggregate DMARC report
Reports that arrive at the rua address are XML files, usually zipped, sent approximately once a day by each recipient that supports reporting. Inside you find, for each source IP: how many messages it sent, whether SPF and DKIM passed for that IP, and whether they align with the From domain. An IP sending high volumes and consistently failing both is almost always a third-party service (newsletter, CRM, billing) never added to SPF or never configured to sign with DKIM: it should be added, not ignored.
If the problem is with the provider
If you use a managed sending service (shared hosting, Google Workspace, Microsoft 365, email marketing platforms) and the DKIM signature doesn't appear in the message header at all, the problem is almost always on the provider's side: the signature needs to be activated and the selector published by them. In this case, contact support stating the exact domain, the DKIM selector their system should use, and if you have them, two or three Message-ID values from emails sent without a signature: without these references the ticket resolves more slowly.
Written with the help of artificial intelligence and checked by the editors (EU AI Act, art. 50).