Configuring SPF Records to Prevent Spoofing
Sender Policy Framework (SPF) is the first authentication layer that tells receiving mail servers which hosts are allowed to send mail using your domain in the MAIL FROM (envelope sender). After reading this guide you will be able to inventory every legitimate sending source, author a syntactically correct SPF TXT record at the apex, deploy it in soft-fail monitoring mode, then tighten it to a hard fail (-all) without bouncing legitimate mail. SPF only works as designed when it stays under the 10 DNS-lookup limit and exists as exactly one record per domain — both constraints break deployments more often than syntax errors do.
Key objectives:
- Build a single, valid SPF TXT record using
a,mx,ip4,ip6,include, andallmechanisms. - Understand the difference between
~all(soft fail) and-all(hard fail) and when to switch. - Stay under the hard 10 DNS-lookup limit to avoid
PermError, using flattening where necessary. - Verify the record with
digand an SPF validator, then align it with DKIM and DMARC.
How receivers evaluate SPF
When a mail server connects to a receiver, it issues a MAIL FROM:<[email protected]> command. The receiver extracts the domain (example.com), queries its TXT record, and looks for a string starting with v=spf1. It then walks the mechanisms left to right, matching the connecting IP against each one. The first mechanism that matches determines the result, and the trailing all is the catch-all default.
A mechanism can be prefixed with a qualifier:
| Qualifier | Result | Behavior on most receivers |
|---|---|---|
+ (default) |
Pass | Authenticated; accept |
~ |
SoftFail | Accept but mark suspicious |
- |
Fail | Reject or quarantine |
? |
Neutral | No policy; accept |
The mechanisms themselves resolve as follows: a matches the domain’s A/AAAA records, mx matches the IPs behind your MX hosts, ip4/ip6 match a literal address or CIDR block, and include pulls in another domain’s SPF record (used for SaaS senders like a marketing platform). Crucially, SPF authenticates the envelope sender, not the visible From: header — closing that gap is the job of DMARC, which checks alignment between the two. SPF and DKIM together provide the two authenticated identifiers DMARC relies on.
Prerequisites and environment setup
You need write access to your authoritative zone (registrar panel, Route 53, Cloudflare, or a BIND master), plus the dig utility from bind-utils/dnsutils:
dig -v
# DiG 9.18.18
sudo apt-get install dnsutils # Debian/Ubuntu
sudo dnf install bind-utils # RHEL/Fedora
Optionally install a CLI SPF checker to count lookups and detect errors before you publish:
pip install checkdmarc
checkdmarc example.com
You also need a complete list of every system that sends mail as your domain. Skipping this inventory is the number-one cause of legitimate mail being marked as spoofed after you switch to -all.
Step 1: Inventory every sending source
Enumerate all hosts and services that put your domain in MAIL FROM. Check your DMARC aggregate reports if you already have a p=none policy running, your application transactional providers, and your outbound mail logs.
Mail server (self-hosted MX) -> 203.0.113.10, 203.0.113.11
Transactional API (provider) -> include:spf.provider.net
Marketing platform -> include:_spf.mktg.example
Office suite -> include:_spf.google.com
Legacy app server -> 198.51.100.42
Side effect of missing one source: that source’s mail will be authenticated as SoftFail or Fail, landing in spam once enforcement is on. Run the inventory against at least 30 days of logs.
Step 2: Build the record
Combine your inventory into a single string, ordering mechanisms from most-specific to least, and end with the qualified all. Start with ~all for the monitoring phase:
example.com. 3600 IN TXT "v=spf1 ip4:203.0.113.10 ip4:203.0.113.11 ip4:198.51.100.42 include:spf.provider.net include:_spf.google.com ~all"
Mechanism notes:
ip4:/ip6:cost zero DNS lookups — prefer them for static infrastructure.include:,a,mx,ptr, andexistseach cost one or more lookups; nested includes count cumulatively.- Drop
aandmxif your sending IPs are static — replace them with explicitip4to save lookups. - Never use
+all; it authorizes the entire internet to spoof you.
Step 3: Publish a single SPF TXT at the apex
Publish exactly one v=spf1 record on the apex (example.com). Two SPF records produce a PermError and most receivers will treat the domain as having no SPF at all.
On Cloudflare or another provider, create a TXT record at the root (@) with the quoted string. With BIND, edit the zone file and bump the serial:
$TTL 3600
@ IN SOA ns1.example.com. hostmaster.example.com. (
2026062001 ; serial - increment on every change
7200 3600 1209600 3600 )
@ IN TXT "v=spf1 ip4:203.0.113.10 ip4:203.0.113.11 ip4:198.51.100.42 include:spf.provider.net include:_spf.google.com ~all"
rndc reload example.com
# zone reload up-to-date
Each subdomain that sends mail needs its own SPF record — SPF is not inherited from the apex. A subdomain with no SPF record falls back to the receiver’s default (usually neutral), so publish a dedicated record (often a strict v=spf1 -all) on any subdomain that should never send mail. For TTL trade-offs during cutover, see Mastering TTL Strategies.
Step 4: Test in monitoring mode, then tighten to -all
Leave ~all in place for at least one to two weeks while you collect DMARC aggregate reports. SoftFail tells receivers to accept but flag unauthorized mail, so nothing bounces while you watch for forgotten senders. Once reports show 100% of legitimate volume passing SPF with proper alignment, flip the qualifier:
example.com. 3600 IN TXT "v=spf1 ip4:203.0.113.10 ip4:203.0.113.11 ip4:198.51.100.42 include:spf.provider.net include:_spf.google.com -all"
-all instructs receivers to reject unauthenticated mail outright. Combine it with a DMARC policy moving from p=none to p=reject so spoofed messages claiming to be from your domain are dropped at the gateway.
Verification
Confirm the published record resolves cleanly and is a single string:
dig +short TXT example.com
# "v=spf1 ip4:203.0.113.10 ip4:203.0.113.11 ip4:198.51.100.42 include:spf.provider.net include:_spf.google.com -all"
Two lines of output here means two SPF records — fix that immediately. Next, count the DNS lookups and validate syntax with a checker:
checkdmarc example.com
# spf:
# record: "v=spf1 ip4:... -all"
# dns_lookups: 4
# valid: true
Send a test message to a mailbox you control and inspect the Authentication-Results header:
Authentication-Results: mx.receiver.example;
spf=pass (sender IP is 203.0.113.10) smtp.mailfrom=example.com;
spf=pass with smtp.mailfrom matching your domain confirms a working record. Cross-check propagation across resolvers as described in Debugging DNS Propagation Delays Across Global Resolvers.
Troubleshooting
PermError: too many DNS lookups
SPF allows a maximum of 10 DNS-resolving mechanisms (include, a, mx, ptr, exists) per evaluation. Exceeding it returns PermError, and DMARC treats SPF as failed. Diagnose with a checker:
checkdmarc example.com | grep -A2 dns_lookups
# dns_lookups: 13
# valid: false (too many DNS lookups)
Fix by flattening: resolve each include: to its underlying IP ranges and replace it with explicit ip4/ip6 mechanisms, which cost zero lookups.
dig +short TXT spf.provider.net
# expand nested includes recursively, then inline the resulting CIDRs:
# v=spf1 ip4:192.0.2.0/24 ip4:198.51.100.0/22 ... -all
Flattening trades safety for lookup budget: if the provider rotates IPs, your flattened record goes stale, so automate it or use a hosted flattening service that refreshes the record for you.
Multiple SPF records
If dig +short TXT example.com returns more than one v=spf1 string, receivers raise PermError and ignore SPF entirely. Merge every authorized source into a single record and delete the extras. This frequently happens when a new SaaS vendor instructs you to “add this SPF record” instead of merging their include: into your existing one.
Subdomain inheritance assumed but absent
Mail sent from mail.example.com is not covered by the apex record. If dig +short TXT mail.example.com returns nothing, the subdomain has no SPF policy:
dig +short TXT mail.example.com
# (empty) -> publish a dedicated record for this subdomain
Add an explicit SPF TXT for each sending subdomain, and lock down non-sending subdomains with "v=spf1 -all".
Mail still marked as spoofed after -all
Check the Authentication-Results header for spf=fail. The connecting IP is missing from your record — capture it from the header, confirm it is a legitimate sender, and add the matching ip4/ip6 or include. Until then, revert to ~all so you do not block real mail while you correct the inventory.
Frequently Asked Questions
Does SPF stop someone from spoofing the visible From address?
No. SPF only authenticates the envelope MAIL FROM, not the header From: your users see. You need DMARC alignment plus DKIM to protect the visible From address.
Should I use ~all or -all?
Start with ~all (soft fail) during a monitoring window so misconfigured senders are flagged rather than rejected, then move to -all (hard fail) once DMARC reports confirm all legitimate mail passes.
How many DNS lookups am I allowed?
A maximum of 10 lookup-causing mechanisms per evaluation. Going over returns PermError; use ip4/ip6 literals or flatten your includes to stay under the limit.
Can I have two SPF records for different providers?
No. A domain must have exactly one v=spf1 TXT record. Merge multiple providers’ include: mechanisms into a single record, or receivers will reject it as a PermError.
Do subdomains inherit the apex SPF record?
No. SPF is evaluated per exact domain name. Publish a separate SPF record on every subdomain that sends mail, and a v=spf1 -all record on those that should not.
Related
- Setting Up DKIM Signing for Your Domain
- Deploying DMARC with Monitoring and Enforcement
- Configuring SRV Records for SIP and XMPP Services
- Mastering TTL Strategies
Back to Advanced SRV & MX Routing