Resources / Authentication
SPF Deep Dive (RFC 7208)
SPF authorizes which hosts may use your domain in the SMTP envelope. This is the full RFC 7208 picture: record format, every mechanism and qualifier, the hard 10-lookup limit that breaks records, the result codes, and the mistakes that quietly cause permerror.
Last checked: June 21, 2026
SPF (Sender Policy Framework) is the oldest of the three core authentication mechanisms and the one most often misunderstood. It does exactly one thing: it lets the owner of a domain “explicitly authorize the hosts allowed to use their domain names” in the MAIL FROM or HELO identities, and lets a receiver check the connecting IP against that authorization (RFC 7208 §1). That is the whole job. It says nothing about the From: header a person reads, and nothing about whether the mail is wanted.
The 60-second version
- One DNS TXT record per domain, starting with
v=spf1. Published as TXT (type 16) only - the old SPF RR (type 99) is discontinued (RFC 7208 §3.1). - Exactly one SPF record per domain. Two matching records is not “belt and suspenders,” it is a
permerror(RFC 7208 §3.2). - Mechanisms are read left to right; the first match wins, and its qualifier (
+ - ~ ?) decides the result. - There is a hard ceiling of 10 DNS-querying terms per evaluation. Exceed it and you get
permerror(RFC 7208 §4.6.4). - SPF validates the envelope, not the visible
From:. For DMARC, only theMAIL FROMidentity counts, and it must align withFrom:(RFC 9989 §3.2.4, §4.4.2). - SPF breaks on forwarding by design, because the forwarder’s IP is not in your record.
Record format
An SPF record is a single DNS TXT record. The version section is v=spf1 and is terminated by a space or end of record; a record beginning v=spf10 does not match and is discarded (RFC 7208 §4.5).
example.com. 3600 IN TXT "v=spf1 ip4:198.51.100.0/24 include:_spf.provider.example -all"
Two formatting rules trip people up:
- One record only. “A domain name MUST NOT have multiple SPF records that would cause an authorization check to select more than one record.” Two
v=spf1records producespermerror(RFC 7208 §3.2, §4.5). - Long records are split into strings, concatenated with no space. A single TXT record may hold multiple character-strings; they are joined with nothing between them (RFC 7208 §3.3):
; these two strings...
IN TXT "v=spf1 ip4:198.51.100.0/24 " "include:_spf.provider.example -all"
; ...are identical to:
IN TXT "v=spf1 ip4:198.51.100.0/24 include:_spf.provider.example -all"
Keep the record small: it SHOULD fit within 512 octets, and DNS messages under 450 octets fit in a single UDP packet (RFC 7208 §3.4).
Qualifiers
Every mechanism may carry a qualifier. It is optional and defaults to + (RFC 7208 §4.6.2).
| Qualifier | Symbol | Result when the mechanism matches |
|---|---|---|
| Pass | + | the client is authorized |
| Fail | - | the client is not authorized |
| SoftFail | ~ | the host is probably not authorized (a weak statement) |
| Neutral | ? | no assertion about authorization |
The practical upshot: -all is a hard “nobody else,” ~all is “probably nobody else but I’m not certain,” and ?all asserts nothing. If no mechanism matches and there is no all, the result is neutral (an implicit ?all).
Mechanisms
Mechanisms are evaluated left to right and defined in RFC 7208 §5.
| Mechanism | Syntax | Matches when | DNS lookup? |
|---|---|---|---|
all | all | always (use as the rightmost default) | no |
include | include:domain | the result of evaluating the referenced domain is pass | yes |
a | a / a:domain / a:domain/cidr | the client IP matches an A/AAAA record of the domain | yes |
mx | mx / mx:domain / mx:domain/cidr | the client IP is one of the domain’s MX hosts | yes |
ptr | ptr / ptr:domain | reverse DNS of the IP resolves within the domain | yes |
ip4 | ip4:net[/cidr] | the client IP is in the IPv4 range (default /32) | no |
ip6 | ip6:net[/cidr] | the client IP is in the IPv6 range (default /128) | no |
exists | exists:domain | a macro-built domain returns any A record | yes |
A few subtleties from the spec:
allalways matches and is the explicit default. Mechanisms listed afterallMUST be ignored, and anyredirectmodifier MUST be ignored wheneverallappears in the record (RFC 7208 §5.1).includeis misleadingly named. It does not splice in the other record’s mechanisms; it runs a full recursive check and uses only the result.passmakes the mechanism match;fail/softfail/neutraldo not match;temperror/permerrorpropagate; and anoneresult from the included domain returnspermerror(RFC 7208 §5.2).mxdoes the MX lookup then A/AAAA on each MX host, and a singlemxevaluation must not query more than 10 address records or it returnspermerror. Implicit MX rules MUST NOT be applied (RFC 7208 §5.4).ptrSHOULD NOT be published - it is slow, unreliable under DNS errors, and burdens.arpaname servers. Implementations must still support it, but you should not use it (RFC 7208 §5.5).
Modifiers
Modifiers are name=value pairs, not mechanisms, and do not directly produce a match (RFC 7208 §6).
redirect=domain- if no mechanism matches and there is noall, evaluation continues with the referenced domain’s record. If that domain has no SPF record, the result ispermerror(notnone).redirectis ignored ifallis present anywhere (RFC 7208 §6.1).exp=domain- on afailcaused by a mechanism match, an explanation TXT string is fetched and macro-expanded; it MUST be US-ASCII (RFC 7208 §6.2).
The 10-lookup limit (the thing that breaks real records)
This is the single most common operational SPF failure. SPF implementations MUST limit the total number of DNS-querying terms to 10 during evaluation. Exceed it and the result is permerror (RFC 7208 §4.6.4).
| Counts toward the 10 | Does NOT count |
|---|---|
include | all |
a | ip4 |
mx | ip6 |
ptr | exp modifier |
exists | |
redirect modifier |
So ip4/ip6 are free; include is expensive. Each include you add costs at least one lookup, plus whatever lookups that included record performs - the count is cumulative across the whole recursive evaluation, not per record. A handful of vendor include: statements, each chaining its own includes, is how a record silently crosses 10 and turns every check into permerror.
Two more limits in the same section (RFC 7208 §4.6.4):
- Void lookups. Queries that return RCODE 0 with zero answers, or NXDOMAIN, SHOULD be limited to two (a default of two is RECOMMENDED). Exceeding produces
permerror. Records pointing atinclude:ora:/mx:targets that no longer exist quietly burn this budget. - Time limit. A processor SHOULD allow at least 20 seconds of elapsed time for the check; if it runs longer, the result SHOULD be
temperror.
Results of evaluation
Defined in RFC 7208 §2.6.
| Result | Meaning |
|---|---|
| None | No valid domain to check, or no SPF record was found. |
| Neutral | The domain explicitly asserts nothing (? qualifier or implicit ?all). |
| Pass | The client is authorized for this identity. |
| Fail | The client is not authorized for this identity. |
| SoftFail | Weak statement that the host is probably not authorized. |
| Temperror | A transient (usually DNS) error; a retry may succeed. |
| Permerror | The published records could not be correctly interpreted; needs operator action. |
The distinction that matters operationally: temperror is “try again,” permerror is “your record is broken.” If your monitoring shows permerror, the cause is almost always the 10-lookup limit, void lookups, or two SPF records.
Why SPF breaks on forwarding
SPF authorizes IP addresses. When a message is forwarded - by a .forward rule, a mailing list, or any relay - the message reaches the final receiver from the forwarder’s IP, which is not listed in your domain’s SPF record. The receiver checks that IP against your record, finds it unauthorized, and SPF fails for your domain (this follows directly from the IP-versus-domain check in RFC 7208 §2.2).
This is not a bug in your record; it is inherent to how SPF works. It is the reason DMARC accepts DKIM as an alternative, and the reason ARC exists. DKIM, being a signature over content rather than a check on the connecting IP, can survive a plain forward where SPF cannot.
SPF and DMARC alignment
For DMARC, SPF is not used the way it is for raw SPF checks:
- DMARC relies solely on SPF validation of the
MAIL FROMidentity (RFC5321.MailFrom). SPF validation ofHELOis not used by DMARC (RFC 9989 §3.2.4, §4.4.2). - A bare SPF
passis not enough for DMARC. The validatedMAIL FROMdomain must also be in alignment with theFrom:(Author) domain (RFC 9989 §4.4.2).
This is why an ESP can show “SPF: pass” while your DMARC still fails: the pass is on the ESP’s bounce/return-path domain, which does not align with your visible From:. To make SPF contribute to DMARC, the MAIL FROM domain has to share your Organizational Domain (relaxed) or be identical (strict).
Common mistakes / what does NOT change
- Two SPF records. Publishing a second
v=spf1TXT record (often when adding a new provider) ispermerror, not additive. Merge into one (RFC 7208 §3.2). +allor a trailingallwith+.+allauthorizes the entire internet to send as your domain. It is the functional opposite of protection.ptr. Do not publish it (RFC 7208 §5.5).- Treating SPF as anti-spoofing for the
From:header. It validates the envelope, full stop. The visible-From:protection comes from DMARC, layered on top. - Assuming SPF survives forwarding. It does not, by design.
- Over-stuffed
include:chains. Every nested record’s lookups count toward your 10. Audit the full expansion, not just the top-level record.
What Egressif does
We keep your SPF record to a single, lookup-conscious TXT entry that stays well under the 10-term ceiling, account for every legitimate sending source so the record does not need a permissive ~all/+all crutch, and bias toward authenticating with aligned DKIM so that DMARC still passes on the forwarded and relayed paths where SPF cannot. We monitor for permerror and temperror in DMARC aggregate reports, because a record that crossed the lookup limit fails silently - mail keeps flowing while the SPF half of DMARC quietly stops counting.
Related references
- Email Authentication: SPF, DKIM, DMARC, ARC, BIMI SPF, DKIM, and DMARC are not interchangeable, and none of them on their own tells a receiver a message is safe. This is the map: what each layer checks, what a pass actually proves, and how alignment, ARC, and BIMI sit on top.
- DMARC in 2026 (RFC 9989, 9990, 9991) The RFC most "DMARC explained" articles still cite is obsolete. In May 2026 DMARC was re-issued as three Standards-Track RFCs. Here is everything that changed - tags, the Tree Walk, reporting, and the new p=reject guidance - in plain language, with records you can copy.
- DKIM Deep Dive (RFC 6376) DKIM signs a message so a domain can take cryptographic responsibility for it. This is the full RFC 6376 picture: selectors, the signature and key tags, simple vs relaxed canonicalization, the l= body-length trap, key rotation, and the crypto updates from RFC 8301 and RFC 8463.
- ARC: Authenticated Received Chain (RFC 8617) ARC preserves a message's authentication result across the intermediaries that break SPF and DKIM. This is the RFC 8617 picture: the three header fields, how a chain is built and validated, the chain-validation states, and the honest limits - ARC is advisory, not a pass.
- BIMI: Logos, DMARC, and Mark Certificates BIMI puts a brand logo next to authenticated mail, but it authenticates nothing itself - it rides on enforced DMARC. This covers the prerequisites, the SVG Tiny PS logo, VMC and CMC mark certificates, the DNS record, and the honest provider-support reality.
Tell us what you run today.
Domains, rough volume, current providers, and what hurts. You will get a straight answer on fit, and a real number, in one conversation.