DKIM SPF DMARC explained: the three records that prove your mail is yours
Published Updated 15 min readBy the Rasket team
Written for developers, startup founders and e-commerce teams

SPF lists the servers allowed to send for your domain. DKIM signs each message so receivers can check it was not forged or altered. DMARC tells receivers what to do when neither check passes for the From domain, and asks them to report back. Gmail asks every sender for SPF or DKIM, and anyone sending over 5,000 a day to Gmail or Outlook.com needs all three.
You usually find out the hard way: password resets start landing in spam, somebody opens one in Gmail, clicks Show original, and sees spf=pass next to dmarc=fail. Both can be true at once. This article explains why, what each record looks like, and the order to publish them in. Three passes get your mail considered, not delivered to the inbox: complaints, list quality and content decide the rest, which the deliverability guide covers.
SPF: who is allowed to send
An SPF record is one TXT record listing the sources allowed to send for a domain (RFC 7208). The receiver takes the domain from the envelope sender, the MAIL FROM address the sending server announces before the message, and checks the connecting IP address against that domain’s record. It never looks at the From header. A bounce has an empty MAIL FROM, so for bounces SPF checks the name the server gave in HELO instead (§2.4).
acme.example. TXT "v=spf1 include:_spf.google.com ~all"Two services go in the same record, one include each, before the ending. Never publish a second v=spf1 record for the new service:
acme.example. TXT "v=spf1 include:_spf.google.com include:spf.newsletter.example ~all"A service that sends from its own return-path subdomain of yours, as Rasket does and as Shopify’s CNAME records do, needs no include in this record at all: SPF is checked at that subdomain, which has a record of its own. Terms are read left to right and the last one is the catch-all. The qualifier on it decides what an unlisted server gets:
| Ending | Result | What RFC 7208 says it means |
|---|---|---|
-all | fail | The host is not authorised. Receivers may reject at once. |
~all | softfail | The host is probably not authorised. Receivers should not reject on this alone, but may filter. |
?all | neutral | No statement either way. Treated like no record. |
Which ending to use is a real trade-off, not a rule. With -all, some receivers reject a forwarded message during the SMTP conversation, before DMARC or DKIM is ever evaluated, so mail that would have passed DMARC on its signature is lost. With ~all, DMARC does the enforcing and SPF just reports. Once DMARC is at quarantine or reject, ~all loses you nothing, which is why the records Rasket generates for its own return path end in it.
One record, ten lookups
A domain may publish only one SPF record; a second one makes every check fail with a permanent error (RFC 7208 §4.5). Evaluating the record may cause at most 10 DNS lookups, counting include, a, mx, ptr, exists and redirect, including everything they pull in. A void lookup is one that finds nothing: the name does not exist, or has no record of the type asked for. An include for a service that has retired its domain is the usual cause. Separately from the 10, receivers should allow at most two void lookups; a third is a permanent error too (§4.6.4). The 10-lookup limit usually arrives slowly: a fourth vendor’s include tips a record that passed for years. To get back under, remove includes for services you no longer use, or move a sender to its own subdomain, which has its own record and its own ten lookups. Flattening the record into IP addresses works until the provider changes them.
Why SPF fails on forwarded mail
When a mailbox auto-forwards your message, the forwarding server connects from its own address, which your record has never listed. The check fails at the final destination through no fault of yours. This is the gap DKIM exists to close. ARC (RFC 8617, still Experimental) lets a forwarder or mailing list seal the results it saw before it changed anything, and a receiver that trusts that intermediary may deliver on them. It is the intermediary’s fix, not a record you publish.
DKIM: a signature that travels with the message
DKIM (RFC 6376) adds a signature header covering chosen headers and a hash of the body. The sending service holds the private key; the public key is a TXT record under a selector, a label that lets one domain publish several keys at once.
rasket._domainkey.acme.example. TXT "p=MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA..."The v= and k= tags many panels show are optional (RFC 6376 §3.6.1), so a bare p= is a complete key. An empty p= means the key is revoked.
DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed;
d=acme.example; s=rasket; h=from:to:subject:date:message-id;
bh=2jUSOH9NhtVGCQWNr9BrIAPreKQjO6Sn7XIkfJVOzv8=;
b=dzdVyOfAKCdLXdJOc9G2q8LoXSlEniSb...A receiver reads d= and s=, fetches the key at s._domainkey.d, and recomputes both hashes. Nothing in that depends on which server connected, so a signature usually survives forwarding. It breaks when something rewrites a signed part of the message, such as a mailing list adding a footer or a gateway rewriting links.
Key size, if you generate your own keys
RFC 8301 §3.2 says signers must use RSA keys of at least 1024 bits and should use 2048, and verifiers must treat signatures made with smaller keys as invalid; RFC 6376 had still required verifiers to accept keys from 512 bits. Gmail requires 1024 bits or more and recommends 2048. So publish 2048 because it is recommended and every large receiver verifies it, not because 1024 is rejected. Rasket generates 2048-bit keys, with a 1024-bit fallback for the few DNS panels that cannot store a TXT value that long. A 2048-bit value is over 255 characters, so it is published as two quoted strings that resolvers join; a panel showing it split is normal.
Subdomains and rotation
The key is looked up at the signature’s d= domain, not at the From domain. Mail from news.acme.example signed with d=acme.example uses the parent’s key and still aligns under relaxed DMARC. A subdomain needs its own key only when it signs as itself. To rotate, publish the new key under a new selector, sign with it, and delete the old record a few days later, once no message signed with it can still be in a queue.
DMARC: the policy, alignment and reports
A DMARC record sits at _dmarc under the domain in the From header. If there is none, receivers fall back to the domain you registered, its organisational domain (acme.example for mail.acme.example); RFC 9989 receivers find it by walking up the DNS tree, older ones from the Public Suffix List. A sensible first record:
_dmarc.acme.example. TXT "v=DMARC1; p=none; rua=mailto:dmarc-reports@acme.example"In a DNS panel the Host or Name box gets _dmarc and the Value box gets the quoted text. Publish exactly one: when a name has two DMARC records, receivers discard both and the domain has no policy (RFC 9989 §4.10).
A message passes DMARC when SPF or DKIM passes for a domain that aligns with the From domain, meaning it is the From domain or shares its organisational domain. It fails only when neither produces an aligned pass. One aligned leg is enough, which is why publishing both is worth it: they fail in different places.
| Policy | What receivers do with a failing message |
|---|---|
p=none | Nothing different. They deliver as before and send you reports. |
p=quarantine | Treat it as suspicious, usually the spam folder. |
p=reject | Refuse it, if the receiver's own evidence agrees; Gmail and most large receivers refuse during the SMTP conversation. RFC 9989 §7.4 says a receiver with no other evidence quarantines instead. |
Alignment: relaxed, not strict
Relaxed alignment, the default, accepts any domain under the same organisational domain: a return path on rasket.acme.example aligns with a From of acme.example. Strict mode, the aspf and adkim tags set to s, demands an exact match. A sending service’s return path is a subdomain of your From domain, so under strict SPF alignment it matches only when the From address uses that same host, which is almost never what you want. Leave both tags out unless you have a specific reason.
What changed in RFC 9989, if you write the record yourself
RFC 9989 (May 2026) replaced RFC 7489 as the DMARC standard. Three changes matter when you write a record. The pct tag is gone; its job is now t=y, a testing flag that asks receivers to apply one step less than your policy (quarantine is treated as none, reject as quarantine). The rf and ri tags are gone too. And the organisational domain is found by walking up the DNS tree rather than from the Public Suffix List. A new np= tag sets the policy for subdomains that do not exist, a favourite of spoofers, beside the older sp=, which sets it for subdomains that do exist: v=DMARC1; p=quarantine; sp=quarantine; np=reject quarantines failures from the domain and its real subdomains and rejects mail from invented ones. (np= first appeared in RFC 9091; a receiver still on RFC 7489 ignores it and applies sp=.) Aggregate reports (rua) are now specified in RFC 9990.
You cannot tell which standard a receiver follows, and Google’s and Microsoft’s help pages still describe pct, so write records that read the same under both. A receiver on RFC 7489 ignores the t tag it does not know, and RFC 9989 defines t=y as what pct=0 did (Appendix A.6): one step softer. So p=reject; pct=0; t=y asks receivers on either standard for quarantine, the safe way to rehearse reject. Any other pct value is read only by the old generation.
Receivers that send aggregate reports to the rua address usually send one a day: how many messages claimed your domain, from which IP addresses, and which checks passed. Many small receivers send none. Failure reports (ruf) contain parts of individual messages and few receivers send them; leave them off unless you need them. On a small team the reports belong to whoever can edit DNS, since every fix they lead to is a record change. If nobody there wants to read XML, a DMARC report service will: it gives you an rua address on its domain and publishes the record that authorises it, here acme.example._report._dmarc.reports.example with the value v=DMARC1, without which receivers will not send to another domain (RFC 9990 §4). This is the part of a report to read:
<record>
<row>
<source_ip>192.0.2.10</source_ip>
<count>412</count>
<policy_evaluated>
<disposition>none</disposition>
<dkim>pass</dkim>
<spf>fail</spf>
</policy_evaluated>
</row>
<identifiers><header_from>acme.example</header_from></identifiers>
<auth_results>
<dkim><domain>acme.example</domain><selector>rasket</selector><result>pass</result></dkim>
<spf><domain>mailer.example.net</domain><result>pass</result></spf>
</auth_results>
</record>auth_results holds the raw checks; policy_evaluated holds the aligned ones DMARC used. Here SPF passed for mailer.example.net, which does not align with acme.example, so DMARC counted SPF as failed; the aligned DKIM pass carried the message. A row where both are fail is either a forger or a sender of yours that still needs its records.
Parsing reports, if you write code
RFC 9990 §3.5.2 asks for gzipped XML attachments, and many reporters still send zip. A few dozen lines of Python’s standard library (3.8 or later) are enough to list the rows that matter:
import gzip, sys, zipfile
import xml.etree.ElementTree as ET
LIMIT = 20_000_000 # a report is untrusted input: refuse a decompression bomb
def read(path):
if path.endswith(".zip"):
with zipfile.ZipFile(path) as archive, archive.open(archive.namelist()[0]) as stream:
data = stream.read(LIMIT + 1)
else:
with (gzip.open if path.endswith(".gz") else open)(path, "rb") as stream:
data = stream.read(LIMIT + 1)
if len(data) > LIMIT:
sys.exit(f"{path}: over {LIMIT} bytes uncompressed")
return data
def field(record, path):
return record.findtext("{*}row/{*}" + path, "?")
# {*} matches any namespace: RFC 9990 reports have one, older reports have none.
root = ET.fromstring(read(sys.argv[1]))
for record in root.iterfind(".//{*}record"):
dkim = field(record, "policy_evaluated/{*}dkim")
spf = field(record, "policy_evaluated/{*}spf")
flag = " <- no aligned pass" if "pass" not in (dkim, spf) else ""
print(f"{field(record, 'source_ip'):<15} {field(record, 'count'):>6} dkim={dkim} spf={spf}{flag}")$ python3 dmarc_rows.py 'google.com!acme.example!1760140800!1760227199.xml.gz'
192.0.2.10 412 dkim=pass spf=fail
198.51.100.23 1180 dkim=fail spf=fail <- no aligned pass
203.0.113.7 3 dkim=fail spf=fail <- no aligned passSort the flagged rows by count and look up each IP. Here 1,180 messages came from an invoicing service still sending as you without your DKIM key, a sender to fix before enforcing; three from an address you have never used are forgeries that p=reject will stop. A report is mail from anyone who claims to be a receiver, and RFC 9990 §8.1 warns of zip and XML bombs. The size cap stops the first. For the second, Python’s docs say its expat parser may be vulnerable below version 2.7.2: check pyexpat.EXPAT_VERSION, and on an older one parse with the defusedxml package’s fromstring instead.
Check the verdicts on a message you already sent
Before changing any DNS, look at what receivers already think. In Gmail, open a message you sent, choose Show original from the three-dot menu, and the top of the page lists SPF, DKIM and DMARC with PASS or FAIL beside each. In Outlook it is View message source. Under that summary is the header it was built from: every large mailbox provider writes its email authentication results into an Authentication-Results header (RFC 8601).
Authentication-Results: mx.google.com;
dkim=pass header.i=@acme.example header.s=rasket;
spf=pass (google.com: domain of bounce@rasket.acme.example designates 192.0.2.10 as permitted sender) smtp.mailfrom=bounce@rasket.acme.example;
dmarc=pass (p=NONE sp=NONE dis=NONE) header.from=acme.exampleEach verdict names a domain, and DMARC’s alignment test compares them. header.from is the domain the reader sees. smtp.mailfrom is the domain SPF was checked for. header.i is the identity DKIM signed as: its domain is the signature’s d= or a subdomain of it, and some receivers print header.d, the d= itself, instead. Here all three sit under acme.example, so DMARC passes. The common failure is spf=pass for your sending service’s own domain: a real pass that aligns with nothing of yours.
What Gmail and Outlook.com require
| Gmail | Outlook.com | |
|---|---|---|
| Applies to | Every sender; more for over 5,000 messages a day to Gmail | Domains sending over 5,000 messages a day to Outlook.com, Hotmail and Live |
| SPF and DKIM | Every sender needs SPF or DKIM; bulk senders need both | Both must pass |
| DMARC | Bulk senders: a record, p=none is enough | At least p=none |
| Alignment | Bulk senders: From must align with SPF or DKIM | With SPF or DKIM, preferably both |
| Since | February 2024 | 5 May 2025 |
| If you miss it | Mail is rate-limited, filtered or rejected | Junk folder or refusal with 550 5.7.515; Microsoft's pages describe both |
The numbers are from each provider’s own page. The bulk sender checklist covers the rest of the bulk rules, unsubscribe and spam rate included. For this article the point is that records are checked on every message, so a record dropped in a DNS migration becomes a delivery incident the same day.
The order to publish them in
- Day 1: SPF and DKIM for every service that sends as you. Your mailbox provider, your product mail, your store, your newsletter tool, your helpdesk. Each shows its records in its own settings, usually under a name like sender domain or domain authentication. A WooCommerce store’s records come from whichever mail service its WordPress site sends through.
- Day 1: DMARC at
p=nonewith aruaaddress. It changes nothing about delivery and starts the reports. - Then read the reports for a full sending cycle. That is a month if a monthly invoice run sends as you. The usual find is a forgotten sender: an invoicing system, a survey tool or, for a store, a review-request app sending as your domain without alignment. Fix or remove each one.
- Then
p=quarantine, thenp=reject. Rehearse reject first with the testing record from the RFC 9989 section above. RFC 9989 §7.4 says a domain at reject “MUST apply valid DKIM signatures” rather than rely on SPF alone, and that one whose people post to mailing lists “SHOULD NOT” publish reject at all; if it does, it should spend “at least a month” atp=noneand as long again atp=quarantinefirst. To back out, publishp=noneagain; receivers see it once the old record’s TTL runs out, so lower the TTL a day before each step. The p=none to p=reject guide has each stage and how long to hold it.
dig +short TXT acme.example | grep spf1
dig +short TXT rasket._domainkey.acme.example
dig +short TXT _dmarc.acme.exampleEach command should print one record. A selector published as a CNAME, as Shopify’s and Microsoft 365’s are, prints the CNAME target on a line above it. No output means the record is missing or at the wrong name; two SPF lines means a permanent error. A new record usually answers within minutes. A changed one can keep serving its old value until the old TTL runs out, and a name you looked up before it existed stays “not found” for the zone’s negative-caching TTL (RFC 2308). To see what you just published without either cache, ask your DNS host’s own server: add @ns1.yourdnshost.example, the name dig NS acme.example prints. Shopify says its records can take up to 48 hours.
What Rasket generates for a sending domain
Adding a domain generates its DKIM key under the rasket selector, signed as your own domain so DKIM aligns exactly, and the records for a return path at rasket.yourdomain, so SPF aligns in relaxed mode. Records keep being re-checked. When one stops resolving, its status changes on the domain page and a domain.updated webhook event fires, so your code can alert on it.
DMARC is not required to verify a domain, so it is the one record we do not check. If your domain has no DMARC policy, the domain page suggests the p=none record above for you to publish, with reports going to your own address, not to us; tightening it is your decision. The return path article explains the SPF half in detail, and the verification walkthrough covers publishing.
If you only send from Google Workspace or Microsoft 365, you do not need a sending service for any of this: both generate a DKIM key and tell you the SPF include to add.
Frequently asked questions
What do I add if my company mail is on Google Workspace or Microsoft 365?
For Google Workspace: include:_spf.google.com in your SPF record, and a DKIM key generated under Apps, Google Workspace, Gmail, Authenticate email. For Microsoft 365: include:spf.protection.outlook.com, and the two DKIM CNAME records (selector1 and selector2) the Defender portal shows. DMARC is yours to publish either way.
I send a few hundred emails a month. What is the minimum?
All three, even at that volume: SPF and DKIM are generated for you by any sending service, and a DMARC record at p=none costs nothing, changes no delivery and shows you who else is sending as your domain.
Can one domain have several DKIM keys?
Yes. Each key lives under its own selector, so your mailbox provider, your product mail and your newsletter tool can each sign with their own key at the same time.
What happens to a message that fails DMARC at p=reject?
Large receivers such as Gmail usually refuse it during the SMTP conversation with a 5xx reply, so it never reaches the recipient and the sender gets a bounce. RFC 9989 asks receivers not to reject on the policy alone, so one with no other evidence quarantines it instead. The risk is legitimate mail that lost alignment on the way, typically through a mailing list that rewrote it. That is why the move to reject comes after weeks of reports, not before.
Does DMARC stop lookalike domains and fake display names?
No. DMARC protects the exact domain in the From header and the domains under it. A message from acme-billing.example, or one showing 'Acme Support' as the name over an unrelated address, passes or fails on its own domain's records. Registering obvious lookalikes and training staff are the controls for those.
Should a domain that never sends email have these records?
Yes, and strict ones, because an unused domain is an easy one to spoof. Publish v=spf1 -all so no server is authorised, v=DMARC1; p=reject at _dmarc, and optionally a null MX (MX 0 ., the only MX record, RFC 7505) to say it receives no mail either. No DKIM key is needed when nothing signs.
Sources
- RFC 7208: Sender Policy Framework (SPF), sections 2.4, 2.6, 4.5 and 4.6.4 — IETF, read 2026-10-17
- RFC 6376: DomainKeys Identified Mail (DKIM) Signatures, section 3.6.1 — IETF, read 2026-10-16
- RFC 8301: Cryptographic Algorithm and Key Usage Update to DKIM, section 3.2 — IETF, read 2026-10-16
- RFC 9989: Domain-Based Message Authentication, Reporting, and Conformance (DMARC), sections 4.7, 4.10 and 7.4 and appendix A.6 — IETF, read 2026-10-17
- RFC 9990: DMARC Aggregate Reporting, sections 3.5.2, 4 and 8.1 — IETF, read 2026-10-17
- RFC 7489: DMARC (obsoleted by RFC 9989), section 6.6.4 — IETF, read 2026-10-17
- RFC 8617: The Authenticated Received Chain (ARC) Protocol — IETF, read 2026-10-17
- XML security: vulnerable Expat versions — Python Software Foundation, read 2026-10-17
- RFC 9091: Experimental DMARC Extension for Public Suffix Domains (np=) — IETF, read 2026-10-17
- RFC 2308: Negative Caching of DNS Queries — IETF, read 2026-10-17
- RFC 8601: Message Header Field for Indicating Message Authentication Status — IETF, read 2026-10-16
- Email sender guidelines — Google, read 2026-10-16
- Outlook.com Postmaster: policies, practices and guidelines — Microsoft, read 2026-10-16
- Setting up your email: authenticating your sender email domain — Shopify, read 2026-10-17
- Rasket docs: Domains (guide) — Rasket, read 2026-10-17
- Rasket docs: Events (domain.updated) — Rasket, read 2026-10-17
Related
- DMARC p=reject: move from p=none safely — Move DMARC from p=none to p=reject without losing mail: read aggregate reports, align every sender with DKIM, quarantine, then reject, and roll back fast.
- Gmail bulk sender requirements: the full checklist — What every Gmail sender must do, what bulk senders add, and Yahoo and Outlook.com side by side: SPF, DKIM, DMARC, unsubscribe, spam rate and error codes.
- Custom return path: why your bounces need it — What the return path is, how it differs from the From header, why a return path on your own subdomain makes SPF count toward DMARC, and the records it needs.
- Email domain verification, step by step — Which DNS records verify a sending domain, where each one goes, how long it normally takes, and what each status on the domain page tells you to do next.
- DMARC alignment — Alignment is the part of DMARC that asks whether the domain that passed authentication is the same one the reader sees. A DKIM signature or an SPF check can…
- Domains — Add a domain, publish the generated record set and send from it. A 2048-bit DKIM key per domain, a custom return path, and verification that runs on its own.