FreeBSD Email Deliverability Guide: SPF, DKIM, DMARC
Running your own mail server on FreeBSD is straightforward. Getting your emails into recipients' inboxes instead of their spam folders is the hard part. The difference comes down to three DNS-based authentication mechanisms: SPF, DKIM, and DMARC. Without all three configured correctly, major providers like Gmail, Outlook, and Yahoo will silently drop or flag your messages.
This guide walks through the complete setup on FreeBSD using Postfix and OpenDKIM. Every step includes verification commands so you know each piece works before moving to the next.
Why Deliverability Matters
Google and Yahoo both enforce strict sender authentication as of early 2024. If you send more than 5,000 messages per day, you must have SPF, DKIM, and DMARC configured. Even for low-volume senders, missing authentication means your messages land in spam.
The three mechanisms work together:
- SPF tells receiving servers which IP addresses are authorized to send mail for your domain.
- DKIM adds a cryptographic signature to each outgoing message, proving it was not altered in transit and originated from your domain.
- DMARC ties SPF and DKIM together with a policy that tells receivers what to do when authentication fails.
All three are implemented as DNS TXT records. DKIM additionally requires a signing daemon running on your mail server.
Prerequisites
This guide assumes you have:
- FreeBSD 14.x with a working Postfix installation
- A domain name with DNS you can edit
- A static IP address with a valid reverse DNS (PTR) record
- Root or sudo access to your mail server
Check your PTR record first. This is the single most common deliverability problem:
shdig -x YOUR_SERVER_IP +short
The result must match the hostname your mail server uses in its HELO/EHLO greeting. If it does not match, fix it with your hosting provider before proceeding.
Step 1: Configure SPF
SPF is the simplest of the three. It is a single DNS TXT record on your domain.
Basic SPF Record
For a server that sends mail only from one IP address:
sh# DNS TXT record for example.com v=spf1 ip4:203.0.113.50 -all
The v=spf1 prefix is required. ip4: specifies an allowed sender IP. -all means "reject everything not explicitly listed." Use ~all (softfail) during testing, then switch to -all once verified.
Common SPF Patterns
If you also send through a third-party service:
shv=spf1 ip4:203.0.113.50 include:_spf.google.com -all
For multiple servers on a subnet:
shv=spf1 ip4:203.0.113.0/24 -all
SPF Lookup Limit
SPF has a hard limit of 10 DNS lookups. Each include: and redirect= counts as one lookup, and nested includes count against the same limit. Exceeding 10 lookups causes a permanent error (permerror), which many receivers treat as a fail.
Check your lookup count:
shpkg install -y py311-pyspf spfquery -s test@example.com -i 203.0.113.50 -h mail.example.com
Verify SPF
After adding the DNS record, verify it resolves correctly:
shdig TXT example.com +short
You should see your SPF record in the output. DNS propagation can take up to 48 hours, but most providers update within minutes.
Step 2: Configure DKIM with OpenDKIM
DKIM requires software on your mail server to sign outgoing messages and a DNS record containing your public key.
Install OpenDKIM
shpkg install opendkim
Generate Keys
Create a directory for your keys and generate a 2048-bit RSA key pair:
shmkdir -p /usr/local/etc/opendkim/keys/example.com opendkim-genkey -b 2048 -d example.com -s 202604 -D /usr/local/etc/opendkim/keys/example.com/
The -s 202604 flag sets the selector. Use a date-based selector so you can rotate keys without ambiguity. This creates two files:
202604.private-- the private key used for signing202604.txt-- the DNS TXT record containing the public key
shcat /usr/local/etc/opendkim/keys/example.com/202604.txt
The output is the DNS record you need to add. It will look something like:
sh202604._domainkey IN TXT ( "v=DKIM1; k=rsa; p=MIIBIjANBgkqhki..." )
Add this as a TXT record for 202604._domainkey.example.com in your DNS.
Configure OpenDKIM
Edit /usr/local/etc/opendkim.conf:
shSyslog yes SyslogSuccess yes LogWhy yes Canonicalization relaxed/simple Mode sv SubDomains no OversignHeaders From AutoRestart yes AutoRestartRate 10/1M Background yes DNSTimeout 5 SignatureAlgorithm rsa-sha256 KeyTable refile:/usr/local/etc/opendkim/key.table SigningTable refile:/usr/local/etc/opendkim/signing.table InternalHosts /usr/local/etc/opendkim/trusted.hosts Socket inet:8891@localhost PidFile /var/run/opendkim/opendkim.pid UMask 007 UserID opendkim:opendkim
Create the key table at /usr/local/etc/opendkim/key.table:
sh202604._domainkey.example.com example.com:202604:/usr/local/etc/opendkim/keys/example.com/202604.private
Create the signing table at /usr/local/etc/opendkim/signing.table:
sh*@example.com 202604._domainkey.example.com
Create the trusted hosts file at /usr/local/etc/opendkim/trusted.hosts:
sh127.0.0.1 ::1 localhost example.com
Set permissions:
shchown -R opendkim:opendkim /usr/local/etc/opendkim chmod 600 /usr/local/etc/opendkim/keys/example.com/202604.private
Integrate OpenDKIM with Postfix
Add the milter configuration to /usr/local/etc/postfix/main.cf:
shmilter_default_action = accept milter_protocol = 6 smtpd_milters = inet:localhost:8891 non_smtpd_milters = inet:localhost:8891
Start Services
shsysrc opendkim_enable=YES service opendkim start service postfix restart
Verify DKIM
Test that your DNS record is correct:
shopendkim-testkey -d example.com -s 202604 -vvv
You should see key OK in the output. If you see key not secure, that means DNSSEC is not configured for your domain -- this is a warning, not a failure.
Send a test email and check the headers:
shecho "DKIM test" | mail -s "DKIM Test" check-auth@verifier.port25.com
The reply will include detailed authentication results for both SPF and DKIM.
Step 3: Configure DMARC
DMARC tells receiving mail servers what to do when SPF or DKIM checks fail. It also enables aggregate reporting so you can monitor authentication results.
DMARC Record
Add a TXT record for _dmarc.example.com:
shv=DMARC1; p=none; rua=mailto:dmarc-reports@example.com; ruf=mailto:dmarc-forensic@example.com; adkim=r; aspf=r; pct=100
Start with p=none to collect data without affecting delivery. The fields:
p=none-- policy: none (monitor only), quarantine, or rejectrua=-- address for aggregate reports (daily XML summaries)ruf=-- address for forensic reports (per-failure details, not all providers send these)adkim=r-- DKIM alignment: relaxed (r) or strict (s)aspf=r-- SPF alignment: relaxed or strictpct=100-- percentage of messages to apply the policy to
DMARC Rollout Strategy
Do not jump straight to p=reject. Follow this progression:
- Week 1-2:
p=none-- collect reports, identify any legitimate mail sources failing authentication - Week 3-4:
p=quarantine; pct=25-- quarantine 25% of failing messages - Week 5-6:
p=quarantine; pct=100-- quarantine all failing messages - Week 7+:
p=reject-- reject all failing messages
Parse DMARC Reports
Aggregate reports arrive as XML files, usually gzipped. Install a parser:
shpkg install -y py311-pip pip install parsedmarc
Process incoming reports:
shparsedmarc -i /path/to/report.xml.gz
For automated processing, configure parsedmarc to read from the mailbox directly and output to Elasticsearch or a CSV file.
Step 4: Additional DNS Records
MTA-STS
MTA-STS (Mail Transfer Agent Strict Transport Security) tells sending servers that your domain supports TLS and they should not deliver mail over unencrypted connections.
Create a file at https://mta-sts.example.com/.well-known/mta-sts.txt:
shversion: STSv1 mode: enforce mx: mail.example.com max_age: 604800
Add a DNS TXT record for _mta-sts.example.com:
shv=STSv1; id=20260409
TLSRPT
TLS reporting lets you receive reports about TLS connection failures. Add a TXT record for _smtp._tls.example.com:
shv=TLSRPTv1; rua=mailto:tls-reports@example.com
Step 5: Testing and Validation
Online Testing Tools
Send test messages to these services:
- mail-tester.com -- gives a 1-10 score with detailed breakdown
- check-auth@verifier.port25.com -- returns authentication results by email
- Gmail -- check the "Show original" option on a received message; look for
spf=pass,dkim=pass, anddmarc=pass
Command-Line Testing
Verify all DNS records at once:
sh# SPF dig TXT example.com +short | grep spf # DKIM dig TXT 202604._domainkey.example.com +short # DMARC dig TXT _dmarc.example.com +short # MTA-STS dig TXT _mta-sts.example.com +short # TLSRPT dig TXT _smtp._tls.example.com +short
Test Email Sending
sh# Install swaks (Swiss Army Knife for SMTP) pkg install -y swaks # Send a test message through your server swaks --to recipient@gmail.com --from sender@example.com --server localhost --tls
Check the received message headers for Authentication-Results. You want to see:
shellspf=pass dkim=pass dmarc=pass
Step 6: Monitoring
OpenDKIM Statistics
Enable statistics in opendkim.conf:
shStatistics /var/db/opendkim/stats.dat
View statistics:
shopendkim-stats /var/db/opendkim/stats.dat
Log Monitoring
Watch for DKIM signing and verification results in your mail log:
shtail -f /var/log/maillog | grep -i dkim
Common log entries to watch for:
DKIM-Signature field added-- outgoing messages are being signeddkim=pass-- incoming messages passing DKIM verificationkey retrieval failed-- DNS issues with your DKIM record
Automated Monitoring
Create a periodic script at /usr/local/etc/periodic/daily/500.dkim-check:
sh#!/bin/sh # Check DKIM key validity result=$(opendkim-testkey -d example.com -s 202604 2>&1) if echo "$result" | grep -q "key OK"; then echo "DKIM key for example.com: OK" else echo "WARNING: DKIM key check failed for example.com" echo "$result" fi
shchmod +x /usr/local/etc/periodic/daily/500.dkim-check
Troubleshooting
SPF Failures
The most common SPF failure is sending mail from an IP not listed in your SPF record. This happens when you add a new server, use a relay, or your IP changes.
sh# Check which IP the receiving server sees swaks --to test@example.com --server localhost --h-From sender@example.com 2>&1 | grep "connection"
DKIM Failures
DKIM failures usually come from one of three issues:
- DNS record mismatch -- the public key in DNS does not match the private key on your server
- Message modification -- a mailing list or forwarder altered the message body after signing
- Selector mismatch -- the selector in your signing config does not match your DNS record
Verify your key pair matches:
sh# Extract public key from private key openssl rsa -in /usr/local/etc/opendkim/keys/example.com/202604.private -pubout 2>/dev/null # Compare with DNS record dig TXT 202604._domainkey.example.com +short
DMARC Alignment Failures
DMARC can fail even when both SPF and DKIM pass individually. This happens when the domain in the From header does not align with the domain that passed SPF or DKIM.
For SPF alignment: the envelope sender (MAIL FROM) domain must match the From header domain. For DKIM alignment: the d= domain in the DKIM signature must match the From header domain.
With relaxed alignment (adkim=r, aspf=r), subdomains are allowed to match. With strict alignment, exact matches are required.
Key Rotation
Rotate your DKIM keys at least once per year. The process with zero downtime:
sh# Generate new key with new selector opendkim-genkey -b 2048 -d example.com -s 202610 -D /usr/local/etc/opendkim/keys/example.com/ # Add new DNS record for 202610._domainkey.example.com # Wait for DNS propagation (check with dig) # Update key.table and signing.table to use new selector # Restart opendkim # Keep old DNS record for at least 7 days (messages in transit may still carry old signatures) # Remove old DNS record after 7 days
FAQ
Do I need all three (SPF, DKIM, DMARC) or can I skip one?
You need all three. Gmail and Yahoo require SPF and DKIM at minimum, and DMARC is required for bulk senders. Even for low-volume senders, missing any one of them significantly increases spam classification risk.
My SPF record is already at 10 DNS lookups. How do I add more senders?
Flatten your SPF record by replacing include: statements with the actual IP addresses they resolve to. Tools like dmarcian.com/spf-survey help identify what each include resolves to. You can also use the ip4: and ip6: mechanisms, which do not count against the lookup limit.
Should I use a 1024-bit or 2048-bit DKIM key?
Use 2048-bit. While 1024-bit keys still work, they are increasingly considered insufficient. Some DNS providers have trouble with the longer TXT records that 2048-bit keys require -- in that case, split the record into two quoted strings within a single TXT record.
How long does DNS propagation take for these records?
Most providers update within 5-30 minutes. The maximum is typically 48 hours. You can check propagation across global DNS servers using tools like dnschecker.org.
I set DMARC to p=reject and now legitimate mail is bouncing. What do I do?
Immediately change to p=none to stop the bleeding. Review your DMARC aggregate reports to identify which legitimate sources are failing. Add those sources to your SPF record or configure them with DKIM signing for your domain. Then gradually re-tighten the policy.
Does this work with Sendmail instead of Postfix?
Yes. OpenDKIM integrates with Sendmail through the same milter interface. The opendkim.conf configuration is identical. For Sendmail, add the milter to your sendmail.mc: INPUT_MAIL_FILTER('opendkim', 'S=inet:8891@localhost').
How do I handle DKIM with mailing lists?
Mailing lists that modify message bodies (adding footers, changing Subject) will break DKIM signatures. Use l= (body length limit) in your DKIM signatures with caution, or configure your DMARC with relaxed alignment. ARC (Authenticated Received Chain) is the long-term solution, but adoption is still incomplete.