Encryption and trust
SPF, DKIM and DMARC
Three DNS-published policies that let a receiving mail server check that a message really comes from the domain it claims.
SPF (a TXT record on the domain, v=spf1 ip4:… -all) lists the addresses allowed to send for the
envelope sender's domain. DKIM signs chosen headers and a hash of the body; the public key is a TXT record at
<selector>._domainkey.<domain>, and any change to the signed content breaks it. DMARC (_dmarc.<domain>) ties
both to the From: line the reader sees: a check counts only if it passed for an aligned domain, and the policy
says what to do when none did: none (report), quarantine (junk) or reject. The receiver records its verdict
in an Authentication-Results header. A valid signature proves the content, not who sent this copy (a replay).
How SPF, DKIM and DMARC work, step by step
In the SPF, DKIM and DMARC lab alice sends from mailNorth (10.0.1.11,
north.lab) to bob at mailSouth (10.0.2.11, south.lab). The path of an email lab shows the
plain relay; here three checks are added on top of SMTP:
- The sender publishes policies in DNS, as TXT records: SPF at
north.lab(v=spf1 ip4:10.0.1.11 -all), the DKIM public key atlab._domainkey.north.lab(selectorlab), and DMARC at_dmarc.north.lab(v=DMARC1; p=reject; adkim=r; aspf=r). - The sending server signs (DKIM, RFC 6376). It hashes the body (
bh=), then signs that hash with the listed headers (h=) using its private key, after relaxed canonicalization (case and spacing normalized). The result goes on top of the message as aDKIM-Signature:header withd=north.lab; s=lab. - The receiver checks SPF (RFC 7208) during the SMTP conversation. It takes the domain of the envelope sender
(
MAIL FROM), reads its SPF record, and tests the connecting address against each term in order:ip4,a,mx,include, and finallyall.-allmeans fail,~allsoftfail. At most 10 DNS lookups are allowed; more is a permanent error. - The receiver checks DKIM. It fetches the key at
s._domainkey.d, recomputes the body hash and verifies the signature. Any change to the body or a signed header fails it. - DMARC decides (RFC 7489). It takes the domain in the visible
From:line and asks: did SPF pass for an aligned envelope domain, or DKIM for an alignedd=? Relaxed alignment (r) accepts the same organizational domain, strict (s) only the exact name. One aligned pass is enough. Otherwise the policy applies:none,quarantineorreject. - The verdict is written down in an
Authentication-Results:header (RFC 8601), and reports go to the addresses in the DMARC record'srua=tag, if any.
What it looks like on a Linux machine
The policies are ordinary DNS records. The 2048-bit key is too long for one 255-character TXT string, so it comes as two, which readers join:
$ dig +short TXT north.lab; dig +short TXT _dmarc.north.lab
"v=spf1 ip4:10.0.1.11 -all"
"v=DMARC1; p=reject; adkim=r; aspf=r"
$ dig +short TXT lab._domainkey.north.lab
"v=DKIM1; k=rsa; p=MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAnCwju2KSZksk2zNq…" "…"
Postfix on mailSouth hands each message to a checker (a short script doing what opendkim and opendmarc do), which logs one line per message. alice's real mail:
$ tail -n 1 /var/log/netlab-mail-check.log
09:41:07 client=10.0.1.11 mailfrom=<alice@north.lab> from=north.lab spf=pass smtp.mailfrom=north.lab; dkim=pass header.d=north.lab; dmarc=pass (p=reject) header.from=north.lab -> inbox
A forger on the outside (10.0.3.10) sends "New bank details" from alice@north.lab. The lab drops it after
SMTP; a real receiver refuses it during SMTP with 550 5.7.1:
09:42:15 client=10.0.3.10 mailfrom=<alice@north.lab> from=north.lab spf=fail smtp.mailfrom=north.lab; dkim=none header.d=-; dmarc=fail (p=reject) header.from=north.lab -> rejected
bob's copy of the real message carries the verdict (Authentication-Results: mailSouth.lab; spf=pass …) and the
signature's bh=. Recomputing the body hash shows what DKIM compares:
$ sed '1,/^\r$/d' /tmp/signed.eml | openssl dgst -sha256 -binary | base64
Its output equals the bh= value while the body is untouched.
What goes wrong, and how you see it
The server moved, SPF did not. Mail now leaves from an address the SPF record does not list: spf=fail.
If the signer was also lost, dkim=none, and p=reject drops every message. Update SPF whenever a sending
address changes, and keep DKIM signing so one check still passes.
SPF passes for the wrong domain. The forger uses its own envelope sender, bounce@outside.lab, whose SPF lists
its own server: spf=pass smtp.mailfrom=outside.lab. DMARC still fails, because outside.lab is not aligned
with From: north.lab. Alignment is the point of DMARC.
The body changed in transit. A footer added by a mailing list, or an edit by an attacker, breaks the hash:
dkim=fail (body hash differs: the body changed after signing). Forwarding also breaks SPF, since the forwarder's
address is not listed; ARC (RFC 8617) exists to carry the original results across such hops.
A signed message is replayed. Someone with access to one real message sends it again, unchanged, from their own
server. SPF fails, DKIM passes, DMARC passes, and it lands in the inbox. The client= address and the newest
Received: line show where this copy really came from.
The policy only watches. With p=none a failing forgery is delivered. Move to quarantine, then reject,
once reports show every real sender passing.
Questions people ask
Do I need all three? Yes. SPF alone breaks on forwarding, DKIM alone does not say what to do with a
failure, and neither checks the From: line the reader sees. DMARC needs at least one of them to pass, aligned.
What does "dmarc=fail" with "spf=pass" mean? SPF passed for the envelope domain, but that domain does not match the visible From domain. Typical of a forger using its own domain, or a third-party sender not set up to sign as you.
Where are SPF, DKIM and DMARC stored? All three are DNS TXT records: SPF on the domain
itself, DKIM at <selector>._domainkey.<domain>, DMARC at _dmarc.<domain>.