Chapter 10 · Part II · Defending the network

Email: SPF, DKIM, DMARC & phishing

Email was designed in an age when every machine on the network was run by someone you could phone. The protocol that still carries it asks the sender to say who they are and then simply believes the answer. This chapter is about the three DNS records that let a receiving server check that claim, the transport fixes that stop a message being read or redirected on the way, and the human layer that remains when an attacker registers a name of their own.

10the most DNS-querying terms (include, a, mx, ptr, exists, redirect) an SPF check may evaluate before it must stop with an error (RFC 7208, section 4.6.4)
3DMARC policies a domain can publish: none (watch), quarantine (spam folder) and reject (refuse), defined in RFC 7489 (2015) and kept by its replacement, RFC 9989 (May 2026)
1,024 bitsthe smallest RSA key a DKIM signer may use under RFC 8301 (2018), which also says signers should use at least 2,048

Common mix-up: SPF does not check the From address you see in your mail app. It checks a different, hidden address in the envelope, which the sender is free to set to any domain whose SPF record they control. A message can "pass SPF" while displaying someone else's name and address. Only DMARC ties the checks to the address a human actually reads.

Read first: DHCP & DNS on networks.jelia.nyc explains names, record types and how a resolver finds an answer. Every control in this chapter is a TXT record a receiving mail server looks up, so we assume all of that and go straight to what the records say. Whether those DNS answers can themselves be trusted is the subject of chapter 07, DNS security.

How a message travels

Two addresses on every message

Mail moves between servers using SMTP, the Simple Mail Transfer Protocol, defined today by RFC 5321. A sending server opens a TCP connection to the receiving domain's mail exchanger (its MX record) and holds a short, typed conversation. It announces itself with EHLO, says who the message is from with MAIL FROM, says who it is for with RCPT TO, and then sends the message itself after DATA.

The message it sends is a separate thing with its own format, RFC 5322: a block of header lines (From:, To:, Subject:, Date: and many more), a blank line, and the body. The header has its own From line. Nothing in SMTP requires the two From addresses to match, and there are good reasons they often do not.

The best way to picture it is a paper letter. The envelope is what the postal system reads to deliver it and to return it if delivery fails. The letter inside is what the recipient reads. A company's invoice might arrive in an envelope from its billing contractor, with a return address at the contractor's mailroom, while the letterhead inside says the company's name. Both are legitimate. And nobody at the sorting office opens the envelope to check the letterhead.

Envelope · RFC 5321 · read by servers
Connecting IP198.51.100.20 (a newsletter service)MAIL FROM<bounces-7731@example.net>RCPT TO<pat@example.org>

Used for routing and for returning bounces. Often called the return path, the 5321.MailFrom, or the envelope sender. Your mail app never shows it.

Message · RFC 5322 · read by people
From:Example Co <news@example.com>To:pat@example.orgSubject:October update

The 5322.From. This is the only sender most people ever see, and SMTP never checked it.

Because the envelope is all the transport layer looks at, the original protocol has no idea whether the header From is true. A server could connect from anywhere, give any envelope sender, and write any header it likes. That is the single design fact the rest of this chapter answers: spoofing a From line costs nothing, so receivers need evidence from somewhere other than the message.

The evidence they use is published in DNS by the owner of the domain, because only the owner can write there. There are three records, each answering a different question.

SPF · RFC 7208 · envelopeWhich servers may send?

A list of the IP addresses allowed to send mail using a domain in the envelope MAIL FROM. Checked against the connecting server's address.

DKIM · RFC 6376 · signatureWas it signed, and unchanged?

A cryptographic signature over the body and chosen headers, made by a key whose public half is published under the signing domain.

DMARC · RFC 9989 · policyDoes it match what people read?

Requires that a passing SPF or DKIM domain aligns with the header From, says what to do when nothing does, and asks for reports.

Sender Policy Framework

SPF: the list of approved vans

SPF lets a domain publish, as a TXT record at its own name, the set of servers allowed to use it in the envelope. When a message arrives, the receiver takes the domain from MAIL FROM (or from the EHLO name if MAIL FROM is empty, as it is for bounces), fetches that domain's record, and walks through it left to right, asking at each term "does the connecting IP match?" The first match decides the result.

example.com.  TXT  "v=spf1 mx ip4:192.0.2.0/28 include:_spf.example.net -all"

Read aloud, that record says: the servers named in example.com's MX records may send; so may anything in 192.0.2.0 to 192.0.2.15; so may anything that example.net's own list approves; and everything else should fail. Each term is a mechanism with an optional qualifier in front of it.

TermMatches when the sender's IP…DNS lookups
ip4:192.0.2.0/28falls inside that IPv4 range (ip6: for IPv6)0
ais one of the A/AAAA addresses of the domain (or a named one)1
mxis an address of one of the domain's MX hosts1, plus up to 10 address lookups
include:dwould get pass from domain d's own SPF record1, plus whatever d's record costs
exists:dany A record exists at d (used with macros)1
redirect=d(modifier) if nothing matched, use d's record instead1
allalways matches; it belongs at the end as the default0

The qualifier says what a match means: + (the default, usually left out) gives pass, - gives fail, ~ gives softfail (probably not authorized, but do not reject on this alone) and ? gives neutral (no statement). So -all at the end means "nothing else is ours", and ~all means "nothing else should be ours, but go easy". A domain with no record at all gets none, and a broken record gets permerror.

Two limits catch real domains every day. First, an SPF evaluation may use at most 10 terms that cause DNS lookups, counted across every nested include, or the result is permerror. Every third-party service tells customers to "add include:their-domain", each include often contains more includes, and a growing company quietly crosses ten. Second, a domain must have exactly one SPF record. Two records starting v=spf1 is itself a permerror, and it is the most common way a well-meaning second team breaks the first team's work.

SPF's deep weakness is forwarding. If pat@example.org forwards their mail to another mailbox, the forwarding server re-sends the message from its own IP, usually keeping the original envelope sender. The final receiver checks example.com's record, finds the forwarder's address nowhere in it, and SPF fails, through no fault of anyone. SPF authenticates the last hop, not the author.

Go deeper: how the walk really works, and the void-lookup limit

RFC 7208 defines a function, check_host(ip, domain, sender), that returns one of seven results: none, neutral, pass, fail, softfail, temperror or permerror. An include: is a recursive call. Only a pass from the inner call counts as a match; fail, softfail and neutral inside an include simply mean "no match here, keep going". An inner none (the included domain has no record) is a permerror for the whole check, because the publisher pointed at something that does not exist.

Besides the limit of 10 lookup terms, the RFC says implementations should allow no more than two "void lookups", queries that return no records at all. This stops a record full of dead names from generating endless failed queries. The mx mechanism also may not resolve more than 10 address records per MX lookup.

The ptr mechanism, which asks reverse DNS for the sender's name, is still in the grammar but the RFC says it should not be used: it is slow, puts load on someone else's reverse zone, and proves little. Most receivers treat it as a smell.

Domains that never send mail should say so explicitly: "v=spf1 -all". Parked and defensive domains are popular spoofing targets precisely because nobody watches them.

DomainKeys Identified Mail

DKIM: a wax seal checked against DNS

DKIM moves the proof from the network into the message. The sending system computes a digital signature over the body and over a list of header fields it chooses, and adds it as a DKIM-Signature: header. Any receiver can check that signature using a public key the signing domain publishes in DNS. If the key verifies, two things are true: someone holding that domain's private key signed this message, and the signed parts have not changed since.

DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=example.com; s=sel1;
    h=from:to:subject:date; bh=<base64 SHA-256 of the body>; b=<base64 signature>

sel1._domainkey.example.com.  TXT  "v=DKIM1; k=rsa; p=<base64 public key>"

The d= tag names the signing domain and s= names a selector. The receiver joins them to find the key: selector, then the fixed label _domainkey, then the domain. Selectors let one domain have many keys at once, one for its own servers, one for each email vendor, and let it rotate keys by publishing a new selector before retiring the old one.

The signature happens in two steps. First the body is canonicalized and hashed: that hash goes in bh=. Then the chosen headers, plus the DKIM-Signature header itself with b= left empty, are canonicalized and signed with the private key; that signature goes in b=.

bh = Base64( SHA-256( canon(body) ) )
The body hash is a fingerprint of the message text after it has been tidied into a standard form. Change one visible character and the fingerprint changes completely, so the receiver's recomputed bh no longer matches.

Canonicalization exists because mail servers fiddle. Some rewrap long header lines, some trim trailing spaces, some convert tabs. The simple algorithm tolerates almost nothing. The relaxed algorithm, used by nearly everyone, lowercases header names, unfolds wrapped headers, collapses runs of spaces and tabs into one space, deletes whitespace at the ends of lines, and ignores empty lines at the end of the body. The instrument below implements relaxed body canonicalization and real SHA-256, so you can watch what does and does not break a body hash.

Unlike SPF, a DKIM signature travels with the message. Plain forwarding, which re-sends the message unchanged, leaves it valid. What breaks it is editing. Mailing lists are the classic case: they add "[list-name]" to the subject and an unsubscribe footer to the body, which changes both a signed header and the body hash.

Go deeper: keys, the l= tag, and what DKIM does not promise

RFC 8301 (2018) retired SHA-1 for DKIM and set the key floor: signers must use RSA keys of at least 1,024 bits and should use at least 2,048. RFC 8463 added Ed25519 keys, which are much shorter. Old 512-bit keys are factorable with ordinary computing and must not be treated as valid.

The optional l= tag tells the verifier to hash only the first so-many bytes of the body. It was meant to survive list footers, but it lets anyone append content after the signed part and keep a valid signature. The RFC warns about this; signers should not use l=, and receivers can treat it with suspicion.

Sign the headers that matter to a reader, at least From, Subject, Date, To and any Reply-To. A good practice from RFC 6376 is to list From (and other important headers) one more time than they occur, so that adding a second, unsigned copy of the header later also breaks the signature.

DKIM proves that a domain took responsibility for a message. It says nothing about whether that domain is the one in the From line. A spammer can sign perfectly with a domain of their own. That gap is what DMARC closes.

Domain-based Message Authentication, Reporting and Conformance

DMARC: the seal must match the name you read

SPF and DKIM each authenticate a domain, but not necessarily the one in the header From. DMARC adds the missing link, called alignment. A message passes DMARC if at least one of these is true:

DMARC pass = (SPF pass ∧ MAIL FROM domain aligned with From) ∨ (DKIM pass ∧ d= domain aligned with From)
You need one authenticated identifier that agrees with what the reader sees. Either path is enough, which is why DKIM rescues forwarded mail that SPF fails.

Alignment comes in two strengths. Relaxed (the default) means the two domains share the same organizational domain: mail.example.com and example.com align. Strict means they must be identical. The domain owner chooses separately for SPF (aspf=) and DKIM (adkim=).

The record lives at _dmarc. in front of the From domain, and it is also where the owner says what to do with mail that fails:

_dmarc.example.com.  TXT  "v=DMARC1; p=quarantine; pct=25; adkim=r; aspf=r; rua=mailto:dmarc-reports@example.com"
TagMeaningNotes
p=nonemonitor only; deliver as usualwhere every rollout starts, and where too many stay
p=quarantinetreat failing mail as suspiciousin practice, the spam or junk folder
p=rejectrefuse failing mail during the SMTP conversationthe goal for a domain whose senders are all aligned
sp=policy for subdomainsdefaults to p; attackers like forgotten subdomains
pct=apply the policy to this percentage of failing mailthe rest is treated one step softer: reject becomes quarantine, quarantine becomes none
rua=where to send daily aggregate reportsXML summaries: source IPs, volumes, SPF/DKIM/alignment results
ruf=where to send per-message failure reportsfew receivers send these, for privacy reasons

The reports are what make DMARC safe to deploy. With p=none and a rua address, every large receiver tells you, each day, which IP addresses sent mail claiming your domain and whether it aligned. That is how a domain owner discovers the payroll vendor, the survey tool and the forgotten office printer that all send "as" the company, and fixes them before turning on enforcement.

Receivers record what they found in an Authentication-Results: header (RFC 8601), which is the first place to look when a message lands in the wrong folder. The mail authenticator below writes one for each message.

Go deeper: organizational domains and the Public Suffix List

"Same organizational domain" sounds simple until you meet names like example.co.uk, where the registrable domain is three labels, not two. RFC 7489 finds the organizational domain by consulting a public suffix list: the longest public suffix (such as co.uk), plus one more label. If there is no DMARC record at the exact From domain, the receiver also looks at the organizational domain's record and applies its sp= policy. The simulator on this page uses the simpler "last two labels" rule, which is right for every name it contains.

In May 2026 the IETF finished that revision ("DMARCbis"): RFC 9989 is now the DMARC standard and obsoletes RFC 7489. Two changes touch this chapter. The public suffix list is replaced by the "DNS Tree Walk" (section 4.10), which finds the organizational domain by querying _dmarc records up the name instead of consulting a list. And the pct sampling tag is removed (appendix A.6); a new t=y test flag covers the use people actually made of it. The simulator on this page still models the RFC 7489 behavior, pct included, because that is what most receivers ran until now. Alignment, policy and reporting, the three ideas, stay the same.

Instrument 1

The mail authenticator

This is a small receiving mail server that runs in your browser on made-up data. On the left is a DNS zone for the documentation domains; on the right, a message as it arrives. The tool evaluates SPF exactly as RFC 7208 describes, with nested includes and the 10-lookup counter; recomputes the DKIM body hash with relaxed canonicalization and real SHA-256; checks alignment against the header From; and applies the published DMARC policy. Real DKIM verifies an RSA or Ed25519 signature; here a keyed SHA-256 stands in for it, so changing the key in DNS breaks the seal just as a real key mismatch would. Pick a message, then try editing the subject, the body, or the zone.

Message presets
Zone presets

One record per line. Types: TXT, A, MX. Lines starting with ; are comments.

Envelope (SMTP)
Message header and body (RFC 5322)
DKIM-Signature (added by the sender)
–
    
          

    When mail takes a detour

    Forwarders, lists and ARC

    Run the "Forwarded" preset above and you see the system working as designed: SPF fails because the forwarder's address is not in example.com's list, DKIM still passes because nobody touched the message, and DMARC passes on the DKIM path. This is the single strongest argument for signing every message with DKIM. A domain that relies on SPF alone will see its own legitimate mail rejected whenever someone forwards it.

    Now run "Mailing list". The list server re-sends from its own address (so SPF passes, but for example.org, which does not align with example.com), adds a subject tag and a footer (so the DKIM body hash and the signed Subject both break), and the message fails DMARC. With p=reject, the subscribers never see it. Lists have answered this in two ways. Many now rewrite the From line to the list's own address ("Pat via Book Club"), which makes the list the author on paper. And the industry built ARC.

    ARC, the Authenticated Received Chain (RFC 8617), lets each intermediary record what it saw before it changed anything. A list server checks SPF, DKIM and DMARC on arrival, writes those results into an ARC-Authentication-Results header, signs the message as it stands (ARC-Message-Signature), and seals the whole chain so far (ARC-Seal). Each hop adds a numbered set, from i=1 up to a limit of 50. The final receiver can then say: "DMARC fails now, but a list server I trust tells me it passed on arrival, and the chain of seals is intact." ARC does not override DMARC by itself; it gives a receiver evidence to make a local exception for intermediaries it has decided to trust.

    What goes wrong

    The common mistakes

    Almost every email-authentication failure in the wild is one of a handful of configuration slips. Each has a zone preset in the authenticator so you can watch it happen.

    MistakeWhat happensFix
    Two SPF recordspermerror for every message; SPF effectively offmerge into one v=spf1 record
    Over 10 lookupspermerror; often appears only when a vendor adds an include on their sideremove unused includes; move vendors to their own subdomains; use ip4/ip6 for your own ranges
    SPF too broada whole shared hosting range is authorized, so other customers can pass as youauthorize the specific service, never a large provider block; prefer DKIM for vendors
    Forgotten sendera real service (helpdesk, payroll, printer) fails DMARC at enforcementread aggregate reports for weeks before quarantine; inventory every sender
    Key removed or rotated earlyDKIM fails for mail still in flight or still signed with the old keypublish the new selector first, switch signing, retire the old key days later
    p=none foreverreports arrive, nobody reads them, and spoofing still lands in inboxesa dated plan from none to quarantine to reject (Instrument 2)
    Open subdomainsattackers spoof an unwatched subdomain such as billing.example.com, or a parked brand domainsp=reject; v=spf1 -all and p=reject on every non-sending domain
    Go deeper: a government example of the end state

    In October 2017 the US Cybersecurity and Infrastructure Security Agency issued Binding Operational Directive 18-01, which required federal civilian agencies to deploy SPF and DMARC on their domains, send reports, and reach p=reject within one year, along with STARTTLS on mail servers and HTTPS on websites. It is a useful public model of a dated rollout: monitor first, then enforce on a schedule, for every domain including the ones that send nothing.

    Instrument 2

    The DMARC rollout planner

    List the services that send mail as your domain and what each one supports. The planner works out, for each service, how much of its legitimate mail would pass DMARC (directly, after forwarding, and through mailing lists), then shows how much real mail each policy step would quarantine or reject. Fix the red rows until the bars at "reject" are empty; that is the safe order of operations. The numbers are yours to change; nothing is sent anywhere.

    Legitimate mail / month–
    Passes DMARC today–
    SPF lookups used–
    Safe to reject?–
    Safe order of operations
      On the wire

      STARTTLS, MTA-STS and the downgrade

      SPF, DKIM and DMARC are about who sent a message. They do nothing to keep it private on the way. For that, servers encrypt the SMTP connection with TLS, the same protocol behind the browser padlock (see chapter 03 for how TLS and certificates work).

      Mail's version is called STARTTLS (RFC 3207). The connection starts in plain text; the receiving server lists STARTTLS among its capabilities; the sender issues the command; and the two upgrade to TLS before any message is sent. The catch is that this is opportunistic. If the receiver does not advertise STARTTLS, the sender shrugs and sends in the clear, because refusing would have broken mail delivery across the early internet. And historically, senders did not check the receiver's certificate either.

      That opens a downgrade. Anyone on the network path, such as a compromised router or a hostile network, can delete the STARTTLS line from the server's capability list. The sender sees a server that apparently cannot encrypt and delivers in plain text. Or the path can be steered to a different server presenting any certificate at all, which an unchecking sender accepts. Neither side notices.

      MTA-STS (RFC 8461) closes the gap with a published promise. The domain posts a small policy file over HTTPS, at a well-known address on an mta-sts. subdomain, and a TXT record saying a policy exists. The policy lists which MX hosts are legitimate and says mode: enforce. A sender that supports MTA-STS caches the policy and from then on refuses to deliver unless it gets TLS with a valid certificate for one of the named hosts. Because the policy is fetched over HTTPS with a real certificate, an attacker on the mail path cannot forge it.

      _mta-sts.example.com.  TXT  "v=STSv1; id=20261022T000000"
      
      https://mta-sts.example.com/.well-known/mta-sts.txt
      version: STSv1
      mode: enforce
      mx: mail.example.com
      max_age: 604800
      
      _smtp._tls.example.com.  TXT  "v=TLSRPTv1; rua=mailto:tls-reports@example.com"

      The last line is TLS-RPT (RFC 8460): like DMARC's aggregate reports, it asks sending systems to send a daily summary of TLS successes and failures when delivering to you. Start MTA-STS in mode: testing, which reports failures without blocking delivery, read the TLS-RPT reports, then switch to enforce. The max_age in seconds says how long senders should remember the policy; RFC 8461 allows up to 31,557,600 (one year), and the long cache is what protects the first connection after an attacker appears.

      Go deeper: DANE, the other answer

      DANE for SMTP (RFC 7672) solves the same downgrade problem using DNSSEC instead of HTTPS. The receiving domain publishes TLSA records, signed with DNSSEC, that pin the certificate or key its MX hosts will present. A DANE-aware sender that sees a signed TLSA record must use TLS and must see a matching key. It needs DNSSEC on both the domain and the sender's resolver, which is why MTA-STS was created as an alternative for domains without it. Many large receivers support one or both; publishing both is reasonable. DNSSEC itself is covered in chapter 07.

      The human layer

      Look-alikes pass every check

      Run the "Look-alike domain" preset in the authenticator. SPF passes, DKIM passes, DMARC passes with alignment and p=reject. The message is perfectly authenticated, because the attacker registered exarnple.example (r-n, not m) and set up SPF, DKIM and DMARC on it properly. Email authentication answers "is this really from the domain it names?" It cannot answer "is that the domain you think it is?"

      Phishing is MITRE ATT&CK technique T1566: a message designed to make someone click a link, open an attachment or hand over credentials or money. Once your own domains enforce DMARC, exact spoofing of them mostly stops working, and the tricks move to the parts a person has to judge:

      • Look-alike domains. Swapped letters, extra hyphens, a brand name in a different top-level domain, or homoglyphs: characters from another script that look identical, such as Cyrillic "а" for Latin "a". Internationalized names travel in DNS as Punycode (xn--…), which is one way software can show what is really there.
      • Display-name spoofing. The From line has two parts, a free-text name and an address. "Example Co Billing" <someone@example.org> authenticates fine for example.org, and many phone mail apps show only the name.
      • Reply-To tricks. The From is genuine or harmless, but a Reply-To: header quietly sends your answer somewhere else. Nothing in SPF, DKIM or DMARC checks Reply-To.
      • Compromised real accounts. Mail sent from a genuinely hijacked mailbox passes everything, because it is genuine. This is why authentication results are evidence, not a verdict.

      The defenses, in order of how much they do:

      • Phishing-resistant sign-in. Passkeys and security keys bind each credential to the real site's origin, so a fake login page on a look-alike domain gets nothing it can use. This turns the most damaging phish, credential theft, into a dead end. See chapter 04.
      • DMARC at reject on every domain you own, including the ones that send nothing, so exact spoofing of your names fails at the receiver.
      • Warnings at the receiver: "external sender" banners, flags on first-time senders and on look-alikes of your own domains, and display of the real address beside the name.
      • Link rewriting and attachment detonation: links checked at click time rather than delivery time, attachments opened in a sandbox first.
      • A report button and a reporting culture. One person who reports quickly lets the security team pull the same message from every other mailbox. Thank reporters, never punish clickers, and use the reports for detection (chapter 17).
      • Out-of-band checks for money and secrets. A request to change bank details or buy gift cards gets confirmed by phone, using a number you already had.
      Instrument 3

      The look-alike spotter

      Type a domain you protect and a domain from a suspicious message. The spotter maps each character through a small confusables table (a subset of the idea behind Unicode Technical Standard #39), compares the resulting "skeletons", measures edit distance, flags characters from non-Latin scripts and shows the Punycode form a mail server would actually see. It is the same kind of check that mail filters run to warn on look-alikes of your own names.

      Characters in the message domain (red = from another script or a known confusable)
      Skeleton of yours–
      Skeleton of theirs–
      What DNS sees (Punycode)–
      Edit distance (names only)–
      –
      Putting it together

      A defender's checklist

      • Inventory every system that sends as your domains. Aggregate DMARC reports will find the ones nobody remembers.
      • Sign everything with DKIM using your own domain in d=, 2,048-bit RSA or Ed25519, and rotate selectors on a schedule.
      • Keep one SPF record per domain, under 10 lookups, ending in -all or ~all, with no broad shared ranges.
      • Publish DMARC with rua=, start at p=none, and move to quarantine and then reject on dates you write down.
      • Put v=spf1 -all and p=reject on every domain that does not send mail.
      • Publish MTA-STS (testing, then enforce) and TLS-RPT, or DANE if you run DNSSEC.
      • On the receiving side: enforce other domains' DMARC, honor ARC from intermediaries you trust, warn on look-alikes and external senders.
      • Give people a report button, phishing-resistant MFA, and a rule that money and credential requests are verified another way.
      Where this comes from

      Sources