SMTP MTA STS with SMTP TLS Reporting

I watched a DNS request for _mta-sts.randy7.com get rejected and it made me wonder. What was that? It’s part of SMTP MTA Strict Transport Security. It uses DNS and HTTPS to prevent downgrade attacks, so email isn’t sent as plaintext when encryption is available.

Turning off non-encrypted transport modes would also prevent a downgrade attack but at the cost of some email being undeliverable. It’s all about risks and best practices.

My current email setup is a modified version of OpenSMTPD developer Gilles Chehade’s “Setting up a mail server with OpenSMTPD, Dovecot and Rspamd” post. He didn’t cover this, so I decided to try MTA-STS and SMTP TLS Reporting on my own.

This is only for receiving email securely from mail servers that check for MTA-STS. This does not include checking for MTA-STS when delivering email.

Current Status

Preparation

First, I prepared for SMTP TLS Reporting by adding the _smtp._tls TXT record. That’s all.

Now for MTA-STS.

Next, I added A and AAAA records for host mta-sts so httpd can eventually serve this URL: https://mta-sts.randy7.com/.well-known/mta-sts.txt. Finally, I added the DNS entry for _mta-sts which has an id that must be updated each time the policy file, mta-sts.txt, is changed.

File: /var/nsd/zones/master/randy7.com

$ORIGIN randy7.com.     
[...]
_smtp._tls    IN    TXT    "v=TLSRPTv1;rua=mailto:postmaster@randy7.com"
_mta-sts      IN    TXT    "v=STSv1; id=2021021101;"
mta-sts       IN    A      71.19.146.7
mta-sts       IN    AAAA   2605:2700:0:3:a800:ff:fe17:f907
[...]

Because the mta-sts.txt file must be served over TLS, I added a mta-sts.randy7.com TLS certificate.

File: /etc/acme-client.conf

[...]
domain mta-sts.randy7.com {
        domain key "/etc/ssl/private/mta-sts.randy7.com.key"
        domain full chain certificate "/etc/ssl/mta-sts.randy7.com.fullchain.pem"
        sign with letsencrypt
}
[...]

Let’s not forget to renew this certificate.

File: /etc/daily.local

[...]
acme-client -v mta-sts.randy7.com && rcctl reload httpd
[...]

And because the certificate doesn’t exist yet, the acme-challenge will need to go over HTTP. Also notice the mta-sts.txt file is only served over HTTPS.

File: /etc/httpd.conf

[...]
server "mta-sts.randy7.com" {
        listen on * port 80
        location "/.well-known/acme-challenge/*" {
                root "/acme"
                request strip 2
        }
        location * {
                block
        }
}

server "mta-sts.randy7.com" {
        listen on * tls port 443
        tls {
                certificate "/etc/ssl/mta-sts.randy7.com.fullchain.pem"
                key "/etc/ssl/private/mta-sts.randy7.com.key"
        }
        location "/.well-known/mta-sts.txt" {
                root "/mta-sts"
                request strip 1
        }
        location "/.well-known/acme-challenge/*" {
                root "/acme"
                request strip 2
        }
        location * {
                block
        }
}
[...]

Finally, we need a policy. No testing here, just going full enforce mode for one week.

File: /var/www/mta-sts/mta-sts.txt

version: STSv1
mode: enforce
mx: randy7.com
max_age: 604800

Activation

Now that everything is prepared, we can start activating all the peices.

$ rcctl restart httpd
httpd(ok)
httpd(ok)

$ nsd-control reload randy7.com
ok

$ acme-client -v mta-sts.randy7.com
acme-client: /etc/ssl/private/mta.randy7.com.key: generated RSA domain key
[...]
acme-client: /etc/ssl/mta.randy7.com.fullchain.pem: created

$ rcctl restart httpd
httpd(ok)
httpd(ok)

$ ftp -o - https://mta-sts.randy7.com/.well-known/mta-sts.txt 2> /dev/null  
version: STSv1
mode: enforce
mx: randy7.com
max_age: 604800

$ dig +short -t txt _mta-sts.randy7.com
"v=STSv1; id=2021021101;"

$ dig +short -t txt _smtp._tls.randy7.com
"v=TLSRPTv1;rua=mailto:postmaster@randy7.com"

Then I sent an email from a Gmail account to myself. Here are DNS requests from Google, from tshark.

74.125.44.144   randy7.com
74.125.40.6     randy7.com
74.125.44.69    _mta-sts.randy7.com
74.125.40.76    randy7.com
74.125.191.14   _mta-sts.randy7.com
66.249.64.177   mta-sts.randy7.com
66.249.79.124   mta-sts.randy7.com

Here is the HTTP request from /var/www/logs/access.log.

mta-sts.randy7.com 74.125.150.77 "GET /.well-known/mta-sts.txt HTTP/1.1" 200 60

And here is the message delivery from /var/log/maillog.

smtp connected address=209.85.160.172 host=mail-qt1-f172.google.com
smtp tls ciphers=TLSv1.3:AEAD-AES256-GCM-SHA384:256
smtp message msgid=6ac45a2e size=2723 nrcpt=1 proto=ESMTP
smtp envelope evpid=6ac45a2e0f4a01ba from=<[redacted]@gmail.com> to=<test@randy7.com>
mda delivery evpid=6ac45a2e0f4a01ba from=<[redacted]@gmail.com> to=<test@randy7.com> rcpt=<test@randy7.com> user=test delay=0s result=Ok stat=Delivered
smtp disconnected reason=quit

Success! Or so I thought.

The next day Google emailed me this:

{  "organization-name": "Google Inc.",
   "date-range": {
       "start-datetime": "2021-02-11T00:00:00Z",
       "end-datetime": "2021-02-11T23:59:59Z" },
   "contact-info": "smtp-tls-reporting@google.com",
   "report-id": "2021-02-11T00:00:00Z_randy7.com",
   "policies": [{
       "policy": {
           "policy-type": "no-policy-found",
           "policy-domain": "randy7.com" },
       "summary": {
           "total-successful-session-count": 2,
           "total-failure-session-count":0 }}]}

But there was nothing wrong, so I waited another day and then Google reported a success. One email sent using the policy, and one sent without the policy. I assume the one sent without the policy was the TLS report.

{   "organization-name":"Google Inc.",
    "date-range": {
        "start-datetime":"2021-02-12T00:00:00Z",
        "end-datetime":"2021-02-12T23:59:59Z" },
    "contact-info":"smtp-tls-reporting@google.com",
    "report-id":"2021-02-12T00:00:00Z_randy7.com",
    "policies": [
        {
            "policy": {
                "policy-type":"no-policy-found",
                "policy-domain":"randy7.com" },
            "summary": {
                "total-successful-session-count":1,
                "total-failure-session-count":0 }
        }, {
            "policy": {
                "policy-type":"sts",
                "policy-string": [
                    "version: STSv1",
                    "mode: enforce",
                    "mx: randy7.com",
                    "max_age: 604800"],
                "policy-domain":"randy7.com" },
            "summary": {
                "total-successful-session-count":2,
                "total-failure-session-count":0 }}]}

Much better!

Thank you Google, httpd, Let’s Encrypt, nsd, OpenBSD, and OpenSMTPD.