uCheckeruChecker
12 min read

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:

  1. Sender resolves MX records for the recipient domain.
  2. Sender queries _mta-sts.example.com for a TXT record.
  3. If the record exists, sender fetches https://mta-sts.example.com/.well-known/mta-sts.txt.
  4. Sender caches the policy for the duration specified in max_age.
  5. On every subsequent delivery, sender enforces the policy: require valid TLS, verify the MX hostname against the mx patterns 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: STSv1 is the only valid version right now.
  • mode has three options: testing, enforce, and none. Start with testing to collect reports without blocking mail. Move to enforce once you are confident everything is working.
  • mx lists MX hostnames the policy allows, one per line. Wildcards work: *.example.com matches mail.example.com. These must match the Common Name or SAN in the server’s TLS certificate.
  • max_age is the cache lifetime in seconds. 86400 is one day, a sensible starting value. After switching to enforce, 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" +short
chmod +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 cover example.com either. Result: mail stops in enforce mode. Fix: list the actual MX hostnames in the mx field, 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_age runs 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 id in 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: testing and spend at least a week reading reports.
  • CNAME pointing to a host without HTTPS coverage for your subdomain. A CNAME to example.github.io is fine. A CNAME to an IP address is not (CNAME cannot point to an IP). Confirm the target host serves a valid certificate for mta-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 postfix

After 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.

MTA-STS setupemail TLS downgrade attackTLS-RPT reportsSMTP TLS encryptionRFC 8461