Post

MTA-STS Actually Working — Postfix, Not RSPAMD

MTA-STS Actually Working — Postfix, Not RSPAMD

MTA-STS Actually Working — Postfix, Not RSPAMD

There are a lot of MTA-STS guides floating around, and a surprising number of them include RSPAMD configuration steps that look authoritative but don’t actually enforce anything. I followed one of them. This post is what I learned cleaning it up and getting MTA-STS working correctly.

What MTA-STS Actually Does

Before getting into the how, it’s worth being precise about what MTA-STS is and isn’t.

MTA-STS (Mail Transfer Agent Strict Transport Security) is a mechanism that lets a domain declare that mail destined for it must be delivered over verified TLS — meaning the connecting server must present a valid, trusted certificate. Without it, SMTP will happily negotiate an opportunistic TLS connection with an untrusted or self-signed cert, which offers basically no protection against a man-in-the-middle attack.

It operates in two directions:

Inbound (your domain): You publish a policy and a DNS record. Remote senders that support MTA-STS will fetch your policy and enforce verified TLS when connecting to your MX hosts. Your server is passive here — you publish, others enforce.

Outbound (your server): Your server fetches remote domains’ MTA-STS policies and enforces verified TLS when delivering to them. This is the active part, and it requires actual software.

Where the Common Guides Go Wrong

The guide I originally followed included an RSPAMD configuration section that looked like this:

1
2
3
4
5
6
7
MTA_STS_DOMAINS {
  type = "from";
  map = "/etc/rspamd/mta_sts_domains.map";
  description = "Domains with MTA-STS policies";
  score = 0.0;
  action = "no action";
}

This does nothing useful. It tags messages from domains that happen to be in a manually maintained flat file, but it doesn’t fetch policies, doesn’t validate certificates, and doesn’t enforce anything. RSPAMD operates at the content inspection layer — it never touches the raw SMTP TLS negotiation, so it architecturally cannot enforce MTA-STS. The section was cargo-culted from somewhere and added noise to logs without providing any security value.

The correct tool for outbound MTA-STS enforcement is postfix-mta-sts-resolver. It runs as a daemon, fetches and caches remote MTA-STS policies, and feeds policy decisions to Postfix via a socketmap interface. Postfix then enforces them at connection time.

What the Working Stack Looks Like

1
2
3
4
5
6
7
DNS (_mta-sts TXT record) + HTTPS policy file (Apache)
        ↓
postfix-mta-sts-resolver  (127.0.0.1:8461)
        ↓
smtp_tls_policy_maps  (Postfix socketmap)
        ↓
Postfix enforces verified TLS outbound

RSPAMD doesn’t appear in this diagram because it has no role here.


Implementation

Step 1: Policy Files

For each domain you control, you need a policy file served over HTTPS at a specific URL. The URL format is fixed by the spec:

1
https://mta-sts.yourdomain.com/.well-known/mta-sts.txt

Note that this requires a dedicated subdomain with its own TLS certificate — not a path on your main domain.

Before writing the policy file, check your actual MX records:

1
dig MX yourdomain.com +short

Every MX host returned must appear in the policy file. Missing even one will cause delivery failures once you’re in enforce mode, because compliant senders will refuse to use an MX that isn’t listed.

A policy file for a domain with two MX hosts:

1
2
3
4
5
version: STSv1
mode: enforce
mx: mail.yourdomain.com
mx: mail.secondarydomain.com
max_age: 86400

If you’re just starting out, use mode: testing first. Testing mode logs violations but doesn’t block delivery, giving you time to validate your setup before you risk deferring legitimate mail.

Step 2: Apache Configuration

Each mta-sts subdomain needs its own virtual host. The HTTP vhost just redirects to HTTPS:

1
2
3
4
5
6
<VirtualHost *:80>
    ServerName mta-sts.yourdomain.com
    DocumentRoot /var/www/mta-sts.yourdomain.com
    RewriteEngine on
    RewriteRule ^ https://%{SERVER_NAME}%{REQUEST_URI} [END,NE,R=permanent]
</VirtualHost>

The HTTPS vhost serves the policy file:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<IfModule mod_ssl.c>
<VirtualHost *:443>
    ServerName mta-sts.yourdomain.com
    DocumentRoot /var/www/mta-sts.yourdomain.com

    <Directory /var/www/mta-sts.yourdomain.com>
        Options -Indexes +FollowSymLinks
        AllowOverride All
        Require all granted
    </Directory>

    <Directory "/var/www/mta-sts.yourdomain.com/.well-known">
        Require all granted
    </Directory>

    SSLCertificateFile /etc/letsencrypt/live/mta-sts.yourdomain.com/fullchain.pem
    SSLCertificateKeyFile /etc/letsencrypt/live/mta-sts.yourdomain.com/privkey.pem
    Include /etc/letsencrypt/options-ssl-apache.conf
</VirtualHost>
</IfModule>

Get a certificate for each subdomain:

1
certbot certonly --apache -d mta-sts.yourdomain.com

Verify the policy file is actually being served correctly:

1
2
curl -sv https://mta-sts.yourdomain.com/.well-known/mta-sts.txt 2>&1 \
    | grep -E "(< HTTP|version:|mode:|mx:|max_age:)"

You want HTTP/1.1 200 OK and the full policy content. Also verify the certificate chain:

1
2
3
echo | openssl s_client -connect mta-sts.yourdomain.com:443 \
    -servername mta-sts.yourdomain.com 2>/dev/null \
    | openssl x509 -noout -dates -issuer

Step 3: DNS Records

Add a TXT record for each domain:

1
_mta-sts.yourdomain.com.  IN  TXT  "v=STSv1; id=20250101"

The id field is a cache-buster. Remote servers cache your policy for up to max_age seconds, and only re-fetch when they see a changed id. Update it whenever you change the policy file.

Formatting matters here. The value must be unescaped:

1
2
"v=STSv1; id=20250101"      ← correct
"\"v=STSv1; id=20250101\""  ← breaks external discovery

Some DNS provider UIs add escape characters automatically. Verify what actually got published:

1
2
3
4
dig TXT _mta-sts.yourdomain.com +short
# Also test from external resolvers
dig @8.8.8.8 TXT _mta-sts.yourdomain.com +short
dig @1.1.1.1  TXT _mta-sts.yourdomain.com +short

If you want TLS failure reports delivered to your inbox, add a TLS-RPT record too:

1
_smtp._tls.yourdomain.com.  IN  TXT  "v=TLSRPTv1; rua=mailto:tls-reports@yourdomain.com"

Step 4: Postfix TLS Baseline

MTA-STS outbound enforcement builds on top of existing Postfix TLS configuration. Your main.cf should already have something like this:

1
2
3
4
5
6
7
8
9
# Inbound
smtpd_tls_cert_file = /etc/letsencrypt/live/mail.yourdomain.com/fullchain.pem
smtpd_tls_key_file  = /etc/letsencrypt/live/mail.yourdomain.com/privkey.pem
smtpd_tls_security_level = may
smtpd_tls_protocols = !SSLv2, !SSLv3, !TLSv1, !TLSv1.1

# Outbound
smtp_tls_security_level = may
smtp_tls_protocols = !SSLv2, !SSLv3, !TLSv1, !TLSv1.1

smtp_tls_security_level = may is correct here — opportunistic TLS for domains without an MTA-STS policy. The resolver will upgrade specific domains to secure as needed.

Step 5: Install and Verify postfix-mta-sts-resolver

On Debian 13 (Trixie), the package is in the main repo:

1
apt install postfix-mta-sts-resolver

The package enables and starts postfix-mta-sts-resolver.service automatically. Verify:

1
2
systemctl status postfix-mta-sts-resolver.service
ss -tlnp | grep 8461

The daemon should be active and listening on 127.0.0.1:8461. The default config at /etc/mta-sts-daemon.yml uses a SQLite cache and is fine for most deployments.

Before wiring this into Postfix, test it directly. Gmail is a good target since they publish a well-known MTA-STS policy:

1
postmap -q gmail.com socketmap:inet:127.0.0.1:8461:postfix

Expected output:

1
secure match=.gmail-smtp-in.l.google.com:gmail-smtp-in.l.google.com servername=hostname

secure means the resolver fetched the policy and will tell Postfix to enforce verified TLS for that domain. If you get no output, check the service status.

Step 6: Wire It Into Postfix

Two settings to add, then a reload:

1
2
3
postconf -e 'smtp_tls_policy_maps = socketmap:inet:127.0.0.1:8461:postfix'
postconf -e 'smtp_tls_CAfile = /etc/ssl/certs/ca-certificates.crt'
postfix reload

The first tells Postfix to consult the resolver for every outbound delivery. The second is easy to miss and causes a frustrating failure mode: without a CA bundle, Postfix can’t verify any remote certificate and will defer all MTA-STS enforced mail with:

1
status=deferred (Server certificate not verified)

On Debian the CA bundle is at /etc/ssl/certs/ca-certificates.crt. Set it and the problem goes away.

Step 7: Verify End-to-End

Send a test email to an address at a domain you know publishes MTA-STS:

1
echo "MTA-STS test" | sendmail -v someone@gmail.com

Then check the logs:

1
journalctl -u postfix -n 30 | grep -E "(TLS|status=)"

What you want to see:

1
2
Verified TLS connection established to gmail-smtp-in.l.google.com[...]:25: TLSv1.3 ...
... status=sent (250 2.0.0 OK ...)

The word Verified is what matters. It means the certificate was checked against the CA bundle and passed. Untrusted TLS connection established means it didn’t — check your smtp_tls_CAfile setting.


Ongoing Maintenance

When you update a policy file: bump the DNS id value so remote servers invalidate their cache and re-fetch. Otherwise they’ll keep enforcing the old policy for up to max_age seconds.

Watch the mail queue: after enabling enforcement, check mailq over the following day. Any remote domain with a broken MTA-STS policy — mismatched MX, expired cert, unreachable policy URL — will cause your outbound mail to that domain to defer. It’s not your fault, but you’ll want to know it’s happening.

Monitor Apache logs for policy fetches: once your DNS propagates, external mail servers will start fetching your policy files. External IPs hitting /.well-known/mta-sts.txt in your Apache logs confirms remote servers are discovering and caching your policy.

1
tail -f /var/log/apache2/mta-sts.yourdomain.com_access.log

Certificate renewal: make sure your renewal process covers the mta-sts.* subdomains. A lapsed cert on the policy endpoint breaks inbound enforcement for anyone trying to fetch your policy.


Summary

MTA-STS is a Postfix and DNS problem. RSPAMD has no role in it. The working approach is:

  1. Publish policy files via Apache with valid TLS certificates
  2. Add _mta-sts TXT records to DNS with correctly formatted values
  3. Install postfix-mta-sts-resolver and verify it responds to policy queries
  4. Add smtp_tls_policy_maps and smtp_tls_CAfile to Postfix
  5. Reload and confirm Verified TLS in the logs

If you already followed a guide that added RSPAMD configuration for MTA-STS, you can safely remove it — it was doing nothing. Clean up any MTA_STS_DOMAINS blocks in multimap.conf and delete the accompanying .map file, run rspamadm configtest, and restart RSPAMD. Nothing will change in behavior because nothing was being enforced.

This post is licensed under CC BY 4.0 by the author.