Concepts / Encryption and trust

Encryption and trust

SPF, DKIM and DMARC

Also called SPF, DKIM, DMARC, email authentication, Authentication-Results, DKIM selector, alignment, email spoofing

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:

  1. 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 at lab._domainkey.north.lab (selector lab), and DMARC at _dmarc.north.lab (v=DMARC1; p=reject; adkim=r; aspf=r).
  2. 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 a DKIM-Signature: header with d=north.lab; s=lab.
  3. 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 finally all. -all means fail, ~all softfail. At most 10 DNS lookups are allowed; more is a permanent error.
  4. 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.
  5. 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 aligned d=? Relaxed alignment (r) accepts the same organizational domain, strict (s) only the exact name. One aligned pass is enough. Otherwise the policy applies: none, quarantine or reject.
  6. The verdict is written down in an Authentication-Results: header (RFC 8601), and reports go to the addresses in the DMARC record's rua= 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>.

See it on real machines

Learn it on real machines.

Every lab is a small network of real Linux machines in your browser. Start with a free account: Google or email, no card.

Open this lab