Most developers have heard of SPF, DKIM, and DMARC. Far fewer have heard of MTA-STS, and almost nobody has set up TLS Reporting. Which is a shame, because MTA-STS closes a real gap that the other three don't touch — and TLS Reporting is the only way to know when your email delivery is silently failing.
This guide covers both: what they do, how to set them up, and how to verify they're working.
The problem MTA-STS solves
When two mail servers exchange email, they're supposed to negotiate TLS so the message travels encrypted. But "supposed to" is doing a lot of work in that sentence.
SMTP's STARTTLS mechanism is opportunistic by default. That means:
- Sending server connects to your MX host
- Sending server says "do you support TLS?"
- If yes, they upgrade to TLS
- If no (or if an attacker strips the STARTTLS offer), they fall back to plaintext
Step 4 is the problem. An attacker who can intercept traffic between two mail servers — a network-level attacker, a rogue DNS resolver, anyone doing BGP hijacking — can silently strip the STARTTLS advertisement. The sending server sees "no TLS available" and delivers in plaintext. No error. No bounce. No indication anything went wrong.
This is called a STARTTLS downgrade attack, and it's been documented in the wild against real mail providers.
MTA-STS fixes this by publishing a policy that tells sending servers: "our MX hosts support TLS, you should require it, and here's a certificate you can validate against." A sending server that respects MTA-STS will refuse to deliver if TLS negotiation fails or if the certificate doesn't match, rather than falling back to plaintext.
The short version: SPF, DKIM, and DMARC protect against spoofing and phishing. MTA-STS protects the transport layer — the connection between mail servers — from being downgraded or intercepted.
How MTA-STS works
The mechanism involves two components: a policy file served over HTTPS, and a DNS TXT record that tells sending servers where to find it.
- Sending server looks up
_mta-sts.yourapp.comin DNS to check if a policy exists - If found, it fetches
https://mta-sts.yourapp.com/.well-known/mta-sts.txtover HTTPS - The policy tells it which MX hosts to expect and what mode to operate in
- It connects to your MX host and requires TLS with a valid, matching certificate
- If TLS fails or the cert doesn't match, it refuses delivery instead of falling back to plaintext
The policy file is served over HTTPS from a subdomain you control. The HTTPS requirement is what makes this work: the policy itself can't be tampered with in transit.
The policy file is served over HTTPS from a subdomain you control. The HTTPS requirement matters: it means the policy itself can't be tampered with in transit.
Setting up MTA-STS
Step 1 — Create the policy file
The policy file is a plain text file served at exactly this path:
https://mta-sts.yourapp.com/.well-known/mta-sts.txt
It looks like this:
version: STSv1
mode: enforce
mx: mail.yourapp.com
mx: *.yourapp.com
max_age: 86400
| Field | Description |
|---|---|
version |
Always STSv1
|
mode |
enforce, testing, or none (see below) |
mx |
One entry per MX host you accept mail on. Wildcards allowed. |
max_age |
How long (seconds) sending servers should cache this policy |
Mode choices explained:
| Mode | What happens when TLS fails |
|---|---|
testing |
Delivery proceeds, failure reported via TLS-RPT |
enforce |
Delivery refused if TLS fails or cert doesn't match |
none |
Policy effectively disabled |
Start with testing. Run it for a week or two while you monitor TLS-RPT reports (covered below). Once you're confident your MX hosts are properly configured, switch to enforce.
Don't start with
enforceif you haven't verified your MX hosts present valid, trusted certificates with matching hostnames. You will break inbound email delivery.
Step 2 — Serve the policy file over HTTPS
You need a subdomain mta-sts.yourapp.com with a valid TLS certificate serving the policy file. The response must:
- Return HTTP 200
- Have
Content-Type: text/plain - Be accessible over HTTPS (not HTTP)
If you're on Nginx:
server {
listen 443 ssl;
server_name mta-sts.yourapp.com;
ssl_certificate /etc/letsencrypt/live/mta-sts.yourapp.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/mta-sts.yourapp.com/privkey.pem;
location /.well-known/mta-sts.txt {
alias /var/www/mta-sts/mta-sts.txt;
default_type text/plain;
}
}
If you're on a static hosting platform (Netlify, Vercel, Cloudflare Pages), put the file at /.well-known/mta-sts.txt and set the content type to text/plain. Make sure HTTPS redirect is enabled.
Step 3 — Add the DNS TXT record
Add a TXT record at _mta-sts.yourapp.com:
_mta-sts.yourapp.com. IN TXT "v=STSv1; id=20260101000000Z"
The id field is how sending servers know when your policy has changed. It's a version string — you can use a timestamp or any string up to 32 characters. Every time you update your policy file, update the id in this DNS record. Sending servers that have cached your policy check the id to know whether to re-fetch.
Common mistake: Updating the policy file without updating the
idmeans sending servers keep using the cached version. If you're changing fromtestingtoenforce, the old cached policy stays in effect until themax_ageexpires.
Setting up TLS Reporting (TLS-RPT)
TLS Reporting is RFC 8460. It lets you receive reports when sending mail servers encounter TLS failures while trying to deliver to your domain.
You add one DNS TXT record at _smtp._tls.yourapp.com:
_smtp._tls.yourapp.com. IN TXT "v=TLSRPTv1; rua=mailto:tls-reports@yourapp.com"
That's it. Sending servers that support TLS-RPT will now send daily JSON reports to that address summarizing:
- How many messages were delivered successfully with TLS
- How many failed, and why
- Which sending servers encountered the failures
The reports are machine-readable JSON. You'll want either a dedicated inbox you check periodically or a service that parses and aggregates them. Report URI and Postmark's DMARC/TLS aggregator both handle TLS-RPT reports.
Sample report structure:
{
"organization-name": "Google Inc.",
"date-range": {
"start-datetime": "2026-01-01T00:00:00Z",
"end-datetime": "2026-01-01T23:59:59Z"
},
"policies": [{
"policy": {
"policy-type": "sts",
"policy-domain": "yourapp.com"
},
"summary": {
"total-successful-session-count": 1240,
"total-failure-session-count": 3
},
"failure-details": [{
"result-type": "certificate-expired",
"sending-mta-ip": "209.85.128.0",
"receiving-mx-hostname": "mail.yourapp.com",
"failed-session-count": 3
}]
}]
}
Three failures from certificate expiry. Without TLS-RPT, you'd never know.
Verifying your setup
Check the DNS record:
dig TXT _mta-sts.yourapp.com +short
dig TXT _smtp._tls.yourapp.com +short
Check the policy file is reachable:
curl -I https://mta-sts.yourapp.com/.well-known/mta-sts.txt
curl https://mta-sts.yourapp.com/.well-known/mta-sts.txt
Expected output from the first command: HTTP/2 200 with content-type: text/plain.
Full validation tools:
| Tool | What it checks |
|---|---|
| mta-sts.io | Full MTA-STS policy validation |
| internet.nl | Comprehensive mail security check including MTA-STS |
| checktls.com | TLS configuration on your MX hosts |
| MXToolbox | MTA-STS policy lookup |
Common failure modes
Policy file returns wrong content type
Some static hosts serve .txt files as application/octet-stream. Sending servers will reject the policy. Set Content-Type: text/plain explicitly in your hosting config.
Certificate on mta-sts subdomain doesn't cover the subdomain
If you're using a wildcard cert for *.yourapp.com, make sure it covers mta-sts.yourapp.com. A separate cert for just the subdomain also works.
MX host certificate doesn't match hostname
Your policy file lists mail.yourapp.com as an MX host. The certificate on that host needs to include mail.yourapp.com in the SAN. If it only has the bare domain or a different hostname, validation will fail under enforce mode.
id not updated after policy change
Covered above, but worth repeating: if you change the policy file without changing the id, sending servers use the stale cached version. The id is how they know to re-fetch.
HTTP redirect on the policy URL
The policy must be served directly over HTTPS. If your setup serves HTTP and redirects to HTTPS, some sending servers won't follow the redirect and will treat the policy as unavailable.
The rollout sequence
Here's the order that avoids breaking inbound email:
Week 1-2: deploy in testing mode
- Deploy the policy file with
mode: testing - Add both DNS records (
_mta-stsand_smtp._tls) - Verify with the validation tools listed above
Week 2-4: collect reports and fix issues
- Read the TLS-RPT reports coming into your inbox
- Fix any certificate or TLS config issues on your MX hosts
- Re-check with checktls.com
Week 4 onwards: switch to enforce
- Update the policy file: change
mode: testingtomode: enforce - Update the
idvalue in your_mta-stsDNS TXT record - Keep watching TLS-RPT reports for anything new
Don't rush the middle step. The reports will show you things you didn't know were broken.
Monitoring
Once MTA-STS is in enforce mode, any DNS misconfiguration on your end can break inbound email delivery. If your policy file goes down, if the mta-sts subdomain certificate expires, or if someone changes the DNS records unexpectedly, sending servers that respect MTA-STS will stop delivering to you.
Two things to monitor:
The HTTPS endpoint. Uptime monitoring on https://mta-sts.yourapp.com/.well-known/mta-sts.txt should alert you if the file becomes unavailable or returns a non-200 status.
The DNS records. If your _mta-sts TXT record is modified or deleted, sending servers can't find your policy. ZeroHook monitors DNS records continuously and alerts you when something changes — useful not just for MTA-STS but for the full set of email security records (SPF, DKIM, DMARC, MX). One change to the wrong record can quietly break email in ways that take days to diagnose: zerohook.org.
Quick reference
DNS records you need:
| Record | Type | Value |
|---|---|---|
_mta-sts.yourapp.com |
TXT | v=STSv1; id=20260101000000Z |
_smtp._tls.yourapp.com |
TXT | v=TLSRPTv1; rua=mailto:tls-reports@yourapp.com |
Policy file location:
https://mta-sts.yourapp.com/.well-known/mta-sts.txt
Policy file content:
version: STSv1
mode: testing
mx: mail.yourapp.com
max_age: 86400
Rollout order: deploy in testing → collect reports → fix issues → switch to enforce → monitor.
TL;DR
SPF, DKIM, and DMARC don't protect the connection between mail servers. A network-level attacker can strip TLS from SMTP and your email travels in plaintext with no indication anything went wrong.
MTA-STS fixes that by publishing a policy that says "require TLS, validate the cert, refuse delivery if it fails." TLS Reporting gives you visibility into when and why TLS is failing so you can fix it before you switch to enforce mode.
Setup is two DNS records and a text file on a subdomain. The tricky part is the rollout order — start in testing, read the reports, fix your cert issues, then enforce.
Part of an ongoing series on DNS and email security.
Top comments (0)