Module 5 · Lesson 3

SPF: Sender Policy Framework

14 min read

spfemail-authenticationdnstxt-recorddeliverability

SPF: Sender Policy Framework

Someone is spoofing your domain. They're sending spam that looks like it comes from @yourcompany.com. Recipients see your domain in the From field. Your reputation tanks. Support tickets start coming in. You didn't configure SPF.

SPF gives you a TXT record where you publish exactly which IP addresses and mail servers are authorized to send email as your domain. Receiving servers check this record and decide whether to accept, flag, or reject messages that fail the check.

How SPF Works

When a sending server delivers email, the receiving server checks the MAIL FROM address (the envelope sender, not the From: header you see in your email client) and looks up the SPF TXT record for that domain.

dig TXT example.com +short

If the sending server's IP matches the policy, the check passes. If it doesn't, the outcome depends on the all mechanism qualifier in your record.

Record Syntax

v=spf1 <mechanisms> <qualifier>all

A real example:

v=spf1 ip4:203.0.113.0/24 ip6:2001:db8::/32 include:_spf.google.com ~all

Breaking this down:

v=spf1 — Required. Identifies this as an SPF record.

ip4:203.0.113.0/24 — Allow this IPv4 range to send as this domain.

ip6:2001:db8::/32 — Allow this IPv6 range.

include:_spf.google.com — Look up the SPF record at _spf.google.com and add all its allowed IPs to this policy. Used when you send email through third parties like Google Workspace, SendGrid, Mailchimp.

~all — The catch-all qualifier.

The all Qualifier Options

This is where most misconfiguration happens:

QualifierMeaningBehavior
+allAnyone can sendPassing. Equivalent to no SPF. Don't use this.
~allSoft failMark as suspicious, but don't reject.
-allHard failReject messages from unauthorized senders.
?allNeutralNo policy. Treated like no SPF.

+all is catastrophically wrong. It tells receiving servers that any IP in the world is authorized to send as your domain. It's the same as publishing no SPF at all, except it's an explicit authorization. This shows up in shared hosting configurations, copy-paste from bad examples, and legacy setups nobody audited. If you see +all in your SPF record, fix it now.

-all vs ~all: Use -all in production once you're confident you've listed every legitimate sending source. Use ~all while you're auditing. The practical difference is that some receiving servers that would reject on -all will only flag and pass on ~all. Moving from ~all to -all is a signal that you've done the work to enumerate your senders.

Other Mechanisms

a — Allow the A record(s) of the domain itself.

mx — Allow the MX record(s) of the domain. Convenient for domains that both send and receive through the same server.

ptr — Reverse DNS check. Slow, deprecated, avoid it.

exists — Complex conditional logic. Rarely used.

redirect — Use another domain's SPF record entirely (replaces the whole record, unlike include).

The 10-Lookup Limit

SPF evaluation triggers DNS lookups. Each include:, a, mx, ptr, exists, and redirect mechanism counts against a limit of 10. If the evaluation exceeds 10 lookups, the result is permerror — a permanent error that typically causes the message to be treated as failed SPF.

The trap: nested include: statements each trigger their own lookups, and those included records can themselves include others. Adding include:sendgrid.net might look like one lookup, but SendGrid's SPF record includes three more, which include others. You burn through the limit fast.

Audit your lookup count:

# Use mxtoolbox.com/spf.aspx to visualize the full lookup tree
# Or use the command line:
dig TXT example.com +short
# Then follow each include: manually
dig TXT _spf.google.com +short
# Count them up

If you're over 10, flatten your SPF record by resolving include: statements to their actual IP ranges and replacing the includes with direct ip4: and ip6: entries. The downside: if your provider changes their sending IPs, your flattened record becomes stale. Some services (Dmarcian, Valimail) automate this flattening with DNS-based macros.

The Multiple SPF Records Problem

You can only have one SPF TXT record per domain. Not two. One.

If you publish two TXT records starting with v=spf1, the result is permerror. The receiving server doesn't know which to use and fails the lookup.

This happens constantly when someone adds a new email provider ("just add their include: to your DNS") and creates a second record instead of editing the existing one.

# Check if you have multiple SPF records
dig TXT yourdomain.com | grep "v=spf1"

If that returns two lines, you have a problem. Merge them into one record.

The Forwarding Problem

SPF has a fundamental limitation with email forwarding. Here's what happens:

  1. user@example.com has an SPF record listing their mail server at 198.51.100.1.
  2. Someone sends them an email.
  3. The email is forwarded from a forwarding service at 198.51.100.50.
  4. The receiving server checks SPF for example.com.
  5. 198.51.100.50 is not in the SPF record. SPF fails.
  6. The forwarded email gets flagged or rejected.

The forwarding server changed the envelope sender's IP without changing the domain. SPF checks the sending IP against the domain's policy — and the forwarder's IP isn't authorized.

Solutions:

  • SRS (Sender Rewriting Scheme): The forwarding server rewrites the envelope MAIL FROM to use its own domain. SPF now checks the forwarder's domain, which it passes. Some forwarding services support this.
  • DKIM survives forwarding — which is why having both SPF and DKIM matters. If DKIM signs the message at the origin, the signature survives forwarding even when SPF fails.

Auditing an SPF Record

Run through this checklist:

  1. Do you have exactly one v=spf1 TXT record?
  2. Does it include all services that send as your domain? (transactional email, marketing, CRM, ticketing, etc.)
  3. Does it use ~all or -all (not +all)?
  4. Is it under the 10-lookup limit?
  5. Is the record under 255 characters? (DNS TXT records have a 255-character string limit, though multiple strings can be concatenated)

A realistic production record for a company using Google Workspace, SendGrid, and a custom MTA:

v=spf1 include:_spf.google.com include:sendgrid.net ip4:203.0.113.10 ~all

Once you've verified delivery from all sources, move to:

v=spf1 include:_spf.google.com include:sendgrid.net ip4:203.0.113.10 -all

Key Takeaways

  • SPF is a TXT record at your domain root. One record per domain.
  • +all authorizes every IP on the internet to send as you. Never use it.
  • Use ~all while auditing, -all once you're confident in your sender list.
  • Each include: triggers DNS lookups. Stay under 10 total.
  • Multiple SPF records = permerror = failed check.
  • SPF breaks with email forwarding. DKIM is what survives forwarding.

Further Reading

  • RFC 7208 — Sender Policy Framework (SPF) for Authorizing Use of Domains in Email
  • kitterman.com/spf/validate.html — SPF validator
  • mxtoolbox.com/spf.aspx — visualize the full lookup tree

Up Next

Lesson 04 covers DKIM: the cryptographic layer that signs email at the origin and survives forwarding — the mechanism that makes your emails verifiable end-to-end even when SPF fails.