H4CK0R Network Security Β· Recon Β· Education

βœ‰οΈ Email Authentication: SPF, DKIM, DMARC and Beyond

Email Authentication: SPF, DKIM, DMARC and Beyond

SMTP was specified in 1982 with no concept of sender authentication. Any host that can open a TCP connection to a mail server can claim to be any domain, and the protocol has no way to object. Everything below is a retrofit bolted on later, which is why there are three overlapping protocols instead of one.

The envelope and the header are two different things

This distinction is the whole reason DMARC exists. An SMTP transaction has an envelope β€” the MAIL FROM and RCPT TO commands, used for routing and bounces β€” and then a message whose headers include the From: that a human actually sees. They are set independently:

220 mx.example.net ESMTP ready
EHLO relay.marketing.example
250-mx.example.net
MAIL FROM:<bounces@marketing.example>      <-- envelope sender: SPF checks THIS
250 2.1.0 Ok
RCPT TO:<victim@example.net>
250 2.1.5 Ok
DATA
354 End data with <CR><LF>.<CR><LF>
From: "Billing Department" <billing@yourbank.example>   <-- what the user sees
Subject: Invoice 4471 overdue
...
.
250 2.0.0 Ok: queued

SPF validates marketing.example. The recipient reads yourbank.example. SPF alone therefore cannot stop the spoof that matters β€” and that gap is exactly what DMARC's alignment requirement closes.

SPF β€” which IPs may send for a domain

SPF (RFC 7208) is a single TXT record at the domain apex listing authorised senders. There must be exactly one; two v=spf1 records is a permanent error.

example.com.  300  IN  TXT  "v=spf1 ip4:203.0.113.0/24 ip6:2001:db8:25::/48 include:_spf.google.com include:sendgrid.net -all"
MechanismMatches when…DNS lookups
ip4: / ip6:the connecting IP is in the given CIDR0 β€” free, prefer these
a / a:hostthe IP matches an A/AAAA record of the domain (or named host)1
mxthe IP matches an A/AAAA of one of the domain's MX hosts1, plus one per MX name (max 10 MX names)
include:the SPF evaluation of another domain passes1 plus everything inside it
exists:a macro-expanded name resolves to anything1
ptrreverse DNS of the IP lands in the domainexpensive β€” deprecated, do not use
redirect=evaluation is replaced by another domain's record1
allalways β€” must be last0

Each mechanism carries a qualifier: + pass (the default), - fail, ~ softfail, ? neutral. So -all means "anything not listed above is forged", ~all means "probably forged, accept but mark". Use ~all while you are still discovering senders and -all once you are confident.

The 10-lookup limit is the number one SPF bug Evaluating a record may trigger at most 10 DNS-querying mechanisms β€” a, mx, ptr, exists, include and redirect, counted recursively through every include. Exceed it and the result is permerror, which receivers treat as "not a pass", so SPF stops helping you entirely β€” silently, and usually months after someone added one more SaaS sender. There is also a limit of two "void" lookups (those returning NXDOMAIN or no records). Flatten by replacing include: chains with explicit ip4:/ip6: ranges where the provider publishes stable ones, and count before you publish.

SPF's other structural weakness is forwarding. When a mailbox auto-forwards, the forwarding server connects from its own IP, which is not in your record, so SPF fails at the final destination through no fault of yours. Sender Rewriting Scheme (SRS) works around it by rewriting the envelope sender, but the durable answer is DKIM, which survives forwarding untouched. For a domain that never sends mail at all, publish v=spf1 -all and an equally restrictive DMARC record.

DKIM β€” a signature over the message itself

DKIM (RFC 6376) has the sending server sign selected headers and a hash of the body with a private key, and publish the public key in DNS. Because the signature travels with the message, it keeps verifying after forwarding β€” as long as nothing modifies what was signed.

DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=example.com; s=s1;
    t=1769420100; h=from:to:subject:date:message-id:mime-version;
    bh=<base64 SHA-256 of the canonicalised body>;
    b=<base64 signature over the canonicalised signed headers>
TagMeaning
d=The signing domain. DMARC alignment is judged against this.
s=Selector β€” picks which key, so one domain can have many.
a=Algorithm: rsa-sha256, or ed25519-sha256 (RFC 8463). RFC 8301 forbids rsa-sha1 and sets a 1024-bit floor; 2048-bit RSA is the practical standard.
c=Canonicalisation, header/body. relaxed/relaxed tolerates whitespace and header-case changes in transit; simple breaks if anything touches the message.
h=The exact list of headers covered. From is mandatory. Listing a header twice ("over-signing") stops a relay adding a second copy that clients display instead.
bh= / b=Body hash and the signature itself.
l=How many bytes of the body were signed. Do not use it. Anything after that offset is unsigned, so an attacker can append arbitrary content β€” a whole new visible message β€” to a validly signed mail.

The public key lives at <selector>._domainkey.<domain>:

s1._domainkey.example.com.  300  IN  TXT  "v=DKIM1; k=rsa; p=MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA...IDAQAB"

Rotate by publishing a new selector (s2), switching the signer to it, then removing the old record only after messages signed with it have aged out. Never delete a selector while mail signed by it is still in flight β€” verification will fail. A 2048-bit key exceeds the 255-character limit of a single TXT string, so it must be published as multiple quoted strings that the resolver concatenates; getting this wrong is a common cause of "no key for signature".

DMARC β€” alignment, policy and reporting

DMARC (RFC 7489) is the layer that ties SPF and DKIM to the domain the human sees, and tells receivers what to do when nothing matches.

_dmarc.example.com.  300  IN  TXT  "v=DMARC1; p=reject; sp=reject; adkim=s; aspf=s; fo=1; rua=mailto:dmarc@example.com; ruf=mailto:forensics@example.com"

Alignment is the key concept. A DMARC pass requires at least one of:

Relaxed alignment (adkim=r, aspf=r, the default) accepts any subdomain of the same organisational domain β€” mail.example.com aligns with example.com. Strict (s) demands an exact match. This is why a message can have spf=pass in its headers and still be rejected by DMARC: the SPF pass belonged to a different domain than the one in From:.

TagValuesNotes
p=none / quarantine / rejectPolicy for the domain. none is monitoring only.
sp=same valuesPolicy for subdomains. Omit it and subdomains inherit p.
pct=1–100Apply the policy to a sample. Honoured inconsistently across receivers, and the in-progress DMARC revision moves away from it toward a plain testing flag β€” useful as a ramp, not as a permanent setting.
rua=mailto:Daily aggregate XML reports. This is where all the value is.
ruf=mailto:Per-message failure reports. Most large receivers do not send them, for privacy reasons.
fo=0/1/d/sWhen to generate failure reports; fo=1 means "any underlying check failed".

If the rua/ruf address lives on a different domain from the one publishing the record β€” which it does the moment you use a third-party report analyser β€” that other domain must opt in, or conforming receivers will simply not send the reports:

; published in the REPORT RECEIVER's zone, naming the domain it accepts reports for
example.com._report._dmarc.analyser.example.  IN  TXT  "v=DMARC1"
A realistic rollout
  1. Publish p=none with rua=. Nothing changes for your mail flow; aggregate reports start arriving within a day or two.
  2. Inventory for two to four weeks. The reports will name senders you had forgotten: the CRM, the ticketing system, the print shop, the office scanner, a payroll provider. Every one needs either an SPF entry or DKIM signing with an aligned d=.
  3. Fix alignment, not just pass/fail. Many SaaS senders pass SPF on their own bounce domain, which does not align. Ask them for domain-aligned DKIM (a CNAME to their key) β€” that is the durable fix.
  4. Move to p=quarantine, optionally ramping with pct=, and keep reading reports.
  5. Move to p=reject, and set sp=reject plus v=spf1 -all on every non-sending subdomain and parked domain β€” those are what actually get spoofed.

Do not jump straight to p=reject. The first messages it silently kills are usually your own invoices. Analyse a domain's SPF/DKIM/DMARC β†’ Β· build an SPF record β†’ Β· build a DMARC record β†’

The transport layer: MTA-STS, TLS-RPT and DANE

SPF/DKIM/DMARC authenticate the sender. They say nothing about whether the message crossed the network encrypted. SMTP's STARTTLS (RFC 3207) is opportunistic: the receiving server advertises 250-STARTTLS, and an active attacker simply deletes that line from the response, after which the sender happily delivers in plaintext. No warning is shown to anyone.

MTA-STS (RFC 8461) fixes the downgrade by publishing a policy over HTTPS that a sender caches. It needs a DNS record and a static file:

_mta-sts.example.com.  300  IN  TXT  "v=STSv1; id=20260726T101500;"

; served at https://mta-sts.example.com/.well-known/mta-sts.txt
version: STSv1
mode: enforce
mx: mx1.example.com
mx: mx2.example.com
max_age: 604800

mode: testing reports problems without failing delivery β€” start there. max_age: 604800 is one week of caching. Change id= whenever the policy file changes, or senders will keep using the cached copy.

TLS-RPT (RFC 8460) asks senders to report delivery/TLS problems back to you, which is the only way to find out that MTA-STS is quietly breaking mail:

_smtp._tls.example.com.  300  IN  TXT  "v=TLSRPTv1; rua=mailto:tlsrpt@example.com"

DANE (RFC 7672 for SMTP, using the TLSA record of RFC 6698) solves the same problem through DNSSEC instead of HTTPS: you publish a fingerprint of the MX host's key in DNS, and the DNSSEC chain guarantees it is genuine.

_25._tcp.mx1.example.com.  300  IN  TLSA  3 1 1 <hex SHA-256 of the SubjectPublicKeyInfo>
;                                         | | |
;                                         | | +-- matching type 1 = SHA-256
;                                         | +-- selector 1 = SubjectPublicKeyInfo
;                                         +-- usage 3 = DANE-EE: this exact key, no CA involved

DANE is strictly dependent on DNSSEC β€” without a signed zone the TLSA record is just as forgeable as the STARTTLS banner, so the whole mechanism collapses. MTA-STS was designed precisely because DNSSEC deployment is patchy. Running both is reasonable and they do not conflict. Check MX, STARTTLS, MTA-STS, TLS-RPT and DANE β†’

BIMI and ARC

BIMI publishes a logo for participating mailbox providers to display next to your messages. It is still an IETF draft rather than a finished standard, and it is not a security control β€” it is a reward for having DMARC at enforcement, since a BIMI record is only honoured when the domain publishes p=quarantine or p=reject. Major providers additionally require a Verified Mark Certificate (or the cheaper Common Mark Certificate), which attests to the logo's ownership and is issued by a very small number of CAs β€” the real cost of BIMI is that certificate, not the DNS record.

default._bimi.example.com.  300  IN  TXT  "v=BIMI1; l=https://example.com/bimi/logo.svg; a=https://example.com/bimi/vmc.pem"

ARC (RFC 8617) addresses the mailing-list problem. A list server rewrites the subject, appends a footer and re-sends from its own infrastructure β€” breaking SPF (new IP, unaligned envelope) and DKIM (the body changed). ARC lets each intermediary record the authentication results it observed and seal that record cryptographically, forming a chain:

ARC-Authentication-Results: i=1; lists.example.org; spf=pass ...; dkim=pass ...; dmarc=pass ...
ARC-Message-Signature:      i=1; a=rsa-sha256; d=lists.example.org; s=arc; ...
ARC-Seal:                   i=1; a=rsa-sha256; d=lists.example.org; s=arc; cv=none; ...

The final receiver can then see that the message authenticated correctly before the list touched it, and choose to honour that. Crucially, ARC is advisory: it only helps if the receiver trusts the sealer. It does not override DMARC.

Reading the verdict

Every mailbox provider stamps its findings into an Authentication-Results header (RFC 8601). This is the first thing to read on a suspicious message, and the only trustworthy one is the header added by your own receiving server β€” the topmost one; anything below it was written by hosts you do not control and can be forged.

Authentication-Results: mx.example.net;
    spf=pass (mx.example.net: domain of bounces@marketing.example
        designates 203.0.113.24 as permitted sender) smtp.mailfrom=marketing.example;
    dkim=pass header.i=@example.com header.s=s1;
    dmarc=fail (p=REJECT sp=REJECT dis=REJECT) header.from=yourbank.example

SPF passed and DKIM passed β€” but for the wrong domains. DMARC failed because neither aligned with the visible From:. That single line is the difference between a legitimate message and a convincing forgery. Paste full headers and walk the hops β†’

Key takeaways
  • SPF checks the envelope sender; users see the From: header. DMARC's alignment requirement is what connects them.
  • SPF has a hard 10-DNS-lookup budget. Exceeding it is a permerror, which quietly disables your SPF entirely.
  • SPF breaks on forwarding; DKIM does not. Publish both.
  • DKIM: 2048-bit keys, rotate by selector, sign From:, and never use the l= tag.
  • A DMARC pass needs SPF or DKIM to both pass and align. Passing without aligning is the most common surprise.
  • Roll out p=none β†’ read rua reports β†’ fix alignment β†’ quarantine β†’ reject, with sp=reject and v=spf1 -all on parked domains.
  • STARTTLS is trivially stripped. MTA-STS (HTTPS policy) or DANE (DNSSEC) is what makes SMTP TLS non-optional; TLS-RPT tells you when it breaks.
  • BIMI is a draft and a branding reward for DMARC enforcement, not a security control. ARC is advisory help for forwarders and lists.