Skip to content

SPF Record Explained: What spf=pass Really Means in Mail Headers

You are triaging a reported phishing mail. The From: line shows the CEO's domain, the Authentication-Results: header says spf=pass — and the mail is still a spoof. No contradiction: SPF never looked at the From: header your user saw. Understanding exactly what SPF checks, and what it does not, is what makes the verdict useful instead of misleading.

This article covers SPF from the analyst's side: how the mechanism works, how to read the verdict in raw headers, and how to check any domain's SPF record yourself with dig. It is not a deployment guide for your own mail server.


Pre-requisites

  • A raw mail with full headers — if all you have is an Outlook .msg export, convert it first: Open Outlook .msg Files on Linux
  • dig on your analysis machine (package bind9-dnsutils on Debian/Ubuntu, bind-utils on RHEL-family)
  • The companion articles for the other two mechanisms: DKIM and DMARC

All examples use RFC 2606 reserved domains (example.com, example.org, example.net) and RFC 5737 documentation IPs — substitute the real values from your case.


What SPF Actually Checks

SPF (Sender Policy Framework, RFC 7208) lets a domain publish, in DNS, which IP addresses are allowed to send mail on its behalf. The receiving mail server looks at the connecting client IP, fetches the sender domain's SPF record, and checks whether that IP is listed.

The crucial detail is which sender domain. Every mail has two sender identities:

Identity Where it lives Who sees it
RFC5321.MailFrom — the envelope sender SMTP MAIL FROM command; preserved as the Return-Path: header Mail servers only
RFC5322.From — the header sender The From: header The human reading the mail

SPF validates the envelope sender (MAIL FROM) and the HELO hostname — never the From: header. RFC 7208 section 2.4 requires verifiers to check the MAIL FROM identity; checking other identities is explicitly not recommended by the spec.

This is the single most common misconception about SPF, and attackers exploit it directly: they send from their own domain in the envelope (MAIL FROM: <bounce@example.org>), which passes SPF cleanly because their own record authorises their own server — while the From: header displays ceo@example.com. SPF has done its job correctly and the mail is still a spoof. Tying the authenticated domain to the visible From: is exactly what DMARC alignment adds on top.


Reading the Verdict in Headers

The receiving server records its SPF check in two places. Both sit near the top of the raw header block.

Received-SPF: — the dedicated trace header defined by RFC 7208:

Received-SPF: pass (mx.example.net: domain of
    bounce@example.org designates 203.0.113.25 as permitted sender)
    receiver=mx.example.net; client-ip=203.0.113.25;
    envelope-from="bounce@example.org"; helo=mail.example.org;

Read it right to left: the connecting IP (client-ip) was checked against the SPF record of the envelope-from domain — example.org, not whatever the From: header claims.

Authentication-Results: — the combined header (RFC 8601) that carries SPF, DKIM, and DMARC results together. Microsoft 365 stamps it like this:

Authentication-Results: spf=pass (sender IP is 203.0.113.25)
 smtp.mailfrom=example.org; ...

smtp.mailfrom names the domain that was actually validated. During triage, always compare it against the From: header domain — a mismatch is normal for newsletters and bulk senders, but combined with a lookalike or high-value From: domain it is your strongest spoofing signal.

Only trust your own gateway's headers

Authentication-Results: and Received-SPF: are plain text — a sender can forge them. RFC 8601 therefore requires border MTAs to strip inbound headers that claim their own identifier. Trust only the instance stamped by your own mail infrastructure (the authserv-id right after the colon names who wrote it), which sits above the forged ones in the header block.


Reading an SPF Record

SPF records are published as DNS TXT records at the name of the domain being checked — usually the apex, but an envelope domain like bounce.example.org carries its own record at exactly that name. The record must start with v=spf1. Query one yourself:

dig +short txt example.org
"v=spf1 ip4:203.0.113.0/24 include:_spf.example.net mx -all"

Evaluation runs left to right; the first mechanism that matches the connecting IP decides the result. The most common mechanisms:

Mechanism Matches when the client IP ...
ip4: / ip6: is in the given address or CIDR range
a matches an A/AAAA record of the domain
mx matches one of the domain's MX hosts
include: passes the SPF check of another domain (nested policy)
exists: a constructed DNS name resolves (rare, macro-based)
all always — the catch-all at the end

Each mechanism carries an optional qualifier that sets the result on match:

Qualifier Result Typical reading
+ (default) pass authorised sender
- fail explicitly not authorised ("hardfail")
~ softfail probably not authorised — accept but mark
? neutral domain makes no statement

So -all at the end means "everything not matched above is unauthorised", while ~all is the weaker "probably unauthorised". A domain ending in ~all or ?all gives receivers little to act on — one reason spoofed mail from such domains still reaches inboxes.


The Seven Verdicts

RFC 7208 defines exactly seven results. What they mean on your triage screen:

Verdict Meaning for the analyst
pass Connecting IP is authorised for the envelope domain. Says nothing about the visible From:.
fail IP is explicitly not authorised (-all). Strong signal — but legitimate forwarded mail also fails, see below.
softfail "Probably not authorised" (~all). Suspicious, not conclusive.
neutral Domain explicitly makes no assertion (?). Treat like none.
none No SPF record exists, or no usable domain in the envelope.
temperror Transient DNS problem during the check. The verdict may differ on retry.
permerror The record itself is broken and could not be interpreted.

Why Big Domains Throw permerror: the 10-Lookup Limit

RFC 7208 caps the number of DNS-querying terms per SPF evaluation at 10. include:, a, mx, ptr, exists and the redirect modifier each count; ip4:, ip6: and all do not. Exceeding the limit must return permerror. Two additional caps: at most 2 lookups may return no answer ("void lookups"), and each mx or ptr evaluation may query at most 10 address records.

This matters in practice because include: chains nest: every SaaS tool a company signs mail through adds includes of its own. When you see permerror on mail from a large organisation, count the lookups in their record before assuming malice — an overgrown record is the usual culprit, and it means receivers cannot evaluate the policy at all.


What SPF Cannot Do

Three structural gaps to keep in mind during triage:

  • It never checks the visible From:. An attacker passing SPF for their own envelope domain is routine, not an anomaly.
  • Forwarding breaks it. A forwarder resends the mail from its own IP with the original envelope domain, so a perfectly legitimate mail arrives as fail or softfail. An SPF fail alone is therefore not proof of spoofing.
  • It says nothing about content integrity. The message body could be altered in transit without SPF noticing — that is DKIM's job.

SPF only becomes an anti-spoofing control when DMARC forces the SPF-validated domain to align with the From: header. Read that article next for the complete picture.


Check It Yourself

Confirm what a domain publishes, straight from your analysis shell:

dig +short txt example.org

Only the string starting with v=spf1 is the SPF record — domains park many other TXT records (site verifications, keys) at the apex. To follow an include:, query the included domain the same way:

dig +short txt _spf.example.net

If dig returns no v=spf1 string at all, the domain publishes no SPF policy and every check on it yields none.