As my old mailserver crashed a few weeks ago for reasons related to it being a Raspberry Pi, I’ve had to quickly recreate one to receive a very important email in an hour. I found that most of the online tutorials to set up a proper mailserver are quite incomplete and outdated, so here’s my take on how to set it all up.
My set-up is based on Ubuntu 18.04 Bionic Beaver, and includes DKIM signatures on outgoing mail, and DMARC verification and reports. All this (save for DMARC reports) I already had, but Archlinux is significantly different from Ubuntu, so there’s some interesting hurdles.
Configuring the server
I’m going to assume that you’ve already installed a regular ol’ Postfix mailserver. If you haven’t, find one of the tens of thousands guides available elsewhere (or just the Arch wiki) and follow that. I’ll just show you what you need to do to add DMARC and DKIM.
First, let’s install the dependencies:
sudo apt install opendkim opendmarc
Setting up OpenDKIM
OpenDKIM will do two things for us: validate DKIM signatures on incoming email,
and sign our outgoing email. The latter requires us to generate our own key if
you don’t already have one. The tool you need is called
opendkim-genkey and it
is located in the
opendkim-tools package. For more details, you can just look
up the instructions on
the Arch wiki.
When generating a key, the tool also generates a DNS record which should be
Assuming you’ve got your DNS set-up, you need to edit the configuration file. In
/etc/opendkim.conf there are a few things of interest. The only fields you
really need to change are as follows:
Domain comma.separated, list.of, your.domains KeyFile /path/to/your/key.private Selector your_key_selector
The selector is the selector you chose when generating the DKIM key.
Setting up OpenDMARC
Ubuntu’s default configuration for OpenDMARC is pretty good; all the default
values worked for me. However, its default configuration doesn’t actually
validate SPF. Instead, it will just assume any SPF validation header is valid.
This is not what you want. You also probably want it to ignore authenticated
clients, so that you can still send emails. To do that, you can simply add the
## Allow authenticated users to do whatever. IgnoreAuthenticatedClients true ## Manually check for SPF. SPFIgnoreResults true SPFSelfValidate true
Working around the chroot
By default, on Ubuntu, postfix is configured to run in a chroot for security reasons. This means that we need to expose the sockets for both OpenDKIM and OpenDMARC in that chroot. To do that, we first need to make sure that both have a directory available to them, and then change their socket locations. First part: the directories.
for service in opendmarc opendkim; do # Create run directory sudo mkdir -p /var/spool/postfix/var/run/$service; sudo chown $service: # Add postfix to the group sudo usermod -a -G $service postfix done
Now we need to update the socket location for both daemons so that they actually put it in the right place. Just find the right lines and replace them with the following:
# /etc/opendkim.conf Socket local:/var/spool/postfix/var/run/opendkim/opendkim.sock # /etc/opendmarc.conf Socket local:/var/spool/postfix/var/run/opendmarc/opendmarc.sock
With that enabled, we can now add these two programs to milter (mail filter) lists, and then everything should work. We can add the following lines to Postfix’s
# OpenDKIM/OpenDMARC non_smtpd_milters=unix:/var/run/opendkim/opendkim.sock, unix:/var/run/opendmarc/opendmarc.sock smtpd_milters=unix:/var/run/opendkim/opendkim.sock, unix:/var/run/opendmarc/opendmarc.sock
Now we can restart (not reload, otherwise the added groups for Postfix won’t propagate) Postfix, and tada: you are now validating DMARC.
There are still some caveats. Currently, your set-up does only flags invalid emails, but still receives them. You can instruct OpenDMARC to reject them instead.
Bonus: sending DMARC reports
With the set-up described in this post, you will be validating all incoming email for DMARC conformance, but by default you won’t be sending the actual reports. These reports are supposed to inform senders of who’s spoofing their email (in case of failures) or in general who is receiving their mail. Doing so may be a privacy issue as you are notifying people that you are receiving their email.
The reporting tools built into OpenDMARC require MySQL to function but I personally don’t like Oracle very much, so I prefer MariaDB. This database system is mostly compatible with anything that supports MySQL, and it has a few extra goodies. You can install either one, and create a database and user for opendmarc.
One thing it doesn’t have, however, is the InnoDB patch that allows for key
sizes of arbitrairy length. This is unfortunate, because the default schema for
OpenDMARC’s (located in
database goes over that limit. Fortunately, there is very little harm in
shrinking the affected fields to be under the limit, unless you frequently
encounter domain names of over 191 code-points (not bytes) in length. The fixed
schema is shown below.
CREATE DATABASE IF NOT EXISTS opendmarc; USE opendmarc; SET TIME_ZONE='+00:00'; -- A table for mapping domain names and their DMARC policies to IDs CREATE TABLE IF NOT EXISTS domains ( id INT NOT NULL AUTO_INCREMENT, name VARCHAR(191) NOT NULL, firstseen TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY(id), UNIQUE KEY(name) ); -- A table for logging reporting requests CREATE TABLE IF NOT EXISTS requests ( id INT NOT NULL AUTO_INCREMENT, domain INT NOT NULL, repuri VARCHAR(255) NOT NULL, adkim TINYINT NOT NULL, aspf TINYINT NOT NULL, policy TINYINT NOT NULL, spolicy TINYINT NOT NULL, pct TINYINT NOT NULL, locked TINYINT NOT NULL DEFAULT '0', firstseen TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, lastsent TIMESTAMP NOT NULL DEFAULT '1970-01-01 00:00:01', PRIMARY KEY(id), KEY(lastsent), UNIQUE KEY(domain) ); -- A table for reporting hosts CREATE TABLE IF NOT EXISTS reporters ( id INT NOT NULL AUTO_INCREMENT, name VARCHAR(191) NOT NULL, firstseen TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY(id), UNIQUE KEY(name) ); -- A table for IP addresses CREATE TABLE IF NOT EXISTS ipaddr ( id INT NOT NULL AUTO_INCREMENT, addr VARCHAR(64) NOT NULL, firstseen TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY(id), UNIQUE KEY(addr) ); -- A table for messages CREATE TABLE IF NOT EXISTS messages ( id INT NOT NULL AUTO_INCREMENT, date TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, jobid VARCHAR(128) NOT NULL, reporter INT UNSIGNED NOT NULL, policy TINYINT UNSIGNED NOT NULL, disp TINYINT UNSIGNED NOT NULL, ip INT UNSIGNED NOT NULL, env_domain INT UNSIGNED NOT NULL, from_domain INT UNSIGNED NOT NULL, policy_domain INT UNSIGNED NOT NULL, spf TINYINT NOT NULL, align_dkim TINYINT UNSIGNED NOT NULL, align_spf TINYINT UNSIGNED NOT NULL, sigcount TINYINT UNSIGNED NOT NULL, PRIMARY KEY(id), KEY(date), UNIQUE KEY(reporter, date, jobid) ); -- A table for signatures CREATE TABLE IF NOT EXISTS signatures ( id INT NOT NULL AUTO_INCREMENT, message INT NOT NULL, domain INT NOT NULL, pass TINYINT NOT NULL, error TINYINT NOT NULL, PRIMARY KEY(id), KEY(message) ); -- CREATE USER 'opendmarc'@'localhost' IDENTIFIED BY 'changeme'; -- GRANT ALL ON opendmarc.* to 'opendmarc'@'localhost';
We then need to convince OpenDMARC to start writing its judgements to a file. It
doesn’t really matter where it stores them, so I store them in the run
directory. To do so, you just need to append the following to
## Save history HistoryFile /run/opendmarc/opendmarc.dat
After that, we need a daily cronjob that will collect all of this data into the
database, send out all the reports, and (optionally) discard old records. There
are various cronjobs listed online, but I figured I’d write my own. I use the
default username and database name so I don’t have to specify them to each
command, but you can add those with
# File: /etc/cron.daily/dmarc-reports #!/bin/bash HISTFILE=/var/run/opendmarc/opendmarc.dat # DBUSER=opendmarc # DBNAME=opendmarc DBPASS=s3cr37.P455w0rd REPORT_ORG=example.org REPORT_EMAILfirstname.lastname@example.org [[ ! -f $HISTFILE ]]; then echo "No DMARC history file found." exit 1 fi OLDHIST="$HISTFILE.old" mv -f "$HISTFILE" "$OLDHIST" # Import the new history opendmarc-import --dbpasswd="$DBPASS" < "$OLDHIST" # Send the reports to servers that haven't had a report in the last day opendmarc-reports --dbpasswd="$DBPASS" --report-email="$REPORT_EMAIL" \ --interval=86400 --report-org="$REPORT_ORG" # Optional: expire old data from the database to save space. opendmarc-expire --dbpasswd="$DBPASS"
And that’s it! You should now be sending daily DMARC reports to everyone mailing you that wants one. You will probably notice that quite a few (spam) domains request DMARC reports, but that they don’t actually accept emails. This is unfortunate, and you can probably filter them, which I may write about in the future. Until then, enjoy your system.