
Set Up SPF, DKIM and DMARC on Postfix with OpenDKIM
Getting mail to authenticate is three separate jobs that people lump together: SPF says which hosts may send for your domain, DKIM cryptographically signs your outbound mail, and DMARC tells receivers what to do when the first two disagree with the From header. SPF and DMARC are pure DNS. DKIM needs a signing daemon wired into Postfix. This guide walks all three.
Assumptions
I'm assuming this concrete stack:
- Debian 12 or Ubuntu 22.04+ with systemd.
- Postfix already installed and delivering mail for your domain.
- OpenDKIM as the signing milter. (rspamd can also sign DKIM; if you already run rspamd for filtering, do it there instead and skip the OpenDKIM section.)
Paths below are the Debian/Ubuntu defaults. On RHEL/AlmaLinux the package is also opendkim but some paths differ — confirm on your system.
Throughout, example.com and the selector mail are examples. Substitute your real domain and a selector of your choosing everywhere.
SPF: one DNS record
SPF is just a TXT record at your domain apex listing who may send. The most common form authorises your MX hosts:
; EXAMPLE — adapt to your domain and infrastructure
example.com. IN TXT "v=spf1 mx -all"
mx authorises the hosts in your MX records. -all means "reject anything else." If you also send through a smarthost or a third party, add their mechanism (for example include: for a provider, or ip4:/ip6: for a fixed address) before -all. Keep the whole record within the DNS lookup limits — SPF allows at most 10 DNS-querying mechanisms.
You publish exactly one SPF record per domain. Two SPF records is a common misconfiguration that makes SPF fail entirely.
There's no Postfix change for outbound SPF — it's about how others judge your mail. If you also want to check SPF on inbound mail, that's a separate package (postfix-policyd-spf-python) and outside this guide.
DKIM: install and generate a key
Install OpenDKIM and its tools:
sudo apt update
sudo apt install opendkim opendkim-tools
Create a directory for your keys and generate a 2048-bit key pair:
sudo mkdir -p /etc/opendkim/keys/example.com
cd /etc/opendkim/keys/example.com
sudo opendkim-genkey -b 2048 -d example.com -s mail
This writes two files: mail.private (the private key — keep it secret) and mail.txt (the public key, formatted as a DNS TXT record). The private key must be readable by OpenDKIM and no one else:
sudo chown opendkim:opendkim /etc/opendkim/keys/example.com/mail.private
sudo chmod 600 /etc/opendkim/keys/example.com/mail.private
DKIM: configure OpenDKIM
OpenDKIM's main config is /etc/opendkim.conf. The modern, standard approach uses three helper files — a KeyTable (which key signs for which domain), a SigningTable (which senders get signed), and a TrustedHosts list (hosts whose mail we sign and whose incoming mail we don't verify).
Set these directives in /etc/opendkim.conf. Some may already be present with defaults — adjust rather than duplicate:
Syslog yes
UMask 007
Mode sv
Canonicalization relaxed/simple
KeyTable /etc/opendkim/key.table
SigningTable refile:/etc/opendkim/signing.table
ExternalIgnoreList /etc/opendkim/TrustedHosts
InternalHosts /etc/opendkim/TrustedHosts
Socket inet:8891@localhost
Mode sv means sign outbound and verify inbound. The Socket line tells OpenDKIM to listen on TCP port 8891 on localhost, which is the simplest reliable way to connect it to Postfix. (You can instead use a Unix socket, but then you must give Postfix access to it via group membership — the inet socket avoids that.)
On some Debian releases the socket is also referenced in /etc/default/opendkim. If that file sets a SOCKET= value, make it match the one above or leave it unset so opendkim.conf wins. Check your version's opendkim.conf man page for the exact directive behaviour.
Now the three helper files.
/etc/opendkim/key.table — one line, the selector, domain and key path:
mail._domainkey.example.com example.com:mail:/etc/opendkim/keys/example.com/mail.private
/etc/opendkim/signing.table — which addresses get signed with that key:
*@example.com mail._domainkey.example.com
/etc/opendkim/TrustedHosts — hosts trusted to relay through and be signed:
127.0.0.1
::1
localhost
example.com
Add your server's own hostname and any internal relays that should have their mail signed. Do not add the whole internet here.
Restart and enable OpenDKIM:
sudo systemctl restart opendkim
sudo systemctl enable opendkim
Check it started cleanly before touching Postfix:
sudo systemctl status opendkim
Wire OpenDKIM into Postfix
Postfix talks to milters over the socket you configured. Add these to /etc/postfix/main.cf:
milter_default_action = accept
milter_protocol = 6
smtpd_milters = inet:localhost:8891
non_smtpd_milters = inet:localhost:8891
smtpd_milters handles mail arriving over SMTP; non_smtpd_milters handles mail injected locally (like sendmail). milter_default_action = accept means that if OpenDKIM is down, mail still flows unsigned rather than bouncing — a safer failure mode than losing mail. See the Postfix documentation for smtpd_milters and the MILTER_README for the details.
If you already run other milters (for example Amavis or a spam filter), add this socket to the existing list rather than replacing it — the order matters and DKIM signing should generally come after content filters that might alter the body.
Reload Postfix:
sudo systemctl reload postfix
Publish the DKIM public key
Open mail.txt — it contains the record OpenDKIM generated:
sudo cat /etc/opendkim/keys/example.com/mail.txt
It looks roughly like this (example — publish your own generated key, never this one):
; EXAMPLE — this is not a real key; use the contents of your own mail.txt
mail._domainkey.example.com. IN TXT ( "v=DKIM1; k=rsa; "
"p=MIIBIjANBgkq...your actual public key...IDAQAB" )
Create a TXT record at mail._domainkey.example.com (the selector is mail; use your own). Many DNS panels want the value as a single string — if so, concatenate the quoted chunks into one, removing the quotes and the surrounding parentheses. Pasting the wrong half here is the classic mistake: this is the public key and is meant to be published; the .private file stays on your server.
DMARC: one more DNS record
DMARC lives at _dmarc under your domain. Start in monitoring mode so you don't reject your own mail while you settle things:
; EXAMPLE — adapt the domain and the report address
_dmarc.example.com. IN TXT "v=DMARC1; p=none; rua=mailto:dmarc-reports@example.com"
p=none means "don't act, just report." The rua address receives aggregate XML reports from receivers. Once you've watched reports for a week or two and confirmed all your legitimate mail passes SPF or DKIM with alignment, tighten to p=quarantine and eventually p=reject. Don't jump straight to p=reject — you will silently lose real mail from any sender you forgot.
Verify — do not skip this
Confirm the private key matches the published DNS record:
opendkim-testkey -d example.com -s mail -vvv
A healthy result reports the key OK. If it complains it can't retrieve the record, DNS hasn't propagated yet or the selector/domain is wrong.
Confirm the DNS records are actually live:
dig +short TXT mail._domainkey.example.com
dig +short TXT example.com
dig +short TXT _dmarc.example.com
Read the headers on a real message. Send an email from your server to an account you control at a large provider, then look at the received message's raw headers for an Authentication-Results line. You want to see dkim=pass, spf=pass, and dmarc=pass for your domain.
Use a send-and-check service. Two standard ones:
- Send a message to the address given by mail-tester.com and read its score page.
- Send to the reflector at check-auth@verifier.port25.com, which replies with a full SPF/DKIM/DMARC summary.
If DKIM shows pass in a received header and opendkim-testkey is happy, your signing chain is correct. If it shows none, Postfix isn't reaching the milter — check that OpenDKIM is listening on the socket and that smtpd_milters points to the same address. Once all three read pass, you can start tightening DMARC.
For deeper reference, see the OpenDKIM documentation, the Postfix MILTER_README, and the DMARC specification (RFC 7489).
