Engineer Training · Internal

SPF, DKIM & DMARC — how email proves who sent it

An interactive walkthrough of email authentication: what each record does for an organisation, what the records look like in DNS, and exactly what happens between a sending Exchange server and the recipient's mail server. Work left to right through the tabs — then break things in the Mail Journey simulator.

The problem: anyone can claim to be you

SMTP — the protocol that moves email between servers — was designed in 1982 and has no built-in identity check. Any server on the internet can connect to your customer's mail server and declare it is sending on behalf of twenty-four.it. Nothing in the protocol stops it.

What makes this worse is that every email actually carries two different sender identities, and most people only ever see one of them:

ENVELOPE

✉️ The envelope — RFC 5321 (SMTP)

Used by servers for routing and bounces. Thrown away on delivery — the user never sees it. The receiving server saves the envelope sender into a hidden Return-Path: header.

MAIL FROM: <mark@twenty-four.it>
Connecting IP: 40.107.22.75
HELO/EHLO: ...outbound.protection.outlook.com
SPF checks these
LETTER

📄 The letter inside — RFC 5322 (the message)

The headers and body — this is all Outlook shows. The From: here is just text. It does not have to match the envelope.

From: Mark <mark@twenty-four.it>
Subject: Invoice for July
DKIM-Signature: d=twenty-four.it; s=selector1; b=…
DKIM seals this DMARC aligns this with the checks
The whole game in one sentence: spoofing works because the letterhead (header From:) is unverified free text — SPF and DKIM verify other identities, and DMARC is what finally ties those checks back to the From address the user actually sees.

The three checks — a postal analogy

📮

SPF — the approved courier list

The domain publishes, in DNS, a list of servers allowed to hand over its envelopes. The receiver checks: did this envelope arrive via an approved courier (IP address)?

Checks the envelope sender + connecting IP. Knows nothing about the letter inside.

🔏

DKIM — the wax seal

The sending server stamps the letter with a cryptographic seal only it can make. The receiver fetches the matching public key from DNS and verifies the seal is genuine and the letter wasn't altered in transit.

Proves integrity + which domain signed it. Survives forwarding.

📋

DMARC — the policy card

The domain's instruction to every receiver: "If neither check lines up with the From address on the letterhead — deliver anyway / junk it / reject it. And send me a report either way."

Adds alignment, policy and reporting on top of SPF + DKIM.

Why an organisation cares: without these three records enforced, anyone can invoice your customers as accounts@twenty-four.it, and your own legitimate mail is more likely to land in junk. With them, spoofed mail is rejected before a human ever sees it — and you get reports showing who is sending as your domain.

SPF — Sender Policy Framework

A public list, published in DNS, of every server allowed to put your domain on the envelope. The receiving server looks at the connecting IP address and the domain in MAIL FROM, fetches that domain's SPF record, and checks whether the IP is on the list.

The record — click each part

TXT record published at: twenty-four.it  (the domain apex — no special prefix)

👆 Click any part of the record to see what it does.

Try it: which senders pass?

The receiver walks the record left to right and stops at the first mechanism that matches the connecting IP. Pick a sender and run the check:

Qualifiers — the character in front of a mechanism

QualifierResultReceiver's usual treatment
+ (default)passSender is authorised. ip4:… means +ip4:…
-allfailHard fail — "anyone else is not us". Strongest signal; what we deploy.
~allsoftfail"Probably not us" — usually accepted but spam-scored. Useful mid-rollout.
?allneutralNo opinion — near useless in production.

What engineers get wrong about SPF

It never looks at the From: header. SPF validates the envelope (MAIL FROM) only. A phish can pass SPF for the attacker's own envelope domain while displaying mark@twenty-four.it to the user. That gap is exactly what DMARC alignment closes.
The 10-DNS-lookup limit. include:, a, mx, exists, redirect each cost a lookup (nested includes count too). Exceed 10 and the result is permerror — treated as no valid SPF. Audit before adding "just one more include".
Forwarding breaks it. When partner.org auto-forwards your mail on, their server's IP is now the connecting IP — it isn't in your record, so SPF fails. This is why SPF alone can't be trusted to reject, and why DKIM matters (see the Journey tab).
One record only. Two TXT records starting v=spf1 on the same name = permerror. Merge new senders into the existing record — never add a second one. (Empty or missing record = result none.)
When MAIL FROM is empty (bounces/NDRs are sent with MAIL FROM:<>), receivers check SPF against the HELO/EHLO hostname instead — one more reason sending servers should have honest HELO names with matching SPF.

DKIM — DomainKeys Identified Mail

A tamper-evident cryptographic seal. The sending server holds a private key and signs each outgoing message; the matching public key sits in DNS for anyone to fetch. If the signature verifies, the receiver knows two things: the message really was handled by the domain in d=, and the signed content hasn't changed one byte since signing.

🔐

1 · Sign at source

Exchange Online canonicalises the message, hashes the body, then signs the chosen headers + body hash with the domain's private key. The result is added as a DKIM-Signature: header.

🌐

2 · Key in DNS

The public key is published at <selector>._domainkey.<domain>. The selector in the signature tells the receiver exactly which record to fetch — so a domain can hold several keys at once.

3 · Verify at destination

The receiver re-hashes what arrived and checks the signature with the public key. Any edit to the signed headers or body — even one character — and verification fails.

The signature the sender adds — click each tag

Header inside the message: DKIM-Signature:

👆 Click a tag — d= and s= are the two that matter most day-to-day.

The public key the receiver fetches — click each part

TXT record published at: selector1._domainkey.twenty-four.it

👆 Click any part of the record.

Tamper test — why the seal works

A signed message is in transit. Deliver it untouched, or let an attacker-in-the-middle edit the body, and watch the body hash comparison:

From: Mark <mark@twenty-four.it>
Subject: July invoice
DKIM-Signature: d=twenty-four.it; s=selector1; bh=; b=…
Hi Sarah, Please find our July invoice attached. Bank details are unchanged from last month. Thanks, Mark
📩
Message arrives at recipient. Signature says the body hash at signing time (bh=) was:
🧮
Receiver re-hashes the body as it actually arrived:
⚖️
Press a button above to run the comparison.

What engineers get wrong about DKIM

DKIM pass ≠ trustworthy mail. It proves the d= domain signed it and nothing changed. Spammers sign their own spam perfectly well. The question DMARC asks next is: does d= match the From the user sees?
Anything that modifies mail in transit breaks it. Old-style mailing lists adding [listname] to the subject or a footer to the body, or gateways rewriting links, invalidate b=/bh=. That's a genuine fail — the content did change.
Microsoft 365 specifics: DKIM for a custom domain is enabled in the Defender portal (or PowerShell). You publish two CNAMEsselector1._domainkey and selector2._domainkey…onmicrosoft.com targets — and Microsoft hosts the real keys behind them. Two selectors exist so keys can be rotated with zero downtime.
Unlike SPF, DKIM survives forwarding — the seal travels inside the message, so it verifies no matter which server delivers the final hop (as long as content is untouched). This makes DKIM the more robust half of a DMARC pass.

DMARC — Domain-based Message Authentication, Reporting & Conformance

SPF checks the envelope. DKIM checks the seal. Neither looks at the From address the user reads. DMARC is the policy layer that closes that gap: it demands the passing check lines up with the visible From domain, tells receivers what to do when it doesn't, and sends you reports about everyone sending as your domain.

DMARC = PASS  if  (SPF passed and its domain aligns with From:)  OR  (DKIM passed and its d= aligns with From:)
One aligned pass is enough. Zero aligned passes = DMARC fail → the published policy applies.

The record — click each tag

TXT record published at: _dmarc.twenty-four.it

👆 Click a tag. p= is the one that changes the world.

Alignment lab — the concept that makes DMARC click

The message the user sees says From: mark@twenty-four.it. Set what actually happened underneath, and see the DMARC verdict:

The policy ladder — how we roll DMARC out

p=none

Monitor. Deliver as normal, but send aggregate reports. Weeks 1–4: discover every legitimate system sending as the domain (CRM, payroll, copiers, ticketing…) and fix their SPF/DKIM.

p=quarantine

Junk it. Failing mail goes to spam. Ramp with pct=25 → 100 once reports show legitimate sources are aligned.

p=reject

Refuse it. Failing mail is bounced at SMTP — the destination-side goal. Spoofed invoices never reach a human. Keep watching reports for new senders.

Never jump straight to p=reject. On day one you don't know every system sending as your domain — that's what p=none + rua reports are for. Going to reject blind is how payroll notifications and copier scans silently vanish.

Reports — the R in DMARC

Every major receiver (Microsoft, Google, Yahoo…) sends a daily aggregate report (gzipped XML) to your rua= address: which IPs sent as your domain, volumes, and SPF/DKIM/alignment results per source. Raw XML is painful — in practice we point rua at an analyser (e.g. a DMARC reporting service) and read dashboards.

<!-- one row of an aggregate report: someone in 198.51.100.0/24 spoofed us 37 times -->
<record>
  <row><source_ip>198.51.100.66</source_ip><count>37</count>
    <policy_evaluated><disposition>reject</disposition>
      <dkim>fail</dkim><spf>fail</spf></policy_evaluated></row>
  <identifiers><header_from>twenty-four.it</header_from></identifiers>
</record>

What DMARC can and cannot do

It protects your exact domain. Direct spoofs of twenty-four.it get rejected everywhere that honours your policy — and you get visibility of every abuse attempt via reports.
It cannot stop lookalikes or compromised mailboxes. twenty-fuor.it is a different domain with its own (passing!) records, and mail sent from a genuinely compromised M365 account passes SPF, DKIM and DMARC perfectly. That's what user training, MFA and filtering are for.

The Mail Journey — sender ⇄ DNS ⇄ recipient

This is the whole story end-to-end: Mark emails sarah@contoso.com. Watch the SMTP conversation, every DNS lookup, and each check as the recipient server makes its decision. Then change the scenario — spoof it, forward it, misconfigure it — and change our published DMARC policy to see how the outcome shifts.

TwentyFour IT — sender

Exchange Online (EOP outbound)
IP: 40.107.22.75
Microsoft 365 Outlook
Mark hits Send in Outlook…

🌐 Public DNS

where all three records live
No lookups yet.
sender recipient
✉️

Contoso Ltd — recipient

mail.contoso.com (inbound MX)
Protecting sarah@contoso.com
Listening on port 25…
SPF
envelope + IP
DKIM
signature
DMARC
alignment + policy

SMTP conversation & events

What's happening & why

Ready

Press Play or Step

The simulator will walk through every stage: composing, DKIM signing at source, the MX lookup, the SMTP handshake, then the recipient's SPF, DKIM and DMARC checks and the final verdict.

Tip: run the ✅ legitimate scenario first, then the 🕵️ clever spoof — the contrast between them is the reason DMARC exists.

Knowledge check

Eight questions, the kind that come up on real tickets. Answer honestly — explanations appear as you go.

Engineer's cheat sheet

The three records at a glance

Lives at (DNS)Looks likeProtects / provides
SPFtwenty-four.it  (TXT)v=spf1 ip4:203.0.113.10 include:spf.protection.outlook.com -allWhich IPs may use the domain on the envelope (MAIL FROM)
DKIMselector1._domainkey.twenty-four.it  (TXT / CNAME for M365)v=DKIM1; k=rsa; p=<public key>Message integrity + proof the d= domain signed it
DMARC_dmarc.twenty-four.it  (TXT)v=DMARC1; p=reject; rua=mailto:dmarc@twenty-four.itAlignment with the visible From + policy + reports

Look them up (PowerShell / dig)

# PowerShell (any Windows box)
Resolve-DnsName twenty-four.it -Type TXT | Select -Expand Strings   # SPF
Resolve-DnsName _dmarc.twenty-four.it -Type TXT                     # DMARC
Resolve-DnsName selector1._domainkey.twenty-four.it -Type TXT       # DKIM (follows the M365 CNAME)

# dig equivalents
dig +short TXT twenty-four.it
dig +short TXT _dmarc.twenty-four.it
dig +short TXT selector1._domainkey.twenty-four.it

Read the receiver's verdict on any message

Open the message headers (Outlook: File → Properties → Internet headers, or the Message Header Analyzer add-in / mha.azurewebsites.net) and find Authentication-Results: — the receiving server writes its SPF/DKIM/DMARC results there:

Authentication-Results: spf=pass (sender IP is 40.107.22.75)
  smtp.mailfrom=twenty-four.it; dkim=pass (signature was verified)
  header.d=twenty-four.it; dmarc=pass action=none header.from=twenty-four.it

Also useful: Received-SPF: and Return-Path: (the saved envelope sender — compare it with From: to spot alignment issues by eye).

Ticket triage — symptom → first thing to check

SymptomLikely causeCheck / fix
Customer's outbound lands in recipients' junkSPF/DKIM/DMARC missing or failing; new sender not in SPFAuthentication-Results on a caught message; verify all three records; align the sending service
Mail from a new platform (CRM, mailshot, alerts) bounced with 550 5.7.509 … does not pass DMARCPlatform sends as the domain but isn't authorisedAdd their include to SPF and/or (better) set up their DKIM signing as the domain; test before go-live
Forwarded mail failing / quarantined at final hopSPF always breaks on forward; DKIM must carry the passConfirm DKIM is enabled and intact; that's the aligned pass DMARC needs
"Too many DNS lookups" / SPF permerror in headers>10 lookups from stacked includesCount lookups; remove dead includes; consider consolidating senders
Intermittent SPF fail from one site onlyDevice relaying direct-to-internet (MFD/scanner, app server) with IP not in SPFRoute it via M365 SMTP relay/connector or add its egress IP
Spoof reported by a user but DMARC "passes"Lookalike domain or compromised genuine mailboxCheck the exact From domain character-by-character; check sign-in logs / audit for compromise

Rollout runbook (new customer domain)

StepAction
1Inventory every legitimate sender of the domain: M365, printers/MFDs, apps, CRM, payroll, mailshot tools, website forms.
2Publish/repair SPF (one record, all legit sources, aim for -all, stay under 10 lookups).
3Enable DKIM everywhere possible — M365 selectors via Defender portal + each third-party platform signing as the domain.
4Publish DMARC p=none with rua= to the reporting mailbox/analyser. Wait 2–4 weeks.
5Fix every legitimate source the reports expose. Then p=quarantine (optionally pct= ramp).
6Move to p=reject. Keep reading reports — new senders appear whenever someone buys new software.
Why this is no longer optional: Google and Yahoo have required SPF + DKIM + at least p=none DMARC for bulk senders since early 2024, and Microsoft applies equivalent rules for high-volume senders to consumer Outlook. No auth = no delivery, regardless of content.