DMARC is the least understood of the three email-authentication standards. SPF and DKIM are each a mechanism; DMARC is the policy layer that ties them to your domain and tells receiving servers what to do when they disagree.
The three acronyms
All three are published as DNS records on the domain you send from, and all three are checked by the receiving server, not by yours.
SPF — Sender Policy Framework. Lists the servers allowed to send mail for your domain. The receiver checks the IP address that connected against that list.
DKIM — DomainKeys Identified Mail. Signs each message with a private key whose public half is published in your DNS. Proves the message was authorised by the signing domain and has not been altered since.
DMARC — Domain-based Message Authentication, Reporting, and Conformance. Ties the other two to the From address a human actually reads, states what
receivers should do when neither backs it up, and asks them to report what they
saw.
The last two words of DMARC’s name are the two things it adds. Conformance is the policy — none, quarantine, reject — and it is worth nothing without
alignment, the rule the next two sections are about. Reporting is the part
people skip and then regret: without it you are guessing at which of your own
senders you are about to break.
This guide takes those one-line summaries as its starting point and gets into how SPF and DKIM actually interact under DMARC, what every record tag does, and the specific ways real-world mail breaks when you get it wrong.
What DMARC actually protects
DMARC protects your domain from being spoofed in the From header. It does
not encrypt mail, protect privacy, or stop inbound spam. A receiving server that
implements DMARC checks two things about every message that claims to be from
your domain:
- Does it pass SPF or DKIM?
- Does whichever one passed align with the domain in
From?
Only if both questions are answered yes does the message pass DMARC. If it fails, the receiver applies the policy you published.
The alignment problem
This is the part most people get wrong. SPF and DKIM passing is not enough —
they must align with the From domain.
- SPF authenticates the envelope sender (
RFC5321.MailFrom, theReturn-Path). A message can pass SPF for one domain while itsFromheader says a completely different domain. - DKIM authenticates via a header signature with a
d=domain. Thed=domain is whatever domain signed the message — again, not necessarily theFromdomain.
Alignment is the rule that the From domain must match the domain that
actually passed. Without alignment, an attacker could pass SPF with their own
domain and forge your From header.
DMARC defines two alignment modes, set independently for SPF and DKIM:
- Relaxed (default): the organizational domain must match.
mail.example.comaligns withexample.com. - Strict: the domains must match exactly.
mail.example.comdoes not align withexample.com.
Alignment is the entire reason DMARC exists. SPF and DKIM alone have been around
since the early 2000s; spoofing kept working because nothing forced the From domain to match either of them.
The record
DMARC is published as a TXT record at _dmarc.example.com. A full, working
record looks like this:
_dmarc.example.com TXT "v=DMARC1; p=reject; rua=mailto:dmarc@example.com; adkim=s; aspf=s; sp=reject; fo=1" Every tag, in order of importance:
| Tag | Meaning | Notes |
|---|---|---|
v=DMARC1 | Version, must be first | Literally always DMARC1 |
p= | Policy: none, quarantine, or reject | Required. The whole point |
sp= | Subdomain policy | Defaults to p= if omitted |
adkim= | DKIM alignment: r or s | r = relaxed (default) |
aspf= | SPF alignment: r or s | r = relaxed (default) |
rua= | Aggregate report address | mailto: required, comma-separated |
ruf= | Forensic report address | Often ignored by receivers |
fo= | Forensic reporting options: 0, 1, d, s | Only matters if ruf= is set |
pct= | Percentage of mail the policy applies to | Widely ignored; avoid relying on it |
ri= | Report interval in seconds | Default 86400 (daily) |
The three policies
p=none — monitoring only. Receivers send you reports but deliver mail
normally regardless of DMARC result. This is where you start.
p=quarantine — failing mail goes to spam/junk instead of the inbox.
Receivers interpret “quarantine” loosely; some treat it as reject.
p=reject — failing mail is bounced outright. This is the end state for a
domain you fully control and have tested.
The correct progression is none → quarantine → reject, and each step
should last long enough to read the aggregate reports and confirm legitimate
mail isn’t failing. Jumping straight to reject is the single most common way
people break their own outbound mail.
How evaluation works, step by step
When a receiver gets a message claiming to be from example.com:
- Run SPF: does the connecting IP match the sender’s SPF record? If yes, note the envelope domain.
- Run DKIM: does the
DKIM-Signaturevalidate? If yes, note thed=domain. - Check SPF alignment: does the envelope domain align with the
Fromdomain (under youraspfmode)? - Check DKIM alignment: does the
d=domain align withFrom(underadkim)? - DMARC passes if SPF passes AND aligns, OR DKIM passes AND aligns.
Either mechanism is sufficient. You don’t need both — but you do need at least one to both pass and align. The two checks are independent, and each one has to clear the same second gate:
- Does the connecting IP match the SPF record?
- Does the envelope domain align with
From?
Breaks on forwarding: the server that connects is the forwarder, not yours, and it will not be in your record.
- Does the
DKIM-Signaturevalidate? - Does the
d=domain align withFrom?
Survives forwarding, because the signature travels with the message. Breaks if anything modifies what was signed.
p= policy is for — and
the report in the next section shows Path A failing while Path B carries the
message through.What a report actually looks like
That is the theory. Here is one day of it, for this site’s own domain.
Aggregate reports arrive as gzipped XML, one per receiver per day, to whatever
address you put in rua=. Nobody reads them by hand at volume — but reading one
by hand, once, is the fastest way to understand what the reports are telling you.
This is a real report from Google for cripta.to, in full, split into its three
parts.
Part one: who is reporting, and what they think your policy is
<report_metadata>
<org_name>google.com</org_name>
<email>noreply-dmarc-support@google.com</email>
<report_id>7607524531478255721</report_id>
<date_range>
<begin>1786752000</begin>
<end>1786838399</end>
</date_range>
</report_metadata>
<policy_published>
<domain>cripta.to</domain>
<adkim>r</adkim>
<aspf>r</aspf>
<p>quarantine</p>
<sp>quarantine</sp>
<pct>100</pct>
<np>quarantine</np>
</policy_published> date_range is in Unix seconds, which catches everyone the first time — date -r 1786752000 on macOS, date -d @1786752000 on Linux. It is one UTC day.
policy_published is the more useful half, and it is the part people skim past:
it is your DMARC record as the receiver actually parsed it. If you published p=reject and this says quarantine, your DNS change has not propagated, or you
have a second record shadowing it, or a tag is malformed and was dropped. It is
the only feedback loop that tells you what the world sees, rather than what you
think you published. Note np= too — the policy for subdomains that do not
exist at all, a later addition to the standard that Google echoes back whether or
not you set it.
Part two: a message that passed
<record>
<row>
<source_ip>46.224.237.87</source_ip>
<count>1</count>
<policy_evaluated>
<disposition>none</disposition>
<dkim>pass</dkim>
<spf>pass</spf>
</policy_evaluated>
</row>
<identifiers>
<header_from>cripta.to</header_from>
</identifiers>
<auth_results>
<dkim>
<domain>cripta.to</domain>
<result>pass</result>
<selector>v1-rsa-20260517</selector>
</dkim>
<spf>
<domain>cripta.to</domain>
<result>pass</result>
</spf>
</auth_results>
</record> Each <record> is a group of messages that shared a fate, not a single message — count is how many. source_ip is the server that connected to Google.
This one is the boring case, which is the case you want: our own mail server,
both mechanisms passing, both aligned with the From domain. disposition: none means Google did nothing to the message, because there was nothing to do.
Part three: a message where SPF failed, and it was delivered anyway
<record>
<row>
<source_ip>2a00:1450:4864:20::348</source_ip>
<count>1</count>
<policy_evaluated>
<disposition>none</disposition>
<dkim>pass</dkim>
<spf>fail</spf>
</policy_evaluated>
</row>
<identifiers>
<header_from>cripta.to</header_from>
</identifiers>
<auth_results>
<dkim>
<domain>cripta.to</domain>
<result>pass</result>
<selector>v1-rsa-20260517</selector>
</dkim>
<spf>
<domain>cripta.to</domain>
<result>softfail</result>
</spf>
</auth_results>
</record> This is the record worth understanding, and it demonstrates three things the rest of this guide only asserts.
The source is not our server. 2a00:1450:… is Google. Something sent, or
more likely forwarded, a message carrying our From address through Google’s
infrastructure. That IP is not in our SPF record and never will be.
SPF failed and the message was delivered. DKIM passed and aligned, which is
sufficient on its own — the signature travelled with the message and survived
the trip, exactly as the “forwarding breaks SPF” section below describes. disposition: none under a published policy of quarantine is DMARC working,
not DMARC being lenient. Had we been relying on SPF alone, this message would
have been quarantined.
policy_evaluated and auth_results disagree on purpose. auth_results reports the raw SPF result — softfail, because our SPF record ends in ~all. policy_evaluated reports the result after DMARC alignment, and there it is
plain fail. That is the “softfail is invisible to DMARC” gotcha below, visible
in a real file: the distinction you tuned so carefully is flattened to pass-or-fail
one element up.
What to do with them
The question a report answers is not “am I being spoofed”. It is “which of my own senders would break if I tightened the policy”. Work down the records:
- A source you recognise, everything passing. Nothing to do.
- A source you recognise, one mechanism failing. Fix that mechanism before you tighten. This is the CRM you forgot to add to SPF, or the marketing platform that is not signing with your domain.
- A source you do not recognise, DKIM passing. Almost always a forwarder. Harmless, and the reason DKIM matters more than SPF.
- A source you do not recognise, everything failing. Either a sender you have forgotten about — which is the common case, and the reason to look before tightening — or someone spoofing you, which is what the policy is for.
When every source you recognise passes, and has for a few weeks, you can move to the next policy. That is the entire loop. Everything else in the tooling is a nicer rendering of these two questions.
The gotchas
These are the failure modes that actually bite people in production.
SPF’s 10-lookup limit
SPF is limited to 10 DNS lookups per message (RFC 7208). Every include:, a, mx, ptr, and exists mechanism counts, as does redirect=. Nested include: chains count too — a record that includes a record that includes
three more is four lookups. ip4: and ip6: are free, which is what makes
flattening work.
Exceeding the limit returns permerror, which receivers treat as SPF failure.
The symptom is confusing: SPF “passes” in every online validator but fails for
real mail. If you use many SaaS senders (CRM, helpdesk, marketing), you will hit
this ceiling, and the only fixes are flattening your SPF record (inlining the
included IPs) or moving senders to a subdomain with its own SPF.
Forwarding breaks SPF
SPF authenticates the connecting server. When a message is forwarded — by a
mailing list, a .forward, or a forwarder like a university alumni address —
the final delivery server is the forwarder, not your authorized server. The
forwarder is not in your SPF record, so SPF fails.
Proper forwarders use SRS (Sender Rewriting Scheme) to rewrite the envelope
sender to their own domain, which fixes SPF but changes the Return-Path. Naive
forwarders don’t, and their forwarded mail fails SPF at the destination.
DKIM is the fix here, because a DKIM signature survives forwarding as long as the forwarder doesn’t modify the signed headers or body.
DKIM breaks when the message is modified
A DKIM signature covers specific headers (h=) and a hash of the body. Anything
that changes those — a mailing list appending a footer, a server rewriting the
subject line, a forwarder adding an unsubscribe header — invalidates the
signature.
If a message passes through a system that modifies it and that system doesn’t
re-sign it, DKIM fails at the destination. This is why mailing lists were the
biggest casualty when large providers moved to p=reject.
Mailing lists and From rewriting
A mailing list that sends a message “from” the original author, with the list’s
own envelope sender, will have SPF fail (wrong connecting server) and DKIM fail
(modified body/footer). The list either has to rewrite the From header to the
list’s own address, or wrap the message in ARC (Authenticated Received Chain,
RFC 8617) so the receiver can verify the original authentication passed at the
list’s boundary.
This is a real, ongoing problem. If you run a list, plan for ARC. If you send through a list, test that your messages still pass DMARC after the list has touched them.
pct= is not a rollback
The pct= tag is supposed to apply the policy to only a percentage of failing
mail, letting you roll out reject gradually. In practice many major receivers
ignore it and apply the full policy. Treat pct= as advisory at best and test
with p=none/p=quarantine instead of trusting a percentage.
ruf= forensic reports mostly don’t arrive
Forensic (failure) reports contain full headers and sometimes body fragments,
so most large receivers stopped sending them over privacy concerns. You’ll get
aggregate (rua=) reports reliably; you will likely get few or no forensic
reports. Build your tooling around aggregate data.
Report addresses and external domains
If your rua= points at an address outside the domain the record belongs to — a
DMARC analytics service, or just a different domain you own — the receiving
domain has to opt in, or reports are silently not sent. This is “external
destination verification” (RFC 7489 §7.1), and it is a specific TXT record on
the destination side:
example.com._report._dmarc.analytics.example TXT "v=DMARC1" Read it as ”analytics.example agrees to accept DMARC reports about example.com”. Analytics services publish this for you when you add a domain to
their dashboard, which is why it usually goes unnoticed until you point rua= at a domain you own yourself and no reports ever arrive.
Reports to an address on the same domain need no such record. They do need a mailbox that actually exists and accepts mail from strangers — reports arrive from the reporting receiver’s own domain (Google, Microsoft, Yahoo), so a restrictive inbound filter can quietly discard them.
Subdomains inherit silently
If you publish DMARC at the organizational domain and don’t set sp=,
subdomains inherit the parent’s p= policy. A marketing subdomain you forgot
about is now bound by p=reject. Be explicit: either set sp= deliberately or
publish a _dmarc record on every subdomain you care about.
Softfail vs hardfail is invisible to DMARC
Whether your SPF record ends in ~all (softfail) or -all (hardfail), DMARC
treats both as SPF failure for alignment purposes. Softfail softens the message
for non-DMARC receivers; under DMARC, a softfail is still a fail.
DMARC doesn’t look at Reply-To
Attackers set a forged From and a Reply-To pointing at themselves. DMARC only
checks From. This is by design — but it’s worth remembering that DMARC stops spoofing your domain in From, not social engineering that abuses other headers.
Best practices
Start at
p=nonewithrua=and read the reports for at least a month before tightening. You cannot see what breaks until real mail flows.Get DKIM right first. DKIM is the mechanism that survives forwarding and mailing lists, so it’s the more durable half of DMARC. Prioritise it over endless SPF tuning.
Use relaxed alignment unless you have a specific reason for strict. Strict is more correct but breaks subdomain senders you may not have mapped.
Keep SPF flat. Monitor your lookup count; flatten before you hit 10. Tools like
dmarcianandmxtoolboxreport your lookup depth.Route third-party senders through their own subdomain (
marketing.,mail.) rather than piling includes into the parent SPF. It isolates their failures from your main domain.Publish DMARC on parked/never-sending domains too, at
p=reject. A domain that never sends mail is the easiest to spoof and the easiest to protect.Don’t treat quarantine as final. Some receivers downgrade
quarantineto inbox. If you need spoofed mail gone, the end state isreject.Re-validate after any DNS or sending change. A new provider, a changed
From, or a new subdomain can silently shift alignment.
Testing it
dig +short TXT _dmarc.example.comto confirm the record is live and correctly formatted.mxtoolbox.com/dmarc.aspxanddmarcian.com/dmarc-inspectorparse your record and flag common mistakes (wrong version order, missingp=, tag typos).- Send test mail to Gmail and Yahoo from every sender you use, then inspect
the
Authentication-Resultsheader the receiver adds. It shows the SPF result, DKIM result, and DMARC result for that exact message. - Read the aggregate reports. They’re gzip-compressed XML and unusable by
hand; point
rua=at a tool (dmarcian, Postmark’s free parser, or a self-hosted one) rather than a mailbox nobody reads.
DMARC is a system that rewards patience and punishes shortcuts. The people who
get burned are almost always the ones who set p=reject on a Friday afternoon
and discovered on Monday that their CRM’s email was being bounced. Set up the
reports, watch them, tighten deliberately — and your domain becomes the one
spoofers avoid.