How SPF and DKIM Became DMARC

A domain with no DMARC record gives a receiving server no instruction about mail that fails authentication, and sends nobody a report when it happens. That is the state most domains I have worked on have been in, including my own. It is the same record whether the domain belongs to a company with a mail team or to one person on a VPS: three or four tags in DNS.

DMARC runs no checks of its own. It reads the results of two older standards, SPF and DKIM, and decides what a receiver should do when neither of them backs up the address in the From line. So choosing a value for p meant understanding those two first.

Here we will explore what each of the three does, in the order they arrived:

  • SPF, which authorises IP addresses against an address the recipient is never shown
  • DKIM, which signs the message with a key published in DNS
  • DMARC, which requires either of those to line up with the visible From line, and reports on what happened

SPF authorises the envelope, not the message

Meng Weng Wong’s Sender Policy Framework went out as RFC 4408 in April 2006, marked Experimental. It took until RFC 7208 in April 2014 to reach Standards Track.

A domain publishes a TXT record listing who may send for it. Mine reads:

v=spf1 include:_spf.mx.cloudflare.net ~all

The check runs against the SMTP envelope, MAIL FROM, which becomes the Return-Path header. That is the address bounces go to, and no mail client displays it. A message can pass SPF with a Return-Path on one domain and a From line on a completely different one.

Two limits matter in practice. The first is the ten DNS lookup cap: every include and a and mx term costs a lookup, and going over the limit is a permanent error rather than a soft failure, so an over-stuffed record fails the whole domain. The second is forwarding. A forwarder connects from its own IP, which is not in the original domain’s record, so SPF fails on the second hop. Cloudflare works around it with the Sender Rewriting Scheme, rewriting the envelope sender to its own domain so the check passes against a record Cloudflare controls.

Resend is not in my root record and does not need to be. Resend sets the Return-Path on the send. subdomain, and that subdomain publishes its own record with include:amazonses.com. Adding Amazon SES to the root would spend one of the ten lookups on a check that never runs there. I nearly did it anyway, having read the root record and concluded the setup was incomplete.

Microsoft proposed a competing scheme in the same month, Sender ID, RFC 4406, also Experimental. It checked the Purported Responsible Address, derived from headers rather than the envelope, which was closer to what DMARC ended up doing. It did not get adopted.

DKIM signs headers with a key published in DNS

Yahoo’s DomainKeys and Cisco’s Identified Internet Mail merged into one specification. DomainKeys itself was published as RFC 4870 in May 2007 and marked Historic on the same day the merged version, DKIM, went out as RFC 4871. RFC 6376 replaced that in September 2011.

The sender signs a chosen set of headers and the body with a private key and attaches a DKIM-Signature header. The receiver reads the public key from DNS at <selector>._domainkey.<domain>. The selector exists so a domain can hold several keys at once for several senders. A domain sending through Resend and routing inbound through Cloudflare carries two, resend._domainkey and cf2024-1._domainkey, the second added by Cloudflare when Email Routing is switched on.

DKIM survives forwarding, because the signature travels with the message and the forwarder’s IP is irrelevant. It does not survive modification. A mailing list appending a footer to the body invalidates the signature, and so does any relay that rewrites a signed header.

The domain DKIM proves is the d= value inside the signature. Like the Return-Path, it need not match the From line.

Neither standard tied back to the visible address

By 2011 a domain had two ways to authenticate mail and three gaps between them.

A pass proved something about an address the reader never sees. Forged mail could pass SPF and DKIM on a throwaway domain while displaying any From address it liked, and both standards would report success.

Nothing reported back. A domain owner could not tell how much mail claimed to be theirs, which of it their own records covered, or whether a new sending tool had been set up without DNS to match.

Nor was there a policy to apply. A receiver watching SPF fail had nothing from the owner saying what to do about it, so the safe choice was to deliver anyway.

DMARC binds the result to the From header

A group of senders and receivers, including PayPal, Yahoo, Google and Microsoft, published the first DMARC specification in 2012. It became RFC 7489 in March 2015, as an Independent Submission marked Informational, which is unusually low standing for something the whole industry ran on.

DMARC adds one requirement and two mechanisms.

The requirement is alignment. It is not enough for SPF or DKIM to pass. The domain that passed has to line up with the domain in the From header. Relaxed alignment, the default, wants the organisational domain to match, so a DKIM d=send.example.com aligns with a From on example.com. Strict alignment wants them identical. Only one of the two needs to align, which is what makes forwarding survivable: SPF breaks on the second hop, DKIM carries through, and the message still passes.

The mechanisms are policy and reporting. p=none, quarantine or reject tells the receiver what to do when nothing aligns. rua names an address for aggregate reports, which arrive as gzipped XML, typically once a day per receiver, counting messages by source IP with the SPF and DKIM outcome for each.

Reporting across domains needs permission, or anyone could name your address in their record and use the world’s mail providers to post you XML. The receiving domain authorises it with a TXT record at <sending domain>._report._dmarc.<receiving domain>. Pointing several domains at one report address means the domain holding that address publishes one of these per sender:

sender.example.net._report._dmarc.example.com   v=DMARC1

What changed in May 2026

DMARCbis landed as three Standards Track documents. RFC 9989 replaces RFC 7489 and RFC 9091, with RFC 9990 covering aggregate reporting and RFC 9991 covering failure reporting.

Three changes worth knowing if you are reading older guides.

The pct tag is gone, described in Appendix A.6 of RFC 9989 as a sampling rate that operational experience showed was usually not what operators wanted. It survives only as historic in the tag registry.

The Public Suffix List is no longer how the organisational domain gets worked out. Section 4.10 defines a DNS tree walk instead, querying up the label chain rather than consulting a file that someone has to keep current.

The np tag, imported from RFC 9091, sets a separate policy for subdomains that do not exist, so a domain can reject mail from billing.example.com without ever having published anything there.

What I set, and why it is p=none

The same record on each domain, four tags:

_dmarc.example.com   v=DMARC1; p=none; rua=mailto:[email protected]; fo=1

p=none because I have not read a single report yet. I know of two senders, Resend and Cloudflare, and I have no evidence that is the full list. My uptime monitor watches 24 endpoints across these domains, several of them set up more than a year ago, and going to quarantine before the reports arrive would quarantine anything down there I have forgotten sends mail. Whoever is doing this at a company has the same problem at a larger size: the list of things authorised to send as the domain is never the list anyone can recite, and the reports are how you find the rest.

One dependency to note, because it fails silently. The rua address is not a mailbox. It resolves through a catch-all forwarding rule on the zone, and if the catch-all is ever switched off the reports stop arriving with no error raised anywhere.