Postfix SPF, DKIM & DMARC: Self-Hosted Mail Server Guide

Configure OpenDKIM signing, SPF and DMARC on a self-hosted Postfix server running Ubuntu or Debian, including milter wiring, PTR checks and swaks testing.
4 min read
Updated 2026-11-04
Brand Specific
postfixdkimspfdmarcself-hosted

When you run Postfix yourself, no provider is accountable for your reputation. Receivers judge each message on three things: whether the connecting IP may send for your domain (SPF), whether the message is signed and intact (DKIM), and what to do when either fails (DMARC). A correctly configured VPS can deliver as well as a hosted mailbox; a misconfigured one is rejected before the body is read.

This guide configures the sending side with OpenDKIM on Ubuntu 24.04 or Debian 12, then adds SPF and DMARC plus optional inbound checks. Examples use yourdomain.com and 203.0.113.10.

What you need

  • Root or sudo on Ubuntu or Debian, with Postfix installed and relaying outbound mail.
  • A static public IPv4 address and access to your DNS zone.
  • The ability to set a PTR record — on a VPS this is a provider action, not a record in your zone.
  • Outbound port 25 open; many hosts block it by default.

Step 1: fix reverse DNS first

Before DKIM, make the server's identity consistent. The PTR record for your IP must resolve to the hostname Postfix announces in HELO, and back again:

203.0.113.10          PTR   mail.yourdomain.com
mail.yourdomain.com   A     203.0.113.10

Confirm both directions with dig -x 203.0.113.10 +short and dig mail.yourdomain.com +short. Then set myhostname = mail.yourdomain.com in /etc/postfix/main.cf. Gmail and Microsoft treat a generic HELO such as localhost.localdomain as a spam signal regardless of your SPF record.

Step 2: sign outbound mail with OpenDKIM

Install the milter and generate a 2048-bit key. Do not use 1024-bit keys; many receivers treat them as weak. rspamd is the maintained alternative, handling signing and verification in one daemon.

sudo apt install opendkim opendkim-tools
sudo mkdir -p /etc/opendkim/keys/yourdomain.com
sudo opendkim-genkey -b 2048 -d yourdomain.com -s mail \
  -D /etc/opendkim/keys/yourdomain.com
sudo chown -R opendkim:opendkim /etc/opendkim/keys

Wire the configuration in /etc/opendkim.conf:

Domain           yourdomain.com
KeyTable         /etc/opendkim/KeyTable
SigningTable     refile:/etc/opendkim/SigningTable
ExternalIgnoreList refile:/etc/opendkim/TrustedHosts
InternalHosts    refile:/etc/opendkim/TrustedHosts
Socket           inet:8891@localhost
UserID           opendkim

Three tables control what gets signed and who may relay through the milter:

KeyTable:
mail._domainkey.yourdomain.com  yourdomain.com:mail:/etc/opendkim/keys/yourdomain.com/mail.private

SigningTable:
*@yourdomain.com  mail._domainkey.yourdomain.com

TrustedHosts:
127.0.0.1
::1
localhost

Start the service and point Postfix at the socket:

sudo systemctl enable --now opendkim
sudo postconf -e "smtpd_milters = inet:127.0.0.1:8891"
sudo postconf -e "non_smtpd_milters = inet:127.0.0.1:8891"
sudo postconf -e "milter_default_action = accept"
sudo systemctl reload postfix

Publish the key from mail.txt as one string; its line breaks are zone-file syntax:

mail._domainkey.yourdomain.com  TXT  "v=DKIM1; k=rsa; p=MIIBIjANBg..."

Step 3: publish SPF for this server

One TXT record at the root authorizes the sending IP. Use ip4, never ipv4:

yourdomain.com  TXT  "v=spf1 ip4:203.0.113.10 a mx ~all"

a and mx cover the hostname if the address ever changes; ~all softfails everything else while you are still validating. Keep exactly one SPF record; a second v=spf1 record causes a permanent error. Validate it with the SPF Checker or build it with the SPF Generator.

Step 4: publish DMARC and read the reports

_dmarc.yourdomain.com  TXT  "v=DMARC1; p=none; rua=mailto:dmarc@yourdomain.com; fo=1"

Start at p=none. For two to four weeks, receivers send XML reports listing every IP that sent as your domain with each check's result. Read them with the DMARC Report Analyzer, fix any legitimate source that fails, then move to p=quarantine; pct=25 and finally p=reject. Jumping to reject is how self-hosters lose their own invoices.

Step 5: verify inbound mail (optional)

Outbound authentication protects your reputation; inbound checking protects your users. For SPF, install postfix-policyd-spf-python and add a service to /etc/postfix/master.cf:

policy-spf  unix  -  n  n  -  -  spawn
  user=nobody argv=/usr/bin/policyd-spf

Then append check_policy_service unix:private/policy-spf to smtpd_recipient_restrictions after reject_unauth_destination. For inbound DMARC, install opendmarc on inet:127.0.0.1:8893 and append it to smtpd_milters.

Verify the whole chain

Send through submission, not by local injection:

swaks --to you@gmail.com --from you@yourdomain.com --server mail.yourdomain.com:587 --tls --auth LOGIN --auth-user you@yourdomain.com

In Gmail, open Show original and confirm spf=pass, dkim=pass header.d=yourdomain.com and dmarc=pass. Check the published side with the DKIM Checker using selector mail, the DMARC Lookup, and a full Email Health Check. If mail disappears, test port 25 with the SMTP Test and check the Blacklist Checker.

If users send from a webmail front end on the same box, the MTA-level signing above is what authenticates those messages — see Roundcube webmail authentication for the submission flow that keeps SPF aligned.

Common mistakes

  1. Postfix cannot reach the milter. Mail still flows but leaves unsigned because milter_default_action = accept. Watch mail.log for connection refused.
  2. The SigningTable wildcard does not cover everything. *@yourdomain.com misses subdomain senders and some bounce addresses. Add explicit entries for each sending domain.
  3. The DKIM TXT value was pasted with line breaks. DNS returns a truncated key and every signature fails verification.
  4. SPF exceeds 10 DNS lookups. The whole record returns permerror, which receivers read as no SPF at all.
  5. PTR and HELO disagree. Forward-confirmed reverse DNS is checked before content filters ever run.

Frequently asked questions

Do I need OpenDMARC if Postfix already checks SPF?

They answer different questions. SPF checking only tests whether the envelope sender was authorized; OpenDMARC applies the published policy and adds Authentication-Results headers your spam filter can act on. Skip it and you cannot honor other domains' reject policies.

Why does DKIM pass but DMARC still fail?

Alignment. The signature must be valid for the From domain, not just any domain. If the SigningTable uses a different d= value, or a plugin rewrites From after signing, DKIM passes while DMARC fails. Compare the header's d= with your From domain.

Does port 25 need to be open if I only submit on 587?

Yes, for direct delivery. Port 587 is for your users to submit; your server still needs outbound 25 to reach recipients' MX hosts. If your host blocks 25, relay through a smarthost and add its include domain to SPF.

Was this guide helpful?