Revisiting my email server in 2022

More than two years have passed since my first post about setting up an email server in 2020 with OpenSMTPD and Dovecot and its sequel. Since then, my server been going strong, with a few minor hiccups along the way. In this post, I’ll explain some of the changes I made.

This assumes you’ve followed the preceding guides: the configuration snippets given here should be interpreted as modifications to those guides, not as complete setups!

Last updated on 2022-09-12.

More about DMARC

When I wrote my original guide, I didn’t properly understand how DMARC works: I misinterpreted it as an optional wrapper around SPF and DKIM. But oh God, I was wrong. Simon Andrews’ article “I figured out how DMARC works, and it almost broke me” showed me the ugly truth, and I highly recommend reading it. Briefly, DMARC does two arguably unrelated things.

Firstly, DMARC provides a way to diagnose issues with your SPF and DKIM configurations, in the form of reports that get sent to the ruf= and/or rua= email address(es) you put in the DNS record. Without this, there’s no way of knowing why your emails are getting marked as spam.

Secondly, it improves the trustworthiness of SPF and DKIM by enforcing alignment. This means something slightly different for SPF and DKIM, and boils down to fixing a glaring issue:

DMARC’s alignment refers to checking whether the domains match up for SPF and DKIM, thus ensuring that an SMTP server can’t pretend to be someone else. It sounds obvious, but nope, apparently it wasn’t before DMARC was made.

DKIMproxy

To add DKIM signatures to my messages, I switched from Rspamd to DKIMproxy.

Motivation

To sign outgoing emails for DKIM, my original guide used Rspamd — an unusual choice, since it’s a spam filter designed to act on incoming messages. Later, in the sequel to that guide, I needed some ugly workarounds to compensate for Rspamd’s “smartness” when I tried to play around with authentication schemes in OpenSMTPD. Clearly, this wasn’t ideal.

However, the reason I cut my losses and switched to another DKIM signer was actually a bug in MXToolBox’ deliverability tool. It appears that no matter what you do, this tool claims that your email’s signature fails validation. I’m not the first to notice this issue: see e.g. this question on Server Fault. Other tools like the DKIM validator say that my DKIM signatures are correct.

There aren’t many open-source alternatives out there for DKIM signing: the only ones I know of are OpenDKIM and DKIMproxy. The former is a so-called “milter”, meaning it can only interact with MTAs via the milter API, which is only supported by Sendmail and Postfix. Since we’re using OpenSMTPD, our only option is DKIMproxy, which consists of two daemons: dkimproxy.out to sign outgoing mail, and dkimproxy.in to verify incoming mail. We just need the former; Rspamd is still convenient for handling the latter’s functionality.

DKIM settings

Let’s start by disabling Rspamd’s DKIM signer in /etc/rspamd/local.d/dkim_signing.conf:

enabled = false;

Then configure dkimproxy.out as follows in /etc/dkimproxy/dkimproxy_out.conf. If you placed your DKIM public key in a TXT DNS record for <selector>._domainkey.example.com., and stored your private key in /path/to/dkim/private.key, then:

# Receive emails on 10027, sign them, and forward them to 10028
listen    127.0.0.1:10027
relay     127.0.0.1:10028

# Settings for email signing
domain    example.com
signature dkim(c=relaxed/relaxed,a=rsa-sha256)
keyfile   /path/to/dkim/private.key
selector  <selector>

Here, rsa-sha256 is the signature algorithm (this is the best available, because DKIM is ancient), and relaxed/relaxed is the so-called canonicalization method, which is applied before signing and verification, to prevent failures if e.g. the email’s whitespace gets changed in transit.

OpenSMTPD settings

OpenSMTPD needs to send all outbound mail through dkimproxy.out. In /etc/smtpd/smtpd.conf, we tell it that all emails coming from the MUA must be relayed through localhost:10027, and then picked up again on localhost:10028 after DKIM signing:

# Outbound
listen on eth0 port 465 smtps       pki "example.com" auth <passwds> tag "TRUSTED"
listen on eth0 port 587 tls-require pki "example.com" auth <passwds> tag "TRUSTED"
action "SIGN" relay host "localhost:10027"
match from any tag "TRUSTED" for any action "SIGN"

listen on lo port 10028 tag "SIGNED"
action "SEND" relay srs
match from any tag "SIGNED" for any action "SEND"

The tag name TRUSTED reflects that only messages from trusted (i.e. authenticated) MUAs should be signed. After signing, emails get the tag SIGNED, and are sent to their destination as usual.

SMTP relay

Instead of sending my emails directly to their destinations, I now send them to an SMTP relay server, which then passes them on to their actual destinations.

Motivation

Large email providers such as Google, Microsoft and Yahoo manage many user accounts, so for them it makes sense to keep track of IP-based sender reputations. For example, if a number of low-quality emails are sent from a single IP to many of the accounts they manage, it’s cheaper to simply blacklist that IP entirely at the MTA level, rather than passing each message through a computationally-intensive spam filter.

But, as usual, Microsoft has to ruin everything with their draconic policies. In a stroke of genius, someone there decided to blindly ban IPs, seemingly in blocks belonging to VPS providers. One day, I tried to send an email to an Outlook-based account, and OpenSMTPD reported it had been unable to make the delivery, because Microsoft had thrown an error:

Bounce message due to error from Microsoft

To their credit, they seem to be offering a way out. This approach is reasonable: preventively ban high-risk IP ranges, and allow “trustworthy” servers at the owner’s request. I got error 5.7.511, asking me to send an email to a support address. If you’re lucky, you may have a different error, and get the opportunity to use the slick delist portal instead. The URL in the bounce message links to this list of error codes.

I confess, I never actually bothered to forward the message to the provided address: my initial email was time-sensitive, so I couldn’t afford to wait for Microsoft’s response. Also, their customer support’s stellar reputation precedes them, so I chose to use my time more wisely. Even if they would’ve resolved it nicely, there’s nothing preventing Microsoft (or any other provider) from breaking my deliverability again in the future. Instead, I opted for a compromise.

As a result of providers’ IP reputation systems, a whole new business has appeared: SMTP relays. They offer to take the issue out of your hands: you send your emails through their servers, and they do their best to deliver them to large providers. SMTP relays are mostly used for sending marketing emails in bulk, but are also useful to avoid small-scale problems as described above. There are many SMTP relay services to choose from, at various prices.

Using an SMTP relay results in more reliable delivery of your messages to large providers, but an obvious concern is privacy: the relay server can read all your outgoing emails (but not incoming), so you’ll have to trust the service you choose. But it’s no worse than using a major provider, and if you’re sending sensitive material, why use email in the first place? Personally, I use SMTP2GO, but I can’t say how good they are; do your own research.

OpenSMTPD settings

Once you’ve chosen an SMTP relay service, let’s say relay.com, and set up your account, they’ll let you create credentials to use their SMTP servers. Suppose these credentials are <username> and <password>, create a file /etc/smtpd/relaypw with contents:

<label> <username>:<password>

Where <label> is a string of your choice. Note that <password> must be plaintext, because it needs to be provided to the relay server. To tell OpenSMTPD to use the relay, edit /etc/smtpd/smtpd.conf as follows, i.e. register the relaypw table and modify the SEND action:

table relaypw "/etc/smtpd/relaypw"

#action "SEND" relay srs ### Replace this line with the following:
action "SEND" relay host "smtps://<label>@relay.com:465" auth <relaypw> pki "example.com" srs

With <label> replaced by the label you chose earlier, and relay.com:465 replaced by the host/port combination given in the relay service’s documentation. Depending on what they support, you may also need to change the protocol smtps:// (SMTP over TLS) to smtp:// (SMTP with optional STARTTLS) or smtp+tls:// (SMTP with mandatory STARTTLS). I recommend SMTPS.

DNS records

If you’ve been paying attention so far, you have a burning question: what about SPF and stuff? Wasn’t the point to prevent SMTP servers from sending emails on others’ behalf? Well, yes, so you’ll need to add some DNS records for the relay to work. The details depend on which service you choose, so they’ll tell you what to do when you’re setting up your account. As an example, based on my experience with SMTP2GO, you may need to add two CNAME records like:

emXXXXXX.example.com. CNAME return.relay.com.
sXXXXXX._domainkey.example.com. CNAME dkim.relay.com.

Which, roughly speaking, respectively enable SPF and DKIM. Here, XXXXXX is an ID that the service will provide to you, since the DNS addresses must be unique.

Technically, those two DNS records should be enough for SPF and DKIM, but in practice, it seems that different email providers/tools have slightly different interpretations of these standards, and can get confused when an email passes through multiple unaffiliated SMTP servers. Therefore, I recommend explicitly adding the relay to your SPF policy:

example.com TXT "v=spf1 mx include:spf.relay.com -all"

Your relay service might not publicly document their version of spf.relay.com, but you can find it by looking up your CNAME emXXXXXX.example.com (or equivalent) in MXToolBox’ SPF tool.

I also recommend relaxing your DMARC domain policy for SPF and DKIM, such that your CNAME subdomains still pass the alignment checks:

_dmarc.example.com. TXT "v=DMARC1; aspf=r; adkim=r; ..."

Be sure to check that it’s set up correctly using the website “Learn and test DMARC”.