Introduction

SPF (Sender Policy Framework) records in DNS specify which mail servers are authorized to send email for your domain. When SPF records are missing, malformed, or incorrect, your legitimate emails may be marked as spam or rejected outright. SPF problems are particularly tricky because they don't cause immediate failures - you might only discover them when recipients report missing emails or when checking delivery logs.

Symptoms

  • Emails marked as spam despite legitimate sending
  • DMARC reports show SPF failures
  • Recipient mail servers reject emails with "SPF fail"
  • Email headers show "Received-SPF: fail"
  • Third-party sending services (Mailchimp, SendGrid) trigger SPF failures
  • Mail delivery works but deliverability scores are poor
  • DMARC aggregate reports show authentication failures

Common Causes

  • SPF record missing from DNS
  • SPF syntax errors (missing quotes, invalid mechanisms)
  • Too many DNS lookups (max 10 including chained lookups)
  • Not including all authorized senders
  • Including senders in wrong order
  • Using deprecated SPF record type instead of TXT
  • Duplicate SPF records causing conflict

Step-by-Step Fix

  1. 1.Query current SPF record from DNS.

```bash # SPF is stored in TXT records dig example.com TXT +short

# Look for v=spf1 in output # Example: "v=spf1 include:_spf.google.com ~all"

# Also check for SPF type record (deprecated but sometimes used) dig example.com SPF +short

# Use grep to find SPF specifically dig example.com TXT +short | grep "v=spf1"

# Check authoritative server directly dig @ns1.yourprovider.com example.com TXT +short | grep spf ```

  1. 1.Parse and validate the SPF record syntax.

```bash # Install SPF parser if available # Or use online tools like: # - https://mxtoolbox.com/spf.aspx # - https://dmarcian.com/spf-survey/

# Manual validation - check for common errors: spf_record=$(dig example.com TXT +short | grep "v=spf1")

# Common syntax errors to check: # - Missing version: v=spf1 must be first # - Invalid mechanism: include:domain (needs colon) # - Wrong qualifier: -all vs ~all vs ?all # - Unclosed quotes in zone file # - Extra spaces between mechanisms

echo "Current SPF: $spf_record"

# Check for common issues: # - Multiple SPF records (invalid) dig example.com TXT +short | grep -c "v=spf1" # Should be exactly 1

# - SPF record too long (>255 chars needs splitting) echo "$spf_record" | wc -c ```

  1. 1.Check SPF record length and DNS UDP limits.

```bash # DNS TXT records over 255 bytes need special handling # Many providers auto-split, but some don't

# Check record length dig example.com TXT +short | grep "v=spf1" | tr -d '"' | wc -c

# If >450 characters, you may hit UDP limits # Solutions: # - Use include: to delegate to external SPF # - Remove unnecessary mechanisms # - Use ip4:/ip6: instead of include: where possible

# Test with TCP fallback dig +tcp example.com TXT ```

  1. 1.Count DNS lookups in your SPF record.

```bash # SPF allows maximum 10 DNS lookups including nested # Count lookups: include:, a, mx, exists, redirect

spf=$(dig example.com TXT +short | grep "v=spf1" | tr -d '"')

# Count include directives echo "$spf" | grep -o "include:" | wc -l

# Count a: directives (each is a lookup unless no domain specified) echo "$spf" | grep -o "a:" | wc -l

# Count mx directive (adds lookups for each MX) echo "$spf" | grep -o "mx" | wc -l

# If total lookups > 10, SPF will fail # Use SPF flattening services or direct IP includes

# Check nested includes: # Each include: may itself contain more lookups for domain in $(echo "$spf" | grep -oP 'include:\K[^ ]+'); do echo "Checking $domain:" dig $domain TXT +short | grep "v=spf1" done ```

  1. 1.Verify all authorized senders are included.

```bash # Common senders that need to be in SPF:

# Google Workspace include:_spf.google.com

# Microsoft 365 include:spf.protection.outlook.com

# SendGrid include:sendgrid.net

# Mailchimp include:servers.mcsv.net

# Amazon SES include:amazonses.com

# Your own mail server IP ip4:192.0.2.10 # or a:mail.example.com

# Check what senders you actually use: # - Review email headers from your outgoing mail # - Check with IT/admin about mail services # - Review marketing tool configurations ```

  1. 1.Test SPF validation from external perspective.

```bash # Use online SPF validators: # - https://mxtoolbox.com/spf.aspx # - https://dmarcian.com/spf-survey/ # - https://www.kitterman.com/spf/validate.html

# Manual test with sender address: # Check if IP is authorized for domain # Format: IP sender-domain sender-email

# Using dig to trace SPF resolution: domain="example.com" spf=$(dig $domain TXT +short | grep "v=spf1" | tr -d '"') echo "SPF for $domain: $spf"

# Test specific sender IP # (This requires external tools for full validation) ```

  1. 1.Fix duplicate or conflicting SPF records.

```bash # Check for multiple SPF records (invalid) spf_count=$(dig example.com TXT +short | grep -c "v=spf1")

if [ "$spf_count" -gt 1 ]; then echo "ERROR: Multiple SPF records found - must merge into one" dig example.com TXT +short | grep "v=spf1" fi

# Multiple SPF records cause undefined behavior # Merge all into single record: # v=spf1 include:one.com include:two.com ip4:192.0.2.0/24 ~all ```

  1. 1.Construct correct SPF record for your setup.

```bash # Basic SPF patterns:

# Google Workspace only: "v=spf1 include:_spf.google.com ~all"

# Microsoft 365 only: "v=spf1 include:spf.protection.outlook.com ~all"

# Self-hosted mail server: "v=spf1 ip4:192.0.2.10 -all"

# Multiple providers (Google + SendGrid + own server): "v=spf1 ip4:192.0.2.10 include:_spf.google.com include:sendgrid.net ~all"

# Strict SPF (reject unauthorized): "v=spf1 ip4:192.0.2.0/24 -all"

# Testing mode (use ~all during testing): "v=spf1 include:_spf.google.com ~all"

# Production mode (use -all after testing): "v=spf1 include:_spf.google.com -all" ```

  1. 1.Add or update SPF record in DNS.

```bash # BIND zone file format: example.com. IN TXT "v=spf1 include:_spf.google.com include:sendgrid.net ip4:192.0.2.0/24 ~all"

# For records over 255 chars, split into multiple strings: example.com. IN TXT "v=spf1 include:_spf.google.com include:sendgrid.net " "ip4:192.0.2.0/24 ~all"

# Update zone: # 1. Increment SOA serial # 2. Add/update TXT record # 3. Reload zone rndc reload example.com

# For DNS control panels: # Type: TXT # Name: @ (or blank for apex) # Value: v=spf1 include:_spf.google.com ~all # TTL: 3600

# Note: Some panels require quotes, others don't ```

  1. 1.Verify SPF works with DMARC and DKIM.

```bash # Check DMARC record dig _dmarc.example.com TXT +short

# Should return something like: # "v=DMARC1; p=none; rua=mailto:dmarc@example.com"

# Check DKIM record (if using) dig default._domainkey.example.com TXT +short

# Test email authentication with online tools: # - https://mail-tester.com # - https://mxtoolbox.com/deliverability

# Or send test email and check headers: # Look for: Authentication-Results header # Should show: spf=pass ```

Verification

Complete SPF verification checklist:

```bash # 1. Single SPF record exists echo "=== SPF Record Count ===" dig example.com TXT +short | grep -c "v=spf1"

# 2. SPF syntax is valid echo "=== SPF Record ===" dig example.com TXT +short | grep "v=spf1"

# 3. Count DNS lookups echo "=== DNS Lookup Count ===" spf=$(dig example.com TXT +short | grep "v=spf1" | tr -d '"') echo "include: $(echo $spf | grep -o 'include:' | wc -l)" echo "a: $(echo $spf | grep -o 'a:' | wc -l)" echo "mx: $(echo $spf | grep -o 'mx' | wc -l)"

# 4. Test SPF resolution echo "=== Nested SPF Records ===" for domain in $(echo "$spf" | grep -oP 'include:\K[^ ]+'); do echo "Include: $domain" dig $domain TXT +short | grep "v=spf1" done

# 5. Check DMARC alignment echo "=== DMARC Record ===" dig _dmarc.example.com TXT +short

# 6. Verify from external resolver echo "=== External Resolution ===" dig @8.8.8.8 example.com TXT +short | grep spf ```

SPF Qualifiers Reference

```bash # +all (pass) - rarely used, allows all # -all (fail) - hard fail, reject unauthorized # ~all (softfail) - accept but mark suspicious # ?all (neutral) - no policy, accept

# Testing progression: # 1. Start with ?all to monitor # 2. Move to ~all after confirming no issues # 3. Eventually use -all for strict enforcement ```

Monitor DMARC reports to identify legitimate senders missing from your SPF record before moving to strict enforcement.