MTA-STS: how to protect email from downgrade attacks
SMTP was designed in 1982. Encryption was bolted on later through STARTTLS, an opportunistic upgrade that any man-in-the-middle can strip away in transit. MTA-STS fixes this by telling the world: “My mail server requires TLS. If you can’t establish an encrypted connection, don’t deliver the message at all.” This article walks through the protocol, its DNS and HTTPS components, and a complete setup from scratch.
The problem: STARTTLS is optional by default
When a sending mail server connects to a receiving server, it opens a plaintext SMTP session on port 25 and asks whether the receiver supports STARTTLS. If the answer is yes, both sides negotiate a TLS handshake and continue over an encrypted channel. If the answer is no, or if someone on the network removes the STARTTLS response before it reaches the sender, the message travels in plaintext.
This is a downgrade attack. The attacker does not need to break TLS. They only need to intercept one line of text in the SMTP handshake: the 250 STARTTLS response. On networks where BGP hijacking or DNS spoofing is possible, stripping STARTTLS is straightforward.
DANE (DNS-Based Authentication of Named Entities) addressed part of this by publishing TLSA records in DNSSEC-signed zones. But DANE requires DNSSEC, and DNSSEC adoption remains low. According to APNIC data from late 2025, fewer than 5% of .com domains are DNSSEC-signed. MTA-STS takes a different approach: it uses HTTPS, infrastructure that already exists everywhere, to distribute the TLS policy. No separate zone signing needed.
How MTA-STS works
MTA-STS (RFC 8461) has two components: a DNS TXT record that signals the policy exists, and an HTTPS-hosted policy file that contains the actual rules. The sending server checks both before delivering mail.
The flow, step by step:
- Sender resolves MX records for the recipient domain.
- Sender queries
_mta-sts.example.comfor a TXT record. - If the record exists, sender fetches
https://mta-sts.example.com/.well-known/mta-sts.txt. - Sender caches the policy for the duration specified in
max_age. - On every subsequent delivery, sender enforces the policy: require valid TLS, verify the MX hostname against the
mxpatterns in the policy. If TLS negotiation fails, the message is not delivered.
The HTTPS part is what makes it secure. An attacker who can tamper with DNS can spoof a TXT record, but forging a valid TLS certificate for mta-sts.example.com is a different problem entirely. That asymmetry is where the protection comes from.
Step 1. Create the policy file
The policy file is a plain text file served at a fixed URL. It has exactly four fields.
version: STSv1 mode: testing mx: mail.example.com mx: backup-mx.example.com max_age: 86400
Field-by-field breakdown:
version: STSv1is the only valid version right now.modehas three options:testing,enforce, andnone. Start withtestingto collect reports without blocking mail. Move toenforceonce you are confident everything is working.mxlists MX hostnames the policy allows, one per line. Wildcards work:*.example.commatchesmail.example.com. These must match the Common Name or SAN in the server’s TLS certificate.max_ageis the cache lifetime in seconds. 86400 is one day, a sensible starting value. After switching toenforce, raise it to 604800 (one week) or higher. The longer the cache, the longer senders will require TLS even if your HTTPS endpoint goes down temporarily. That is both the benefit and the risk of a large value.
For Google Workspace domains, use Google’s MX hostnames:
version: STSv1 mode: enforce mx: aspmx.l.google.com mx: *.aspmx.l.google.com mx: *.googlemail.com max_age: 604800
For Microsoft 365:
version: STSv1 mode: enforce mx: *.mail.protection.outlook.com max_age: 604800
Step 2. Serve the policy over HTTPS
The file must be accessible at exactly https://mta-sts.example.com/.well-known/mta-sts.txt. You need a subdomain mta-sts pointed at a web server with a valid TLS certificate. Self-signed certificates will not work: senders validate the full chain.
Nginx configuration:
server {
listen 443 ssl;
server_name mta-sts.example.com;
ssl_certificate /etc/letsencrypt/live/mta-sts.example.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/mta-sts.example.com/privkey.pem;
location = /.well-known/mta-sts.txt {
root /var/www/mta-sts;
default_type text/plain;
}
location / {
return 404;
}
}Place the policy file at /var/www/mta-sts/.well-known/mta-sts.txt. Get a certificate with Let’s Encrypt:
certbot certonly --nginx -d mta-sts.example.com
If you prefer not to run a separate server, host the file on a static platform. GitHub Pages, Cloudflare Pages, or S3 + CloudFront all work. The only requirement is valid HTTPS on the mta-sts subdomain. Create a repo, put the policy file at .well-known/mta-sts.txt, attach a custom domain, and the platform handles HTTPS.
Step 3. Add the DNS records
Two records: an A or CNAME for the subdomain, and a TXT signal for senders.
# A record for the policy web server mta-sts.example.com. IN A 203.0.113.10 # TXT record — signals senders that a policy exists _mta-sts.example.com. IN TXT "v=STSv1; id=20260427001"
The id field is a string senders use to detect policy changes. Every time you update the policy file, change the id. A timestamp-based format like 20260427001 (date plus a sequence number) works well in practice. Senders compare the id in DNS against the cached policy. If they differ, the sender re-fetches the policy file.
Step 4. Enable TLS-RPT reporting
TLS-RPT (RFC 8460) is the reporting mechanism for MTA-STS. It tells senders where to send reports about TLS negotiation failures. Without it, you have no visibility into whether legitimate mail is being blocked by a TLS misconfiguration.
_smtp._tls.example.com. IN TXT "v=TLSRPTv1; rua=mailto:tls-reports@example.com"
Reports arrive as JSON files (gzipped) attached to emails. They contain information about successful and failed TLS connections: the sending IP, the result type (certificate-expired, starttls-not-supported, validation-failure), and the count. Google, Microsoft, Yahoo, and other major senders all support TLS-RPT.
A sample TLS-RPT report (simplified):
{
"organization-name": "Google Inc.",
"date-range": {
"start-datetime": "2026-04-27T00:00:00Z",
"end-datetime": "2026-04-28T00:00:00Z"
},
"policies": [
{
"policy": {
"policy-type": "sts",
"policy-string": ["version: STSv1", "mode: enforce", "mx: mail.example.com", "max_age: 604800"],
"policy-domain": "example.com"
},
"summary": {
"total-successful-session-count": 1542,
"total-failure-session-count": 0
}
}
]
}When total-failure-session-count is zero, your setup is clean. Any non-zero value warrants investigation. The failure-details array will tell you the exact failure type and the receiving MX hostname where it occurred. A dedicated mailbox for these reports is good practice: the volume is low (tens of messages per day for average traffic), but mixing them with operational mail is inconvenient.
Verification: full check sequence
# 1. Check the DNS TXT record dig TXT _mta-sts.example.com +short # 2. Fetch the policy file curl -sS https://mta-sts.example.com/.well-known/mta-sts.txt # 3. Verify the TLS certificate on the MX host openssl s_client -connect mail.example.com:25 -starttls smtp -servername mail.example.com < /dev/null 2>/dev/null | openssl x509 -noout -subject -dates # 4. Check the TLS-RPT record dig TXT _smtp._tls.example.com +short
All four should return results. The openssl step confirms that the MX server presents a valid TLS certificate with a hostname matching the mx entries in your policy. A mismatch here means senders in enforce mode will refuse to deliver.
Validation script
A bash script that checks all MTA-STS components in one run:
#!/bin/bash
DOMAIN=${1:?"Usage: $0 domain.com"}
echo "=== MTA-STS check for $DOMAIN ==="
echo ""
# DNS TXT record
echo "[1] _mta-sts.$DOMAIN TXT record:"
dig TXT "_mta-sts.$DOMAIN" +short
echo ""
# Policy file
POLICY_URL="https://mta-sts.$DOMAIN/.well-known/mta-sts.txt"
echo "[2] Policy file ($POLICY_URL):"
HTTP_CODE=$(curl -sS -o /tmp/mta-sts-policy.txt -w "%{http_code}" "$POLICY_URL")
if [ "$HTTP_CODE" = "200" ]; then
cat /tmp/mta-sts-policy.txt
else
echo "FAIL: HTTP $HTTP_CODE"
fi
echo ""
# TLS on MX hosts
echo "[3] MX records and TLS check:"
MX_HOSTS=$(dig MX "$DOMAIN" +short | awk '{print $2}' | sed 's/\.$//')
for MX in $MX_HOSTS; do
CERT_CN=$(openssl s_client -connect "$MX:25" -starttls smtp \
-servername "$MX" < /dev/null 2>/dev/null | \
openssl x509 -noout -subject 2>/dev/null)
if [ -n "$CERT_CN" ]; then
echo " $MX — TLS OK ($CERT_CN)"
else
echo " $MX — TLS FAIL"
fi
done
echo ""
# TLS-RPT
echo "[4] _smtp._tls.$DOMAIN TXT record:"
dig TXT "_smtp._tls.$DOMAIN" +shortchmod +x check-mta-sts.sh ./check-mta-sts.sh example.com
Common mistakes
- MX hostname doesn’t match the policy pattern. Your MX record points to
mx1.mailprovider.com, but the policy lists*.example.com. The server’s TLS certificate doesn’t coverexample.comeither. Result: mail stops in enforce mode. Fix: list the actual MX hostnames in themxfield, not your own domain. - Expired certificate on the mta-sts subdomain. The Let’s Encrypt certificate didn’t auto-renew. Senders can’t fetch the policy. Until the cached policy expires, senders use the old one. After
max_ageruns out, the protection disappears. Set up certificate monitoring. - Forgot to update the id in DNS. You changed the policy file but didn’t bump the
idin the TXT record. Senders don’t know the policy changed and keep using the cached version. - Going straight to enforce without testing first. You enable enforce mode before checking TLS-RPT reports. It turns out one MX server returns a certificate without the required SAN. Some mail gets blocked. Always start with
mode: testingand spend at least a week reading reports. - CNAME pointing to a host without HTTPS coverage for your subdomain. A CNAME to
example.github.iois fine. A CNAME to an IP address is not (CNAME cannot point to an IP). Confirm the target host serves a valid certificate formta-sts.example.com.
Sender-side enforcement: Postfix
MTA-STS protects incoming mail. But if you run your own mail server, you can also check MTA-STS when sending outbound. Postfix doesn’t support MTA-STS natively, but the postfix-mta-sts-resolver package (available on PyPI) fills that gap.
# Install
pip install postfix-mta-sts-resolver
# Configure /etc/mta-sts-daemon.yml
host: 127.0.0.1
port: 8461
cache:
type: sqlite
options:
filename: /var/cache/mta-sts/cache.db
# Add to /etc/postfix/main.cf
smtp_tls_policy_maps = socketmap:inet:127.0.0.1:8461:postfix
# Restart
systemctl restart mta-sts-daemon
systemctl reload postfixAfter this, Postfix queries the MTA-STS policy for each recipient domain and applies it on delivery. If the receiving domain publishes mode: enforce, Postfix will refuse to send without TLS.
MTA-STS alongside SPF, DKIM, and DMARC
MTA-STS solves one specific problem: protecting the transport channel. SPF, DKIM, and DMARC protect the message itself by verifying the sender and ensuring integrity. These are different layers and don’t replace each other.
A complete email security stack looks like this: SPF + DKIM + DMARC (authentication) + MTA-STS (transport encryption) + BIMI (visual identification). MTA-STS is the only one of these that protects against traffic interception between servers.
For a step-by-step walkthrough of the authentication side, see the SPF, DKIM, and DMARC setup guide. For BIMI, see the BIMI configuration guide.
Who should deploy MTA-STS
Any domain that receives email benefits from MTA-STS: it protects inbound messages from interception. Setup takes 20 to 40 minutes (policy file, DNS record, web server), and ongoing maintenance is minimal since certbot handles certificate renewal automatically.
For companies in finance, healthcare, and other regulated industries, MTA-STS is a compliance requirement, not an option. PCI DSS 4.0 explicitly requires encryption of email traffic when transmitting sensitive data.
Google Workspace and Microsoft 365 already publish MTA-STS for customer domains when enabled in the admin panel. If you use either platform, check your settings first: you may only need to add the DNS records.
Channel security is half the equation
MTA-STS ensures messages can’t be intercepted in transit. But if your list contains dead addresses, spam traps, and non-existent domains, deliverability will collapse long before anyone attempts a downgrade attack. A clean list is the foundation everything else rests on: authentication, encryption, reputation.
Before configuring MTA-STS, verify your list is in order. Upload your list to uChecker and see invalid addresses, risky contacts, and potential traps within minutes.
