groundmail

Guides

SPF, DKIM and DMARC for transactional email

What SPF, DKIM and DMARC each check, how alignment works, copy-paste DNS records, dig commands to verify them, the common mistakes, and a safe DMARC rollout plan.

By GroundmailUpdated

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

SPFDKIMDMARC
What it provesThe sending server is allowed to send for the envelope domainThe message wasn't altered and was signed by the holder of a domain's keySPF or DKIM passed for a domain aligned with the From address
Domain it checksReturn-path (MAIL FROM)The d= domain in the DKIM-Signature headerThe From: header domain
DNS recordTXT on the envelope domain: v=spf1 ...TXT at selector._domainkey.domainTXT at _dmarc.domain
Survives forwardingUsually not (the forwarder's IP isn't listed)Usually yes, if the body isn't changedYes, if DKIM survives
SpecRFC 7208RFC 6376RFC 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.

ModeTagAligns whenExample with From `example.com`
Relaxed (default)adkim=r, aspf=rSame organizational domaind=mail.example.com aligns; d=example.net doesn't
Strictadkim=s, aspf=sExactly the same domainOnly 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.com needs 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.com record, receivers apply the root domain's policy (its sp= tag if set, otherwise p=). 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.

PurposeTypeHostValue
OwnershipTXT_groundmail.example.comgroundmail-verify=... (from your dashboard)
DKIMTXT<selector>._domainkey.example.comThe public key shown for your domain
Return pathCNAMErp.example.comrp.groundmail.dev (DNS only, not proxied)
SPFTXTexample.comv=spf1 include:spf.groundmail.dev ~all, merged with any existing SPF
DMARCTXT_dmarc.example.comv=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.1

If 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.com
  • dkim=pass with header.i or d= on your domain: DKIM passes and aligns.
  • spf=pass with smtp.mailfrom under your domain: SPF passes and aligns.
  • dmarc=pass with header.from your domain: at least one of the above aligned.
  • dmarc=fail with SPF and DKIM both passing: they passed for someone else's domain. Check the domains, not just the verdicts.

Common mistakes

MistakeWhat happensFix
Two SPF records on one hostSPF permerror for all mail from the domainMerge into one v=spf1 record
More than 10 SPF lookupsSPF permerrorRemove unused includes; move transactional mail to a subdomain
-all before DMARC is trustedSome receivers reject on SPF failure alone, including forwarded mail that DKIM would have savedUse ~all and let DMARC enforce
Return-path CNAME proxied at CloudflareThe CNAME resolves to Cloudflare IPs, so return-path checks and SPF alignment failSet the record to DNS only (grey cloud)
DKIM key truncated, quoted twice or broken by spacesdkim=fail or dkim=permerrorRe-paste the key exactly; check with dig
DKIM record on the wrong hostdkim=none or key not foundSome DNS UIs append the zone name: enter selector._domainkey, not the full name twice
p=reject on day oneLegitimate mail from a forgotten sender is rejectedStart at p=none, read reports, then tighten
Forgetting other sendersWorkspace, helpdesk or billing mail fails DMARC once enforcedInventory every service that sends as your domain; authenticate each before enforcing

A safe rollout plan

  1. 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. 2

    Publish SPF and DKIM for your transactional domain

    Add the provider's records, verify the domain, and confirm spf=pass and dkim=pass with your domain in a received message.

  3. 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. 4

    Move to p=quarantine

    Once reports show your legitimate mail passing, quarantine failures. Keep reading reports for new sources.

  5. 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.

Send from infrastructure we own.

Groundmail runs its own mail servers on its own IP space. Start free on the shared transactional pool, then move to dedicated IPs or a dedicated server without changing your code.