Securing incoming email with mta-sts

This document describes how to add mail transfer agent strict transport security to our email server to protect incoming email against confidentiality issues such as man in the middle attacks. It has been adopted by all the main email providers so is generally seen as a good thing to have. Specifically it ensures all incoming email is encrypted from the off and so avoids the possibility of downgrade attacks to starttls connections. Unencrypted email simply will not be delivered. We'll learn how to protect two (or more) domains and for that three additional letsencrypt certificates will be needed.

At least temporarily, until everything is seen to be working as expected you should also implement SMTP TLS Reporting (TLSRPT), which is rather like DMARC reporting. This is an essential part of a process to make sure everything is done right and thus to avoid losing incoming email. Passing the comprehensive test suites at both the mxtoolbox site and mailhardener.com with flying colours is important, but no guarantee the job is complete. It's entirely possibly to continue to receive failed-session reports claiming certificate-host-mismatch or certificate-not-trusted when mxtoolbox says everything is ok. Google are particularly reliable at sending the reports and a gmail.com address to do test sends is very helpful.

As with the protocols now essential to ensure that outgoing email is delivered to inbox as opposed to a junk folder, mta-sts is an add on reaction to the fact of a less innocent digital world. There are several disparate areas to complete the implementation; here we will start with the setting up of a subdomain to serve a policy file over a secure connection. Provided all domains to be protected by mta-sts will always use the same policy a single document can be served for multiple domains, but a fully trusted (not self-signed) certificate will be required for each domain. Without going into details for this, I strongly recommend letsencrypt. The subdomain's certificate must be for mta-sts.mydomain.com (replace mydomain.com with your own domain). Once we have the certificate we can configure apache2 to serve our subdomain.

<VirtualHost *:443>
    ServerName mta-sts.mydomain.com
    DocumentRoot /srv/mta-sts/
    SSLEngine on
    SSLOptions +StrictRequire
    SSLCertificateFile /etc/letsencrypt/live/mta-sts.mydomain.com/fullchain.pem
    SSLCertificateKeyFile /etc/letsencrypt/live/mta-sts.mydomain.com/privkey.pem
    Include /etc/letsencrypt/options-ssl-apache.conf
<VirtualHost>
The policy must be served down the correct file path from the subdomain's DocumentRoot and this must be .well-known/mta-sts.txt. You do not specify that in your virtual host file. For now the mta-sts.txt file and its position on the filesystem can look like this:
david@eurydice $ cat /srv/mta-sts/.well-known/mta-sts.txt
version: STSv1
mode: testing
mx: mx.mydomain.com
max_age: 86401
The version: field must come first and at the moment STSv1 is the only acceptable value. We're using testing mode: as policy for now to ensure that no email will be refused delivery until we're certain all works as expected. The other two options here are self-explanatory and are none and enforce.

There may be additional mx: lines as necessary if mydomain.com has more than one mail exchanger. It should also be possible for a second domain to use a different mail exchanger, but the setup is greatly simplified if our single exim4 instance does not present multiple DNS identities. For present purpose, I'm advising not to do as is suggested here. Exim4 should have been configured to handle mail for all your domains - dpkg-reconfigure exim4-config 4th question. So rather than create a mail exchanger at example.org, instead state that mx.example.co.uk is the mail exchanger for example.org and do not specify A and AAAA records for it as that is done already in the example.org.uk zone file. In short, three records in that example(!) can be replaced with one:

@   IN   MX  10 mx.example.org.
mx IN  A x.x.x.x
mx IN  AAAA x:x::x:x

@   IN   MX   10 mx.example.org.uk.
The max_age: field unit is seconds and our suggested value for now is a day plus a second for which there are two reasons. Firstly, if something is not quite right we are not going to have mail servers with email for us caching an error for a lengthy period. Secondly, we will want to have SMTP TLS Reporting for our intitial testing period at least and some domains will only send reports if the value is greater than a single day.

Our subdomain will need to be registered in DNS; if you manage your own DNS using the bind name server, you can add two records to your zone file for that, but additionally also add two text records. The first is to indicate that our mail exchanger is mta-sts ready, the second that we wish to receive reports. Also of course, increment the serial number:
mta-sts IN A    x.x.x.x
mta-sts IN AAAA x:x::x:x
_mta-sts.mydomain.com.  IN   TXT  "v=STSv1; id=20241101"
_smtp._tls.mydomain.com.  IN TXT  "v=TLSRPTv1; rua=mailto:<your email address>"
Then before moving on to the next step, we can test what we have so far with the wget and dig utilities:
david@bulawayo:~ $ wget -q -O- https://mta-sts.mydomain.com/.well-known/mta-sts.txt
version: STSv1
mode: testing
mx: mx.mydomain.com
max_age: 86401

david@bulawayo:~ $ dig +short -t txt _mta-sts.mydomain.com
"v=STSv1; id=20241101"
And if that all looks ok, test our domains at mxtoolbox.

So everything is good, but you still get reports of session failures and definitely do not switch from testing to enforce policy until they go away! The reason is that your exim4 mail exchanger is using the certificate and key that were either generated at install time or were later updated with the /usr/share/doc/exim4-base/examples/exim-gencert utility. We will replace those with a trusted certificate and key, but a quick comparison of /etc/exim4 with /etc/letsencrypt indicates a complication:

david@eurydice $ ls -l /etc/exim4/exim.*
-rw-r----- 1 root Debian-exim 1298 Nov  5 13:00 /etc/exim4/exim.crt
-rw-r----- 1 root Debian-exim 1704 Nov  5 12:58 /etc/exim4/exim.key

root@eurydice: # ls -l /etc/letsencrypt/archive/mydomain.com/
-rw-r--r-- 1 root root 1854 Mar 17  2022 cert1.pem
-rw-r--r-- 1 root root 1854 Jun  3  2022 cert2.pem
-rw-r--r-- 1 root root 1850 Aug 15  2022 cert3.pem
-rw-r--r-- 1 root root 3749 Mar 17  2022 chain1.pem
-rw-r--r-- 1 root root 3749 Jun  3  2022 chain2.pem
-rw-r--r-- 1 root root 3749 Aug 15  2022 chain3.pem
-rw-r--r-- 1 root root 5603 Mar 17  2022 fullchain1.pem
-rw-r--r-- 1 root root 5603 Jun  3  2022 fullchain2.pem
-rw-r--r-- 1 root root 5599 Aug 15  2022 fullchain3.pem
-rw------- 1 root root 1704 Mar 17  2022 privkey1.pem
-rw------- 1 root root 1704 Jun  3  2022 privkey2.pem
-rw------- 1 root root 1708 Aug 15  2022 privkey3.pem
A letsencrypt certificate is created with root ownership and the key is not group readable. The problem will persist each time a renewal is required. First, lets get a trusted certificate for our exim4 and configure it to use that. I assume that TLS has already been added to the exim4 configuration as suggested here, so just two further lines are needed in the main/03_exim4-config_tlsoptions section:
root@eurydice: # certbot certonly -d mx.mydomain.com

root@eurydice: # vi /etc/exim4/exim4.conf.template
# Full paths to Certificate and Private Key. The Private Key file
# must be kept 'secret' and should be owned by root.Debian-exim mode
# 640 (-rw-r-----). exim-gencert takes care of these prerequisites.
# Normally, exim4 looks for certificate and key in different files:
#   MAIN_TLS_CERTIFICATE - path to certificate file,
#                          CONFDIR/exim.crt if unset
#   MAIN_TLS_PRIVATEKEY  - path to private key file
#                          CONFDIR/exim.key if unset
# You can also configure exim to look for certificate and key in the
# same file, set MAIN_TLS_CERTKEY to that file to enable. This takes
# precedence over all other settings regarding certificate and key file.

#!CHANGED*****added this *************************
MAIN_TLS_CERTIFICATE = /etc/letsencrypt/live/mx.mydomain.com/fullchain.pem
MAIN_TLS_PRIVATEKEY = /etc/letsencrypt/live/mx.mydomain.com/privkey.pem
#*************************************************
Before the usual exim update and restart, we have to sort out the ownership and permissions issue. This is best done with a shell script that can be run each time our certificate is renewed.
root@eurydice:/home/david# cat /root/exim_cert.sh 
#!/bin/sh
#exim_cert.sh must run after certificate renewals
#to  make the certificate available for exim4

cd /etc/letsencrypt
chown root:Debian-exim archive/
chown -R root:Debian-exim archive/mx.mydomain.com
chmod 640 archive/dmatthews.org/privkey*.pem

chown root:Debian-exim live/
chown -R root:Debian-exim live/mx.mydomain.com

exit 0
That will make the necessary changes to allow exim4 to serve the certificate and read the key and it can now be updated and restarted in the usual way. Although the script will need to be run immediately after future certificate renewals, the path to the certificate and key do not change and no further exim4 tinkering will be necessary.

You can check the certificate that exim4 is using:

david@bulawayo:~ $ openssl s_client  -connect  mydomain.com:25 -starttls smtp
CONNECTED(00000003)
depth=2 C = US, O = Internet Security Research Group, CN = ISRG Root X1
verify return:1
depth=1 C = US, O = Let's Encrypt, CN = E5
verify return:1
depth=0 CN = mx.mydomain.com
verify return:1
---
Certificate chain
 0 s:CN = mx.mydomain.com
   i:C = US, O = Let's Encrypt, CN = E5
   a:PKEY: id-ecPublicKey, 256 (bit); sigalg: ecdsa-with-SHA384
   v:NotBefore: Nov  6 09:12:55 2024 GMT; NotAfter: Feb  4 09:12:54 2025 GMT
 1 s:C = US, O = Let's Encrypt, CN = E5
   i:C = US, O = Internet Security Research Group, CN = ISRG Root X1
   a:PKEY: id-ecPublicKey, 384 (bit); sigalg: RSA-SHA256
   v:NotBefore: Mar 13 00:00:00 2024 GMT; NotAfter: Mar 12 23:59:59 2027 GMT
---
Server certificate
-----BEGIN CERTIFICATE-----
/\/\/\/\/\/\/\/\/\/\/\/\/\/
Note the CN record which must match your mail exchanger's record in DNS. This is not necessarily the same certificate that mydomain.com will use to host its web site securely. The certificate dates must be within bounds. The best check though is successful reports for all domains.

The policy file can now be edited, replacing testing with enforce; I'm in no rush to alter the max_age: field to allow longer caching of the policy but it can be set as high as 31,557,600 (one year). 604800 (a week) is a common suggestion. Don't forget to update the id field of the _mta-sts.mydomain.com record. I'd also suggest switching from archived TLSRPT reports to single failure reports, which we hope to never see. This is done by altering the _smtp._tls.mydomain.com record, changing the rua=mailto: field to ruf=mailto:.