Most DMARC explanations stop at "SPF or DKIM has to pass and align". True, and useless the moment you are staring at a real record trying to work out why a legitimate sender fails. SPF and DKIM each produce an authentication result and an alignment result, the two are independent, and every tag in the DMARC record bends the outcome. This is the map: every combination that decides a pass or a fail, and what each DNS setting does to it.

Three records are in play, all published as DNS TXT records. SPF (RFC 7208) lists the servers allowed to send for a domain. DKIM (RFC 6376) publishes a public key so receivers can verify a signature. DMARC (RFC 9989, which replaced RFC 7489 in May 2026) ties both to the visible From domain and records the domain owner's handling preference for mail that fails.

What "alignment" means. SPF and DKIM each authenticate a domain, but not necessarily the one a person reads in the From line. Alignment is the extra check DMARC adds on top: does the domain that just authenticated actually match the visible From domain? SPF authenticates the envelope domain (from MAIL FROM); DKIM authenticates its signing domain (from d=). Alignment asks whether that domain lines up with From.

"Match" has two definitions, and the DMARC record chooses which. Under relaxed (the default) the two only need to share the same organisational domain, the registrable part like example.com, so a signature from mail.example.com aligns with a From of example.com. Under strict they must be identical, and that subdomain signature no longer aligns.

This is why a message can pass SPF or DKIM yet still fail DMARC: the check passed for a domain that is not the one in From. Every diamond below labelled "aligns with From?" is running exactly this comparison.

The full decision

DMARC evaluates two legs, and each leg authenticates before it aligns.

The SPF leg reads the envelope domain from the SMTP MAIL FROM (formally the RFC5321.MailFrom, the envelope-from that SPF evaluates; it is normally the Return-Path header once the message has been delivered), fetches that domain's SPF record from DNS, walks its lookup-consuming mechanisms (a, mx, include, and others) within a hard 10-lookup limit, and asks whether the connecting IP is authorised. If it is, SPF then aligns: does that envelope domain match the visible From domain?

The DKIM leg reads the selector (s=) and signing domain (d=) from the DKIM-Signature header, fetches the public key from DNS at selector._domainkey.d=, recomputes the hash over the signed headers and body, and asks whether the signature validates. If it does, DKIM then aligns: does d= match the From domain?

A leg counts only when both its authentication and alignment steps pass, and DMARC passes if either leg counts.

flowchart TD
  M[Incoming message on the wire] --> SPFSTART[SPF leg runs independently on the left]
  M --> DKSTART[DKIM leg runs independently on the right]

  SPFSTART --> SPF1[Read envelope domain from SMTP MAIL FROM, the Return-Path]
  SPF1 --> SPF2[Fetch that domain's SPF TXT record from DNS]
  SPF3[Walk its mechanisms; a, mx, include and other lookup-consuming terms count against the 10-lookup limit, ip4, ip6 and all do not]
  SPF2 --> SPF3
  SPF3 --> SPF4{Connecting IP authorised by a mechanism?}
  SPF4 -->|no, or -all match, or permerror| SPFX[SPF leg fails]
  SPF4 -->|yes| SPFAUTH[SPF authenticated]
  SPFAUTH --> SPFMODE[Read aspf from DMARC record; if the tag is absent, default to relaxed]
  SPFMODE --> SPFAL{Envelope domain aligns with From? strict: exact match. relaxed: same organisational domain}
  SPFAL -->|no| SPFX
  SPFAL -->|yes| SPFOK[SPF leg passes]

  DKSTART --> DK1[Read DKIM-Signature header: selector s= and signing domain d=]
  DK1 --> DK2[Fetch public key from DNS at selector._domainkey.d=]
  DK2 --> DK3[Recompute hash of the signed headers and body]
  DK3 --> DK4{Signature validates against the public key?}
  DK4 -->|no, key missing, or body altered| DKX[DKIM leg fails]
  DK4 -->|yes| DKVER[DKIM verified]
  DKVER --> DKMODE[Read adkim from DMARC record; if the tag is absent, default to relaxed]
  DKMODE --> DKAL{Signing d= domain aligns with From? strict: exact match. relaxed: same organisational domain}
  DKAL -->|no| DKX
  DKAL -->|yes| DKOK[DKIM leg passes]

  SPFOK --> DEC{The two legs meet here: did either leg pass?}
  SPFX --> DEC
  DKOK --> DEC
  DKX --> DEC
  DEC -->|yes| PASS([DMARC pass: no DMARC-based handling preference applies])
  DEC -->|no| POL{Domain owner preference from p=}
  POL -->|p=none| MON([Fail recorded in reports, no handling preference requested])
  POL -->|p=quarantine| QUA([Fail: owner asks receiver to treat as suspicious])
  POL -->|p=reject| REJ([Fail: owner asks receiver to refuse the message])

The two legs run in parallel and only meet at the bottom, where DMARC passes if either one passed. Each leg authenticates first, then aligns (the alignment check is defined in the box above). SPF checks the connecting IP against the envelope domain's published record; DKIM validates the signature against the key at selector._domainkey.d=. Only after a leg authenticates does its domain get compared to the visible From. The aspf and adkim tags are optional and default to relaxed when absent, so the alignment step always runs; the tags only pick strict or relaxed. A failing DMARC result does nothing on its own; the p= tag decides the consequence, and subdomains follow sp= if set, otherwise p=.

The nine leaf states of the two legs are worth having in front of you, because the DMARC verdict collapses them to just two outcomes and the difference between a pass and a fail is often a single gate.

SPF auth  SPF align   DKIM verify  DKIM align   DMARC
pass      pass        pass         pass         PASS   (both legs)
pass      pass        pass         fail         PASS   (SPF leg)
pass      pass        fail         -            PASS   (SPF leg)
pass      fail        pass         pass         PASS   (DKIM leg)
fail      -           pass         pass         PASS   (DKIM leg)
pass      fail        pass         fail         FAIL   (neither aligns)
pass      fail        fail         -            FAIL   (SPF unaligned, no DKIM)
fail      -           pass         fail         FAIL   (DKIM unaligned)
fail      -           fail         -            FAIL   (nothing to align)

Row six is the one that generates support tickets: SPF authenticated, DKIM verified, and DMARC still failed, because neither authenticated domain lined up with the From address. "SPF pass, DMARC fail" is almost always this. The pass you saw was SPF passing for the ESP's own envelope domain, which does not align with yours.

What alignment mode changes: adkim and aspf

Alignment has two modes, set independently for each leg. adkim controls DKIM alignment, aspf controls SPF alignment, and both default to relaxed (r). In relaxed mode the two domains need only share an organisational domain: a signature from d=mail.example.com aligns with a From of example.com, and an envelope of bounce.example.com aligns too. In strict mode (s) they must be identical, so both of those now fail alignment.

_dmarc.example.com. TXT "v=DMARC1; p=reject; adkim=s; aspf=r; rua=mailto:rua@example.com"

That record demands exact DKIM alignment but allows relaxed SPF. A subdomain signature that passed yesterday under the default can start failing the moment someone adds adkim=s. Strict mode does not stop an attacker on a lookalike domain, since they align their own domain to their own From trivially, so its practical effect is mostly on your own legitimate subdomain senders. Relaxed is the sensible default for nearly everyone; reach for strict only when you have a specific reason and you know every sending subdomain.

The policy tags: p, sp, np, and t

Every record opens with v=DMARC1. It is required, must come first, and is the only value the tag takes; a receiver that does not find it treats the record as not-a-DMARC-record and moves on.

p= records the domain owner's handling preference and changes nothing about whether DMARC passes or fails; it only expresses what the owner would like done after a fail. p=none requests no special handling, so failures are recorded in aggregate reports and the receiver applies whatever it would have done anyway. It is not a delivery guarantee: nothing about DMARC forces a message into the inbox, and a none message can still be filtered on other grounds. p=quarantine asks receivers to treat failures as suspicious, which in practice usually means the spam folder. p=reject asks them to refuse the message outright. DMARC only supplies authentication results and a handling preference; the receiver retains local policy and may apply its own exceptions, so every value is a request, not an instruction.

sp= sets the preference for existing subdomains of the organisational domain, overriding p for them; leave it out and existing subdomains inherit p. RFC 9989 splits off a separate case: np= sets the preference for non-existent subdomains, ones that return NXDOMAIN, and falls back to sp (then p) when absent. The split matters because attackers spoof subdomains you never registered; np=reject closes that door without waiting on the parent policy. This also catches people during a staged rollout: publish p=reject with no sp, and every existing subdomain, including ones you forgot send mail, jumps straight to reject, so sp=none keeps that mail flowing while you check it.

One tag you set only if you run a public suffix: psd=. A Public Suffix Domain is one under which independent organisations register their own names, the way .org sits above ietf.org. A public suffix operator publishes psd=y so its record is recognised as sitting above the organisational level rather than being an ordinary domain; the default psd=u ("unknown") is right for everyone else, and psd=n explicitly marks an ordinary organisational domain. If you are not a registry, leave it alone.

t= is the testing flag (t=n by default). t=y tells validators the owner is still testing and does not want the stated p/sp/np policy actually applied yet, so failures are reported but handled one level below the declared policy. It is the modern replacement for the old percentage tag.

_dmarc.example.com. TXT "v=DMARC1; p=reject; sp=none; np=reject; adkim=r; aspf=r; t=y; rua=mailto:rua@example.com"

The pct tag is now historic

On records following the old RFC 7489, pct applied the policy to only a percentage of failing mail, the rest dropping to the next-weaker action (a pct=25 under p=reject quarantined the other 75 percent). It was a rollout throttle. RFC 9989 removed it, replacing the whole idea with the explicit t=y testing flag above, so you will not find pct on current-standard records, though plenty of older published records still carry it and receivers still honour it there.

The reporting tags: rua, ruf and fo

rua is the address for aggregate reports, whose format now lives in its own document (RFC 9990): periodic XML summaries of volume and pass or fail counts per source, which is the data you actually use to reach enforcement safely. ruf is the address for failure reports (RFC 9991): per-message forensic copies, far more sensitive and far less widely sent by receivers, so most operators leave it off. fo tunes when a failure report is generated (fo=1 means "report if any underlying check failed to align", which is more useful than the fo=0 default that only reports when both fail). If you set fo without setting ruf, nothing happens, because the tag is ignored when there is no failure-report address to send to.

Sending an aggregate report to a third party's domain needs that domain's permission, published as its own DMARC-style authorisation record. If rua=mailto:you@reports.vendor.com points off your organisational domain, reports.vendor.com must publish example.com._report._dmarc.reports.vendor.com TXT "v=DMARC1" or the receiver may decline to send. A missing external authorisation record is a common reason reports never arrive despite a valid rua.

The SPF qualifier is a separate lever: -all versus ~all

One setting that decides SPF's outcome lives in the SPF record, not the DMARC record. The qualifier before all sets what happens to an IP that is not listed. -all (hard fail) says anything unlisted is not us. ~all (soft fail) says probably not us, but do not hard-fail it. For DMARC's SPF leg the practical difference is small, since DMARC treats both a hard and a soft SPF fail as "SPF did not pass", but for systems that evaluate SPF on its own, outside DMARC, the qualifier matters. A related trap: SPF allows at most 10 DNS-lookup terms (RFC 7208 section 4.6.4). The terms that consume a lookup are include, a, mx, ptr, exists, and the redirect modifier; ip4, ip6, and all resolve without a lookup and cost nothing. Nest a few includes that each pull in more and you cross 10, the record returns permerror, and DMARC counts that as an SPF failure regardless of what the qualifier says.

example.com. TXT "v=spf1 include:_spf.google.com include:sendgrid.net -all"

Reading it back out of a report

When you open an aggregate report, keep two elements apart. auth_results records the raw SPF and DKIM outcomes on their own terms: auth_results/spf/domain is the envelope domain that was checked, auth_results/dkim/domain is the d= that verified. policy_evaluated records the aligned-for-DMARC result: policy_evaluated/spf and policy_evaluated/dkim, each pass only if that leg both authenticated and aligned. A source showing auth_results pass but policy_evaluated fail is row six: the check passed, the alignment did not. The domain to compare against is identifiers/header_from.

So the practical decision, if you are about to touch a record: before adding adkim=s or aspf=s, pull the last few aggregate reports and confirm every legitimate source aligns on the leg you are about to tighten, because strict mode fails aligned-relaxed subdomain mail silently. And before publishing p=reject, set sp=none unless you have confirmed every subdomain sender, so the policy does not reach senders you have not checked yet. Per-source alignment is exactly what a sender inventory is for; SenderLedger breaks down each sending source by which leg aligned, so the ESP that verifies DKIM under its own d= shows up before a policy change turns it into rejected mail.

Know which leg aligns for every sender before you tighten a record.

SenderLedger maps SPF and DKIM alignment per source and tells you what a change to p, sp or adkim would actually break.

Request access