E-Mail Done My Way, Part 3 - DKIM/DMARC/SPF
|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.
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:126.96.36.199 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 -
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, 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" <firstname.lastname@example.org> 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.
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" <email@example.com> 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
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
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!
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
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,
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: connect from mail-4316.protonmail.ch[188.8.131.52] Sep 3 21:30:29 mailhub postfix/smtpd: discarding EHLO keywords: CHUNKING Sep 3 21:30:29 mailhub postfix/smtpd: Anonymous TLS connection established from mail-4316.protonmail.ch[184.108.40.206]: 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: discarding EHLO keywords: CHUNKING Sep 3 21:30:29 mailhub postfix/smtpd: 440912000541: client=mail-4316.protonmail.ch[220.127.116.11] Sep 3 21:30:29 mailhub postfix/cleanup: 440912000541: message-id=<scFwCi3Gam7SjUdO0T6iDAtVVcusgPZqkr21KOa3em8Mdf9Td_18qitkK35UW8qKGOSsitPQxLMNNDtlvyRKWoUa45S4yK-atQChvKFeIMQfirstname.lastname@example.org> Sep 3 21:30:29 mailhub opendkim: 440912000541: mail-4316.protonmail.ch [18.104.22.168] not internal Sep 3 21:30:29 mailhub opendkim: 440912000541: not authenticated Sep 3 21:30:29 mailhub opendkim: 440912000541: DKIM verification successful Sep 3 21:30:29 mailhub opendmarc: 440912000541: SPF(mailfrom): pm.me pass Sep 3 21:30:29 mailhub opendmarc: 440912000541: pm.me pass Sep 3 21:30:29 mailhub postfix/qmgr: 440912000541: from=<jXXXX@pm.me>, size=2529, nrcpt=1 (queue active) Sep 3 21:30:29 mailhub postfix/smtpd: disconnect from mail-4316.protonmail.ch[22.214.171.124] ehlo=2 starttls=1 mail=1 rcpt=1 data=1 quit=1 commands=7 Sep 3 21:30:29 mailhub dovecot: lmtp(67304): Connect from local Sep 3 21:30:29 mailhub dovecot: lmtp(email@example.com)<67304><wl/fJ1WrE2PoBgEA2EPHzg>: msgid=<scFwCi3Gam7SjUdO0T6iDAtVVcusgPZqkr21KOa3em8Mdf9Td_18qitkK35UW8qKGOSsitPQxLMNNDtlvyRKWoUa45S4yK-atQChvKFeIMQfirstname.lastname@example.org>: saved mail to INBOX Sep 3 21:30:29 mailhub dovecot: lmtp(67304): Disconnect from local: Logged out (state=READY) Sep 3 21:30:29 mailhub postfix/lmtp: 440912000541: to=<email@example.com>, 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 <firstname.lastname@example.org> wl/fJ1WrE2PoBgEA2EPHzg Saved) Sep 3 21:30:29 mailhub postfix/qmgr: 440912000541: removed
Now you see all parts happily working together. We get an incoming connection from
mail-4316.protonmail.ch[126.96.36.199]. 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: email@example.com Received: from mailhub.wildeboer.net by mailhub.wildeboer.net with LMTP id wl/fJ1WrE2PoBgEA2EPHzg (envelope-from <jXXXXX@pm.me>) for <firstname.lastname@example.org>; Sat, 03 Sep 2022 21:30:29 +0200 Received: from mail-4316.protonmail.ch (mail-4316.protonmail.ch [188.8.131.52]) by mailhub.wildeboer.net (Postfix) with ESMTPS id 440912000541 for <email@example.com>; 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 firstname.lastname@example.org 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 <email@example.com>
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.
You can use your Mastodon or other ActivityPub account to comment on this article by replying to the associated post.