Set up SPF¶
Hardening runbook. Publishes an SPF record so receiving servers can check that mail claiming to come from a domain was sent by a host the domain authorises, and optionally checks inbound mail against senders’ SPF records. SPF is one leg of the sender-authentication chain; see the mail stack for how it fits with DKIM and DMARC.
When to run¶
When setting up outbound mail for a domain. When a relay and authentication review shows no SPF record, or one that does not enforce. After changing which servers send mail for the domain.
Two parts¶
Publishing the domain’s own SPF record (so others trust mail from the domain) and checking inbound senders’ records (so the server rejects spoofed inbound mail). The first is essential and low-risk; the second is optional and needs more care.
Publishing the record¶
Add a TXT record at the domain root through the DNS provider. To authorise the hosts listed in the domain’s MX records:
v=spf1 mx -all
To authorise one specific host:
v=spf1 a:mail.example.com -all
The ending is the point. -all (hardfail) tells receivers to reject mail from unlisted hosts. ~all (softfail) tells them to accept but mark it. Exact entry format varies by DNS provider.
Risk¶
-all published before every legitimate sending source is listed causes receivers to reject genuine mail from the omitted sources (a marketing platform, a ticketing system, a separate app server). List every source that sends as the domain first. Starting with ~all and tightening to -all once the record is confirmed complete is the lower-risk path.
Checking inbound mail (optional)¶
The postfix-policyd-spf-python agent adds inbound SPF checking to Postfix. In /etc/postfix-policyd-spf-python/policyd-spf.conf, a test-only mode applies the check without rejecting, so the impact is visible in the logs first:
TestOnly = 1
Wire the agent into Postfix in /etc/postfix/master.cf:
policyd-spf unix - n n - 0 spawn user=nobody argv=/usr/bin/policyd-spf
And in main.cf, add the check after reject_unauth_destination in the restrictions, with a timeout so slow lookups do not abort it:
policyd-spf_time_limit = 3600
smtpd_recipient_restrictions = ..., reject_unauth_destination, check_policy_service unix:private/policyd-spf
Restart Postfix:
sudo systemctl restart postfix
Run with TestOnly = 1 long enough to confirm legitimate inbound mail is not being failed, then set it to 0 to enforce.
Verify¶
For the published record, check it resolves and reads as intended:
dig +short TXT example.com | grep spf1
Then send a test message to an external mailbox and confirm the received headers show an SPF pass. External tools (MXToolbox, appmaildev) validate the record and test delivery.
For inbound checking, the SPF result appears in incoming message headers and in /var/log/mail.log.
Done¶
SPF record published and resolving, listing all legitimate sending sources, ending in -all once confirmed complete. Test mail to an external mailbox passes SPF. If inbound checking is enabled, it enforces after a clean test-only period.
Rollback¶
Soften -all to ~all in the TXT record if legitimate mail is being rejected, while the missing source is identified. For inbound checking, set TestOnly = 1 to stop rejecting while keeping the result in headers.
Follow-up¶
SPF alone does not stop From-header spoofing. Pair with DKIM and DMARC; the mail stack explains why all three are needed.
SPF breaks on forwarding. DKIM survives it, which is part of why both are published.