Short answer
SPF lists the servers allowed to send for a domain, DKIM signs each message with a key published in your DNS, and DMARC tells receivers what to do when a message fails, but only counts SPF or DKIM if they pass for a domain that aligns with the visible From address. For transactional email you need all three, with DKIM signing as your own domain.
The minimum set is one SPF TXT record that includes your provider, the provider's DKIM key at selector._domainkey, and a DMARC record at _dmarc starting at p=none with reports, tightened to quarantine or reject once the reports show all your mail passing.
On this page
What SPF, DKIM and DMARC each do
| SPF | DKIM | DMARC | |
|---|---|---|---|
| What it proves | The sending server is allowed to send for the envelope domain | The message wasn't altered and was signed by the holder of a domain's key | SPF or DKIM passed for a domain aligned with the From address |
| Domain it checks | Return-path (MAIL FROM) | The d= domain in the DKIM-Signature header | The From: header domain |
| DNS record | TXT on the envelope domain: v=spf1 ... | TXT at selector._domainkey.domain | TXT at _dmarc.domain |
| Survives forwarding | Usually not (the forwarder's IP isn't listed) | Usually yes, if the body isn't changed | Yes, if DKIM survives |
| Spec | RFC 7208 | RFC 6376 | RFC 7489 |
None of the three is optional any more. Google's sender guidelines require SPF or DKIM from everyone and SPF, DKIM and DMARC from bulk senders, and Yahoo has matching rules. The thresholds and the rest of the requirements are in Google, Yahoo and Microsoft sender requirements.
Alignment: the part most setups get wrong
SPF and DKIM can both pass for a domain that has nothing to do with the From address the user sees. DMARC closes that gap: it only counts a pass if the domain that passed aligns with the From domain. A message needs one aligned pass, SPF or DKIM, for DMARC to pass.
| Mode | Tag | Aligns when | Example with From `example.com` |
|---|---|---|---|
| Relaxed (default) | adkim=r, aspf=r | Same organizational domain | d=mail.example.com aligns; d=example.net doesn't |
| Strict | adkim=s, aspf=s | Exactly the same domain | Only d=example.com aligns |
Leave alignment relaxed unless you have a reason not to. Strict mode breaks the common setup where the return path is a subdomain such as rp.example.com.
The failure to look for: an email provider signs with its own domain (d=provider-mail.com) and uses its own bounce domain. SPF passes, DKIM passes, DMARC fails. Verifying your domain with the provider, so it signs with d= your domain, is what fixes it.
Send from a subdomain or the root?
Sending transactional mail from a subdomain such as mail.example.com or notify.example.com keeps its reputation partly separate from the root domain, where your team's own mailboxes and perhaps your marketing mail live. It also keeps the SPF record short, because the subdomain's SPF only needs your transactional provider.
Three things to know about subdomains:
- SPF doesn't inherit.
mail.example.comneeds its own SPF record if it's used as an envelope domain. - DKIM is per selector and domain. The key lives under the sending domain:
selector._domainkey.mail.example.com. - DMARC does inherit. With no
_dmarc.mail.example.comrecord, receivers apply the root domain's policy (itssp=tag if set, otherwisep=). You can publish a separate record on the subdomain to give it its own policy and reports.
The From address should still be recognisably yours. no-reply@mail.example.com reads as your brand; a separately registered lookalike domain reads as phishing.
Copy-paste records
SPF: one record, one include per sender
TXT on the sending domain
example.com. TXT "v=spf1 include:spf.groundmail.dev ~all"A host can have only one SPF record. If one already exists, add your provider's include to it rather than creating a second record; two v=spf1 records make SPF return permerror for every message. A merged record for a domain that also uses Google Workspace:
Merged SPF
example.com. TXT "v=spf1 include:_spf.google.com include:spf.groundmail.dev ~all"SPF allows at most 10 DNS lookups per check. Each include, a, mx, ptr, exists and redirect counts, including the ones nested inside the includes you reference; ip4, ip6 and all don't. Go over and SPF fails with permerror. Remove includes for services you no longer use before adding new ones, and use a subdomain for transactional mail if the root is crowded.
DKIM: a public key at selector._domainkey
TXT at <selector>._domainkey
selector1._domainkey.example.com. TXT "v=DKIM1; k=rsa; p=MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA..."Your provider generates the key pair and gives you the selector and the public key; copy them exactly. A 2048-bit key is longer than the 255-character limit for a single DNS string, so it's stored as several quoted strings in one TXT record. Most DNS dashboards split it for you; if you edit zone files by hand, split it yourself and don't add spaces or line breaks inside the key.
DMARC: start at p=none, with reports
TXT at _dmarc
; Start here: monitor only, send aggregate reports
_dmarc.example.com. TXT "v=DMARC1; p=none; rua=mailto:dmarc@example.com"
; After reports show your mail passing
_dmarc.example.com. TXT "v=DMARC1; p=quarantine; rua=mailto:dmarc@example.com"
; Final
_dmarc.example.com. TXT "v=DMARC1; p=reject; rua=mailto:dmarc@example.com"Aggregate reports (rua) arrive daily as XML from the big mailbox providers and list every IP that sent mail claiming your domain, with SPF, DKIM and alignment results. Send them to a mailbox you read or to a DMARC report service. If the report address is on a different domain, that domain has to publish a record authorising it to receive your reports.
The Groundmail record set
Groundmail sends from our own mail servers on our own IP space, with dedicated IPs and dedicated servers available when you want your reputation isolated. Whichever you use, the DNS records are the same, and they make your mail sign and bounce under your own domain so DMARC aligns.
When you add a domain (POST /domains or the dashboard), you get these records. Replace example.com with the domain you added, such as mail.example.com.
| Purpose | Type | Host | Value |
|---|---|---|---|
| Ownership | TXT | _groundmail.example.com | groundmail-verify=... (from your dashboard) |
| DKIM | TXT | <selector>._domainkey.example.com | The public key shown for your domain |
| Return path | CNAME | rp.example.com | rp.groundmail.dev (DNS only, not proxied) |
| SPF | TXT | example.com | v=spf1 include:spf.groundmail.dev ~all, merged with any existing SPF |
| DMARC | TXT | _dmarc.example.com | v=DMARC1; p=none; rua=mailto:... to start |
Ownership, DKIM, return path and SPF are required to verify. DMARC is recommended for every sending domain.
The return-path CNAME puts the envelope sender under your domain, so SPF can align, and the DKIM key makes us sign as d= your domain. On Cloudflare, GoDaddy and Vercel, one-click apply writes the records for you and merges SPF into any record you already have. Details are in the domains docs.
Check the records with dig
Query a public resolver directly so you see what receivers see rather than a stale local cache:
Terminal
# SPF: expect exactly one line starting with "v=spf1"
dig +short TXT example.com @1.1.1.1 | grep spf1
# DKIM: expect "v=DKIM1; k=rsa; p=..." (possibly as several quoted strings)
dig +short TXT selector1._domainkey.example.com @1.1.1.1
# DMARC
dig +short TXT _dmarc.example.com @1.1.1.1
# Return path: expect rp.groundmail.dev. (not an IP address)
dig +short CNAME rp.example.com @1.1.1.1
# Groundmail ownership
dig +short TXT _groundmail.example.com @1.1.1.1If the CNAME query returns nothing but dig +short rp.example.com returns IP addresses, the record is proxied at your DNS host. If the SPF query returns two lines, you have two SPF records.
Read the Authentication-Results header
DNS being right doesn't prove a message passes. Send a real message to a Gmail or Outlook account and open the raw source ("Show original" in Gmail). The receiver records its verdict in Authentication-Results:
Passing and aligned (illustrative)
Authentication-Results: mx.google.com;
dkim=pass header.i=@example.com header.s=selector1;
spf=pass (google.com: domain of bounces@rp.example.com designates 203.0.113.10 as permitted sender) smtp.mailfrom=bounces@rp.example.com;
dmarc=pass (p=REJECT sp=REJECT dis=NONE) header.from=example.comdkim=passwithheader.iord=on your domain: DKIM passes and aligns.spf=passwithsmtp.mailfromunder your domain: SPF passes and aligns.dmarc=passwithheader.fromyour domain: at least one of the above aligned.dmarc=failwith SPF and DKIM both passing: they passed for someone else's domain. Check the domains, not just the verdicts.
Common mistakes
| Mistake | What happens | Fix |
|---|---|---|
| Two SPF records on one host | SPF permerror for all mail from the domain | Merge into one v=spf1 record |
| More than 10 SPF lookups | SPF permerror | Remove unused includes; move transactional mail to a subdomain |
-all before DMARC is trusted | Some receivers reject on SPF failure alone, including forwarded mail that DKIM would have saved | Use ~all and let DMARC enforce |
| Return-path CNAME proxied at Cloudflare | The CNAME resolves to Cloudflare IPs, so return-path checks and SPF alignment fail | Set the record to DNS only (grey cloud) |
| DKIM key truncated, quoted twice or broken by spaces | dkim=fail or dkim=permerror | Re-paste the key exactly; check with dig |
| DKIM record on the wrong host | dkim=none or key not found | Some DNS UIs append the zone name: enter selector._domainkey, not the full name twice |
p=reject on day one | Legitimate mail from a forgotten sender is rejected | Start at p=none, read reports, then tighten |
| Forgetting other senders | Workspace, helpdesk or billing mail fails DMARC once enforced | Inventory every service that sends as your domain; authenticate each before enforcing |
A safe rollout plan
- 1
Inventory every sender
List everything that sends as your domain: Google Workspace or Microsoft 365, your transactional provider, support desk, billing, CRM. Each needs SPF coverage or, better, DKIM signing with your domain.
- 2
Publish SPF and DKIM for your transactional domain
Add the provider's records, verify the domain, and confirm
spf=passanddkim=passwith your domain in a received message. - 3
Publish DMARC at p=none with rua
Collect aggregate reports for two to four weeks, covering monthly jobs like invoices. Fix any legitimate source that fails alignment.
- 4
Move to p=quarantine
Once reports show your legitimate mail passing, quarantine failures. Keep reading reports for new sources.
- 5
Move to p=reject
When quarantine has run cleanly, reject. Receivers that honour DMARC will then refuse mail that spoofs your From domain, and you meet the policy requirement for BIMI if you want it.
Frequently asked questions
Do I need SPF, DKIM and DMARC for transactional email?
Yes. Google requires SPF or DKIM from all senders and all three from bulk senders, Yahoo has matching rules, and unauthenticated transactional mail such as password resets is filtered hard. Set up all three on the domain in your From address. See sender requirements for the details.
Can I have two SPF records?
No. A host can have only one SPF record; two cause SPF to fail with a permanent error for every message. Merge the includes into a single v=spf1 record.
Should I use ~all or -all in SPF?
With DMARC in place, ~all is the safer choice. DMARC decides what happens to failing mail, and -all can make some receivers reject mail on SPF alone before DMARC gets the chance to accept it via DKIM.
What DMARC policy should I start with?
Start with p=none and a rua address so you receive aggregate reports. Once reports show all your legitimate mail passing with alignment, move to p=quarantine and then p=reject.
Why does DMARC fail when SPF and DKIM pass?
Because they passed for a domain that doesn't align with your From address, usually your email provider's own signing or bounce domain. Verify your domain with the provider so DKIM signs with your domain and the return path sits under it.
Does a subdomain need its own SPF, DKIM and DMARC records?
It needs its own SPF record and its own DKIM key if it sends mail. DMARC is inherited from the root domain when the subdomain has no _dmarc record, but you can publish one to give the subdomain its own policy and reports.
Why is my return-path CNAME not working on Cloudflare?
The record is probably proxied. Proxied records answer with Cloudflare's own IP addresses instead of the CNAME target, so set the return-path record to DNS only.
Sources
Facts on this page about other companies' products, prices and policies come from these public pages. They change, so check them before you decide.