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
.msgexport, convert it first: Open Outlook .msg Files on Linux digon your analysis machine (packagebind9-dnsutilson Debian/Ubuntu,bind-utilson 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:
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:
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
failorsoftfail. 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:
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:
If dig returns no v=spf1 string at all, the domain publishes no SPF policy
and every check on it yields none.