Posted:

E-Mail Done My Way, Part 3 - DKIM/DMARC/SPF

13 minute read

0. The Journey - The basics and outlook (on the series, not the Microsoft mail client ;)
1. Postfix - the in and out, so to say. The robust, battle-hardened connection point for other mail servers on the internet to send emails to and receive emails from your domain(s). Also known as the MTA, the Mail Transfer Agent.
2. Dovecot - where you and your users talk to to get emails to their mail client, be it your smartphone, a mail client on your computer or just even the command line. It’s the IMAP server.
3. DKIM/DMARC/SPF - Just having postfix and dovecot up and running isn’t enough. We will also look at user authentication, letsencrypt certificates, DKIM, DMARC, SPF and the daily checks to make sure everything is humming along nicely.
4. The final stuff - How to make sure my e-mail server is happy and can do its job. Some simple checks, how to use fail2ban to keep bad servers and users away, checking log files, all those little things.

After we learned about my general approach and setup in part 0, the details of my postfix configuration in part 1, how I use dovecot in part 2, we now (almost) finish it up by going through my DKIM/DMARC/SPF setup.

There is a corollary to this article called How To Read Your E-Mail Headers that explains the basics of what happens when you send or receive an e-mail and what the headers can tell you about that.

WARNING: This series is not for people trying to set up their first ever email server. I expect readers to know the basics and have some experience with running services on a Linux box.

We will again look at the config files, line by line, to explain how to use opendkim, opendmarc and how it integrates with SPF.

As I already explained in part 1, both opendkim and opendmarc are called by postfix when receiving or sending e-mails through the milter (mail filter) integration.

SPF

The Sender Policy Framework (or Sender Permitted From) SPF is a simple but quite effective system to reject e-mails from spammers. The most simple TL;DR explanation: As an owner of a domain, you add a SPF entry to the DNS. As I’ve explained in my DNS article, I use a quite simple definition:

@ 10800 IN TXT "v=spf1 a mx ip4:51.38.32.100 ip6:2001:41d0:305:2100::548c -all"

This TXT entry that I use for all my domains effectively says that the official a and mx DNS entries are allowed to send e-mail for this domain. Also the IPv4 and IPv6 addresses mentioned. But no-one else - -all.

So when another e-mail server receives an e-mail, seemingly coming from one of my domains, that server can look up the SPF entry and verify if the server IP address sending that e-mail is allowed to do so in the name of my domain. If it uses an IP address I do NOT allow, that’s a very clear sign that it is spam. The receiving server is now free to decide what to do. Reject the connection, mark the e-mail as spam etc.

And I do the same for every incoming e-mail. Check if a SPF entry exists in DNS, verify the server talking to me is allowed to do so and add the result of that check to headers of the e-mail.

We will see where this is done exactly in a minute.

DKIM

DKIM, the DomainKeys Identified Mail system, works a bit different. Where SPF takes care of defining which server is allowed to send e-mails, DKIM works on the e-mail level itself. It adds a signature to an e-mail, which is derived from a private key that can be verified using the public key the sending domain makes available in DNS.

So when you receive an e-mail with a DKIM signature, you can get that public key and verify the signature is valid.

All of this is done by opendkim on my server. So. Let’s look at the config file /etc/opendkim.conf and the other very important files it needs to work. I reduced the config to the relevant lines. All other lines in the config file are left at default settings.

Mode    sv
Syslog  yes
SyslogSuccess   yes
LogWhy  yes
Socket  inet:8891@localhost
Umask   002
SendReports     yes
ReportAddress   "wildeboer.net Postmaster" <postmaster@wildeboer.net>
SoftwareHeader  yes
Selector        default
KeyTable        /etc/opendkim/KeyTable
SigningTable    refile:/etc/opendkim/SigningTable
ExternalIgnoreList      refile:/etc/opendkim/TrustedHosts
InternalHosts   refile:/etc/opendkim/TrustedHosts

Not that much to explain ;) So we start with the Mode sv. This tells opendkim to both sign and verify the e-mails it gets via milter. So verify all incoming e-mails and add the DKIM signature to outgoing e-mails.

The next three lines are about logging results and information to syslog. This allows us to later filter information on what opendkim has done and how a verification might have failed. Helpful stuff when debugging mail transport problems.

With Socket inet:8891@localhostand Umask 002 we tell opendkim to open a socket on port 8891. Yes, that’s the port we also defined in postfix for the milter calls. This is how postfix and opendkim talk to each other. Easy :)

SendReports yes tells opendkim that when it got a verification error, it should send an e-mail report to the domain that is supposed to send us this e-mail. And if we send such a report, it should use ReportAddress "wildeboer.net Postmaster" <postmaster@wildeboer.net> as the from address.

SoftwareHeader yes instructs opendkim to add a DKIM filter header with information on the result of the checks, so the local recipient can check what has happened (and use that information to filter it as spam, when verification has failed).

DKIM uses so called selectors to find the public key in DNS. With Selector default we tell opendkim to use the, uhm, default selector. You can have more than one selector in DNS. The default typically is the newest entry.

Now the slightly complex part. As I use one mail server to send and receive e-mails for 20+ domains, we need to tell opendkim which keys to use for which domain for signing and verifying.

First we have the KeyTable file. It tells us which private key to use when verifying e-mails. One line per domain. You could use different private/public keys per domain, but I am lazy and use the same pair fo all my domains. This is OK to do, according to the specs.

So I have created the key pair with this command:

opendkim-genkey -b 2048 -d mailhub.wildeboer.net -D /etc/opendkim/keys/mailhub.wildeboer.net -s 20220708 -v

That created the public/private keys for my mailserver with the selector 20220708. As explained in my DNS article, that selector and the public key are then placed in DNS.

Back to the KeyTable file.

20220708._domainkey.coronamuc.de           coronamuc.de:/etc/opendkim/keys/mailhub.wildeboer.net/20220708.private
20220708._domainkey.didelonbuytwitter.com  didelonbuytwitter.com:20220708:/etc/opendkim/keys/mailhub.wildeboer.net/20220708.private
20220708._domainkey.fedigrid.com           fedigrid.com:20220708:/etc/opendkim/keys/mailhub.wildeboer.net/20220708.private

This tells opendkim that the DKIM key with the selector 20220708 for a specific domain means the private key found in /etc/opendkim/keys should be used for verifying. And as you see, they all point to the same key.

The second file, SigningTable does the other part. It tells opendkim which key to use to generate the signature for outgoing e-mails. It looks like this:

*@coronamuc.de           20220708._domainkey.coronamuc.de
*@didelonbuytwitter.com  20220708._domainkey.didelonbuytwitter.com
*@fedigrid.com           20220708._domainkey.fedigrid.com

You can read this as “for any sender of the domain X, use the key with the selector 20220708”. And as you can see, all entries point to the same selector and thus the same keypair.

Almost done! In TrustedHostswe tell opendkim which hosts to trust. I use the bare minimum with

127.0.0.1
::1

So localhost in IPv4 and IPv6.

Now make sure opendkim is started as a service and that’s it. All my domains use DKIM for receiving and sending e-mails!

DMARC

Now let’s look at my opendmarc config in /etc/opendmarc.conf. Again, we will go through the config file lines I have changed, all the rest stays as default.

AuthservID OpenDMARC
TrustedAuthservIDs mailhub.wildeboer.net
IgnoreAuthenticatedClients true
RequiredHeaders true
Socket inet:8893@localhost
UMask 007
SoftwareHeader true
SPFIgnoreResults false
SPFSelfValidate true
Syslog true

The first two entries AuthservID and TrustedAuthservIDs are easy enough to understand. We use OpenDMARC as name in the DMARC headers and we trust ourselves to do DMARC.

IgnoreAuthenticatedClients true makes sure that when we send e-mails from our laptop or phone and have authenticated with our e-mail server through SASL, we can actually send e-mails without DMARC checks interfering.

The next line, RequiredHeaders true is quite powerful. It checks incoming e-mails to see if it contains the headers required for a “real” e-mail. If, for example, there is no domain name in the From: header (a typical spam mail thing), the e-mail gets rejected straight away.

The next two lines, Socket and umask set up the socket (port 8893) that postfix uses to communicate with opendmarc.

SoftwareHeader true tells opendmarc to insert the DMARC headers in the processed e-mail, so that we as a mail user can use that info for local checks.

Now we get to SPF, as promised earlier. Opendmarc will perform SPF checks on incoming e-mails. First we tell it with SPFIgnoreResults true to ignore any SPF headers already in the incoming e-mail as they might be spoofed.

Instead we tell opendmarc with SPFSelfValidate true to do the SPF check itself.

And finally, with Syslog true we make sure we add information to syslog so we can find out what is working and what is not. Again, make sure opendmarc is running as a service and we are done.

With all that in place …

So what does all of this finally do? Well, let’s have a look at what happens when I receive an e-mail. In this case from protonmail:

Sep  3 21:30:29 mailhub postfix/smtpd[67297]: connect from mail-4316.protonmail.ch[185.70.43.16]
Sep  3 21:30:29 mailhub postfix/smtpd[67297]: discarding EHLO keywords: CHUNKING
Sep  3 21:30:29 mailhub postfix/smtpd[67297]: Anonymous TLS connection established from mail-4316.protonmail.ch[185.70.43.16]: TLSv1.3 with cipher TLS_AES_256_GCM_SHA384 (256/256 bits) key-exchange X25519 server-signature RSA-PSS (2048 bits) server-digest SHA256
Sep  3 21:30:29 mailhub postfix/smtpd[67297]: discarding EHLO keywords: CHUNKING
Sep  3 21:30:29 mailhub postfix/smtpd[67297]: 440912000541: client=mail-4316.protonmail.ch[185.70.43.16]
Sep  3 21:30:29 mailhub postfix/cleanup[67302]: 440912000541: message-id=<scFwCi3Gam7SjUdO0T6iDAtVVcusgPZqkr21KOa3em8Mdf9Td_18qitkK35UW8qKGOSsitPQxLMNNDtlvyRKWoUa45S4yK-atQChvKFeIMQ=@pm.me>
Sep  3 21:30:29 mailhub opendkim[875]: 440912000541: mail-4316.protonmail.ch [185.70.43.16] not internal
Sep  3 21:30:29 mailhub opendkim[875]: 440912000541: not authenticated
Sep  3 21:30:29 mailhub opendkim[875]: 440912000541: DKIM verification successful
Sep  3 21:30:29 mailhub opendmarc[66822]: 440912000541: SPF(mailfrom): pm.me pass
Sep  3 21:30:29 mailhub opendmarc[66822]: 440912000541: pm.me pass
Sep  3 21:30:29 mailhub postfix/qmgr[1350]: 440912000541: from=<jXXXX@pm.me>, size=2529, nrcpt=1 (queue active)
Sep  3 21:30:29 mailhub postfix/smtpd[67297]: disconnect from mail-4316.protonmail.ch[185.70.43.16] ehlo=2 starttls=1 mail=1 rcpt=1 data=1 quit=1 commands=7
Sep  3 21:30:29 mailhub dovecot[1460]: lmtp(67304): Connect from local
Sep  3 21:30:29 mailhub dovecot[1460]: lmtp(jan@wildeboer.net)<67304><wl/fJ1WrE2PoBgEA2EPHzg>: msgid=<scFwCi3Gam7SjUdO0T6iDAtVVcusgPZqkr21KOa3em8Mdf9Td_18qitkK35UW8qKGOSsitPQxLMNNDtlvyRKWoUa45S4yK-atQChvKFeIMQ=@pm.me>: saved mail to INBOX
Sep  3 21:30:29 mailhub dovecot[1460]: lmtp(67304): Disconnect from local: Logged out (state=READY)
Sep  3 21:30:29 mailhub postfix/lmtp[67303]: 440912000541: to=<jan@wildeboer.net>, relay=mailhub.wildeboer.net[private/dovecot-lmtp], delay=0.6, delays=0.41/0.02/0.02/0.15, dsn=2.0.0, status=sent (250 2.0.0 <jan@wildeboer.net> wl/fJ1WrE2PoBgEA2EPHzg Saved)
Sep  3 21:30:29 mailhub postfix/qmgr[1350]: 440912000541: removed

Now you see all parts happily working together. We get an incoming connection from mail-4316.protonmail.ch[185.70.43.16]. It switches to an encrypted TLS 1.3 connection and we receive an e-mail.

Postfix calls opendkim, which checks the DKIM signature with DKIM verification successful. Next step is opendmarc which performs the SPF check with SPF(mailfrom): pm.me pass. Everything is cool, so now postfix calls dovecot via lmtp to deliver the e-mail to my InBox. And that’s it!

And when I open that e-mail with my mail client, I can check the headers to see:

Return-Path: <jXXXXX@pm.me>
Delivered-To: jan@wildeboer.net
Received: from mailhub.wildeboer.net
	by mailhub.wildeboer.net with LMTP
	id wl/fJ1WrE2PoBgEA2EPHzg
	(envelope-from <jXXXXX@pm.me>)
	for <jan@wildeboer.net>; Sat, 03 Sep 2022 21:30:29 +0200
Received: from mail-4316.protonmail.ch (mail-4316.protonmail.ch [185.70.43.16])
	by mailhub.wildeboer.net (Postfix) with ESMTPS id 440912000541
	for <jan@wildeboer.net>; Sat,  3 Sep 2022 21:30:29 +0200 (CEST)
DMARC-Filter: OpenDMARC Filter v1.4.1 mailhub.wildeboer.net 440912000541
Authentication-Results: OpenDMARC; dmarc=pass (p=quarantine dis=none) header.from=pm.me
Authentication-Results: OpenDMARC; spf=pass smtp.mailfrom=pm.me
DKIM-Filter: OpenDKIM Filter v2.11.0 mailhub.wildeboer.net 440912000541
Authentication-Results: mailhub.wildeboer.net;
	dkim=pass (2048-bit key, secure) header.d=pm.me header.i=@pm.me header.a=rsa-sha256 header.s=protonmail3 header.b=BCQgB0sv
Date: Sat, 03 Sep 2022 19:30:17 +0000
DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=pm.me;
	s=protonmail3; t=1662233428; x=1662492628;
	bh=n2AJCUcbvMty5IMuJ77rD4QKX8BF9ivJsU/RRHSAjyU=;
	h=Date:To:From:Reply-To:Subject:Message-ID:Feedback-ID:From:To:Cc:
	 Date:Subject:Reply-To:Feedback-ID:Message-ID;
	b=BCQgB0svnRomMSjJ8ViZYLk0Jun+ohg6aCfiqIYcSixW55YtxAsII/TeO/bhyTuKR
	 CsBmq/iNy+G4NtKihJDzffT+aXmaicuVCrDKqHjJyAvM14SKJ52PLkDukQakOxjvm6
	 3aLG8Wny3qQyhoD8/PQ8xCGJVkUJzxZs/fYqYWeK9fyHW8zaclTtM9TiIwviUc5C9b
	 kzqZJVhU16TgUeWhKxmsdoXGJWqlO82F1vKaQz8FvEWSmY2OLwZi8RMSufU6A8H6u0
	 PXTH4G6f/1/R6hfab0niDWFNIXl4S7Z+zcBXtt4+vJDGnAaIPd98IflJLUcqb1BaKI
	 DJJiNlglTo+Tg==
To: Jan Wildeboer <jan@wildeboer.net>

There you go. Opendmarc inserted headers with what it found out, opendkim too. You can even see the DKIM signature itself.

And that’s how I run my mail server. In the next and final part we will look at daily maintenance, fail2ban and other little things.

Part 4: The final stuff

COMMENTS

You can use your Mastodon or other ActivityPub account to comment on this article by replying to the associated post.