MTA-STS and TLS-RPT Setup Guide: Enforce TLS for Inbound Email
SMTP was designed without encryption. STARTTLS added it later, but only as an option. If an attacker strips the STARTTLS offer from the server’s response, or presents a forged certificate, most sending servers fall back to plaintext delivery and never tell you. MTA-STS (RFC 8461) closes that gap. It lets your domain tell other mail servers: “Only deliver to my MX hosts over TLS with a valid certificate, and if you can’t, don’t deliver.” TLS-RPT (RFC 8460) is the reporting side. Sending servers send you daily reports on TLS failures they saw when delivering to you.
This guide covers the full setup: the two DNS records, the HTTPS-hosted policy file, a safe rollout from testing to enforce mode, and the mistakes that most often break it.
How MTA-STS Works
MTA-STS has three parts:
- A DNS TXT record at
_mta-sts.example.com. It says a policy exists and gives it an ID. - A policy file served over HTTPS at
https://mta-sts.example.com/.well-known/mta-sts.txt. It lists your allowed MX hosts and the enforcement mode. - Sender-side caching. A supporting sender (Gmail, Microsoft 365, Yahoo and others) fetches the policy, caches it for
max_ageseconds, and checks every later delivery against it. It only fetches the file again when theidin DNS changes.
The policy is fetched over HTTPS with a certificate checked against the Web PKI, so an attacker who can tamper with DNS or SMTP traffic can’t easily forge it. That is what makes MTA-STS stronger than opportunistic STARTTLS. You don’t need DNSSEC, unlike DANE.
Prerequisites
Check these before you publish anything:
- Every MX host supports STARTTLS.
- Every MX host presents a certificate that is valid, unexpired, issued by a public CA, and has a name that matches the MX hostname. A self-signed certificate, or one issued for a different name, will fail under enforce mode.
- You can host a small text file over HTTPS on the
mta-stssubdomain, with a valid certificate for that name.
To check an MX host’s certificate:
openssl s_client -connect mail.example.com:25 -starttls smtp -servername mail.example.com 2>/dev/null | openssl x509 -noout -subject -ext subjectAltName -enddate
The MX hostname has to appear in the Subject Alternative Name list.
Step 1: Publish the TLS-RPT Record First
Turn on reporting before anything else so you can see problems before they cost you mail. Add a TXT record:
_smtp._tls.example.com. 3600 IN TXT "v=TLSRPTv1; rua=mailto:tlsrpt@example.com"
You can list several destinations separated by commas, and https:// endpoints are allowed too. Reports arrive about once a day as gzip-compressed JSON. Each one shows successful and failed session counts per policy, plus failure types such as certificate-expired, certificate-host-mismatch, starttls-not-supported or validation-failure.
Use a dedicated mailbox or a report-processing service. Report volume grows with the number of sending domains.
Step 2: Create the Policy File
Start in testing mode:
version: STSv1
mode: testing
mx: mail.example.com
mx: backup-mx.example.com
max_age: 86400
Field reference:
version: alwaysSTSv1.mode:testingmeans senders report failures but still deliver.enforcemeans senders refuse to deliver over a connection that fails validation.nonewithdraws the policy.mx: one line per allowed MX pattern. A wildcard matches exactly one leftmost label.*.example.netmatchesmx1.example.net, but nota.b.example.netand notexample.netitself.max_age: how long senders cache the policy, in seconds. The maximum is 31557600 (about one year). Use a short value while testing. One to two weeks (604800 to 1209600) or longer is common once you enforce.
MX Patterns for Common Providers
# Google Workspace (classic MX set)
mx: aspmx.l.google.com
mx: *.aspmx.l.google.com
# Google Workspace (newer single MX)
mx: smtp.google.com
# Microsoft 365
mx: *.mail.protection.outlook.com
Every MX record you publish has to match at least one mx: line. Run dig +short MX example.com and compare the results line by line.
Step 3: Host the Policy File
The file has to be available at exactly:
https://mta-sts.example.com/.well-known/mta-sts.txt
Requirements:
- Serve it over HTTPS with a valid certificate for
mta-sts.example.com. - Return HTTP
200directly. Senders must not follow redirects, so a 301 towwwor to another host breaks the policy. - Use
Content-Type: text/plain. - CRLF line endings are what the RFC specifies. Most senders accept LF, but CRLF is the safe choice.
A static host works fine (a small VPS, GitHub Pages with a custom domain, Cloudflare Pages or Workers, an S3 bucket behind CloudFront). Here is a minimal nginx server block:
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 {
default_type text/plain;
root /var/www/mta-sts;
}
}
Step 4: Publish the MTA-STS DNS Record
_mta-sts.example.com. 3600 IN TXT "v=STSv1; id=20261015T120000"
The id can be any alphanumeric string up to 32 characters. A timestamp is the easiest choice. Change the id every time you edit the policy file. Senders only re-fetch the policy when the id changes. If you don’t change it, they keep using the cached copy until max_age runs out.
Step 5: Verify
dig +short TXT _mta-sts.example.com
dig +short TXT _smtp._tls.example.com
curl -sv https://mta-sts.example.com/.well-known/mta-sts.txt
In the curl output, check for HTTP 200 with no redirect, Content-Type: text/plain, a certificate valid for mta-sts.example.com, and a policy body that matches what you wrote.
Step 6: Move from Testing to Enforce
- Leave
mode: testingin place for at least one to two weeks and read the TLS-RPT reports. - Fix every failure you see: expired certificates, hostname mismatches, MX hosts missing from the policy.
- Change the file to
mode: enforceand raisemax_age, for example to1209600. - Update the
idin the_mta-stsTXT record. - Keep reading the reports. Under enforce mode, any failure they show is mail that was not delivered.
Common MTA-STS Errors and Fixes
Policy changes are ignored
The id wasn’t changed. Senders are still using the old cached policy. Change the id in DNS.
“Policy fetch failed” or no policy found
Possible causes: the mta-sts subdomain has no A/AAAA record, its certificate is missing or invalid, the server returns a redirect, or the path is wrong (it’s .well-known with a leading dot). Some CDNs and hosting panels redirect all traffic to www by default, so check for that.
certificate-host-mismatch in TLS-RPT
The MX host’s certificate doesn’t include the MX hostname. This often happens when the MX is a CNAME-style vanity name such as mail.example.com pointing at a shared server whose certificate only covers server123.hoster.net. Either get a certificate that includes the MX name, or point the MX straight at the provider’s hostname.
Mail rejected after an MX migration
You added a new MX record but didn’t add it to the policy. Under enforce mode, senders with a cached policy refuse the new host. When you migrate, add the new MX to the policy and update the id before changing your MX records, and only remove the old entry after the switch.
Expired certificate on the MX
With MTA-STS enforced, an expired MX certificate means compliant senders stop delivering. Monitor certificate expiry on port 25 the same way you monitor it for your website.
How to Remove MTA-STS Safely
Don’t just delete the records. Senders keep enforcing their cached policy until max_age expires. Instead, publish mode: none, update the id, and keep both in place for at least the previous max_age. After that, remove the DNS record and the policy file.
MTA-STS vs DANE
DANE (TLSA records) solves the same problem but relies on DNSSEC. Some senders, such as Microsoft 365 for outbound mail and many European providers, validate DANE. Gmail validates MTA-STS. The two don’t conflict, and if your domain is signed with DNSSEC, deploying both gives you the widest coverage. If it isn’t, MTA-STS is the practical place to start.