Phone:

Hidden from the page source until you click: friction against scrapers, not a guarantee.

Email:

[email protected]

Category:

Security

Tags:
  • spf
  • dkim
  • dmarc
  • email
  • postfix
  • self-hosting

DKIM, SPF and DMARC for Self-Hosted Mail: A Practical Setup

Every self-hosted mail server eventually hits the same wall: you send a perfectly ordinary email, it goes into someone's spam folder, and you have no idea why. The frustrating part is that you can get SPF, DKIM and DMARC entirely correct and still lose the inbox placement fight, because those three records only prove who you are, not that you're trustworthy. This is a walkthrough of setting them up properly on Postfix, plus the bits that trip people up afterwards that no DNS record can fix.

What each record is actually doing

It's worth being precise about this, because the three get conflated constantly:

  • SPF (Sender Policy Framework) is a DNS record listing which IP addresses are allowed to send mail claiming to be from your domain. It's checked against the "envelope from" (the SMTP MAIL FROM), not the header your recipient sees.
  • DKIM (DomainKeys Identified Mail) cryptographically signs outgoing mail with a private key, and the corresponding public key sits in DNS so receivers can verify the signature. It proves the message wasn't altered in transit and really came from a server holding your private key.
  • DMARC ties the two together. It tells receivers what to do when SPF or DKIM fails, and requires the domain in the visible From: header to align with whichever of SPF or DKIM passed. It also gives you a reporting address, which turns out to be the most useful part.

None of them, individually or together, is a "let us through" pass. They're the minimum bar. Below the bar you're rejected outright by anyone paying attention; above it, IP and domain reputation still decides where the message lands.

SPF: authorising your sending IP

Publish a single TXT record on your apex domain:

example.com.  IN  TXT  "v=spf1 mx a:mail.example.com -all"

This says: mail is authorised if it comes from an IP in your domain's MX or A records, or from mail.example.com specifically, and reject everything else (-all). If you also send through a relay, such as a VPS provider's smart host or a transactional email service, add an include: mechanism for their SPF record too.

Two mistakes cause most of the pain here. First, having more than one SPF TXT record on the same name: this is invalid per the spec and receivers are free to treat it as a permanent fail. Merge them into one record instead. Second, using ~all (soft fail) forever out of caution: it's a reasonable starting point while you test, but once you're confident in the record, move to -all. A soft-fail SPF record does nothing useful against spoofing, and DMARC alignment doesn't care about the difference between the two, it just wants an unambiguous pass or fail.

DKIM: signing what you send

On Debian or Ubuntu with Postfix, OpenDKIM is still the standard route:

apt install opendkim opendkim-tools
mkdir -p /etc/opendkim/keys/example.com
opendkim-genkey -b 2048 -d example.com -s mail -D /etc/opendkim/keys/example.com
chown opendkim:opendkim /etc/opendkim/keys/example.com/mail.private

-s mail sets the selector, the prefix that lets you publish multiple keys and rotate them later without breaking anything mid-flight. Configure /etc/opendkim.conf:

Syslog          yes
Domain          example.com
KeyFile         /etc/opendkim/keys/example.com/mail.private
Selector        mail
Socket          inet:8891@localhost
Mode            sv

Then hook it into Postfix via main.cf:

milter_default_action = accept
milter_protocol = 6
smtpd_milters = inet:localhost:8891
non_smtpd_milters = inet:localhost:8891

Restart both services and publish the public key that opendkim-genkey wrote to mail.txt:

mail._domainkey.example.com.  IN  TXT  ( "v=DKIM1; h=sha256; k=rsa; "
  "p=MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA..." )

If you're running rspamd instead of a bare Postfix and OpenDKIM pair, which is increasingly common because it does spam filtering, DKIM signing and ARC in one process, use its dkim_signing module and skip OpenDKIM entirely; the DNS side is identical.

DMARC: policy and, more usefully, reports

Start cautious:

_dmarc.example.com.  IN  TXT  "v=DMARC1; p=none; rua=mailto:[email protected]; pct=100; adkim=s; aspf=s"

p=none takes no enforcement action, it only asks receivers to send you aggregate reports (rua) of what passed and failed and from where. Leave it there for a couple of weeks and read the reports; they'll surface anything you forgot to authorise, like a mailing list plugin or a billing system sending as your domain. The adkim and aspf flags control alignment strictness: s (strict) demands the authenticated domain match the From: domain exactly, r (relaxed, the default) allows a subdomain match. Once the reports are clean, move to p=quarantine, then eventually p=reject, which tells receivers to drop unauthenticated mail claiming your domain outright.

The aggregate reports arrive as gzipped XML attachments, which is not exactly pleasant reading raw. A small parser, or one of the free DMARC report viewers, turns them into something you can actually skim.

The reputation problem no DNS record fixes

This is where most self-hosted setups actually fail, after the records are all correct:

  • Reverse DNS must match your HELO. If your PTR record for the sending IP doesn't resolve back to the hostname you announce in HELO/EHLO, several major providers will throttle or reject you regardless of SPF and DKIM passing. Get your VPS provider to set the PTR record before you send a single message.
  • A brand new IP has no reputation, and no reputation reads as suspicious. Ramp volume up gradually over a week or two rather than firing off a large batch on day one. This is the same "warm-up" advice commercial ESPs give their customers, and it applies just as much to a single VPS.
  • Check yourself against the common blocklists (Spamhaus in particular) before you assume the problem is configuration. A previous tenant of your IP address may have left it listed.
  • Gmail and Yahoo now have explicit bulk sender rules. Since February 2024, any domain sending 5,000 or more messages a day to Gmail addresses is classified as a bulk sender, permanently, even if volume later drops, and must have SPF, DKIM and an aligned, at-minimum-p=none DMARC record, a working one-click unsubscribe header on bulk mail, and a spam complaint rate under 0.3 percent. A personal or small-business mail server is unlikely to hit that threshold, but it's worth knowing the line exists if you're running a newsletter off the same box.

Verifying it actually works

Before trusting any of this, check each record resolves as expected:

dig txt example.com +short
dig txt mail._domainkey.example.com +short
dig txt _dmarc.example.com +short

Then send a real test message to a service like mail-tester.com, which will authenticate the message against all three mechanisms and separately flag reputation issues like a missing PTR record or a blocklist hit. It won't catch everything, Gmail and Outlook both apply engagement-based filtering that no external tool can simulate, but it catches the configuration mistakes, which is most of what's fixable in one sitting.

The records get you a fair hearing. Whether the message lands in the inbox after that is decided by a reputation system you can influence but never fully see into, which is roughly the position every small sender has always been in, self-hosted or not.