MTA-STS and TLS-RPT Guide: Enforcing Email Encryption in Transit
MTA-STS (Mail Transfer Agent Strict Transport Security) enforces TLS encryption for email in transit, preventing downgrade attacks. TLS-RPT provides visibility into TLS failures. Together, they secure email communication beyond what STARTTLS alone provides.
Why MTA-STS Matters
┌─────────────────────────────────────────────────────────────────────────────┐
│ THE PROBLEM: STARTTLS DOWNGRADE ATTACKS │
├─────────────────────────────────────────────────────────────────────────────┤
│ │
│ WITHOUT MTA-STS (Opportunistic TLS): │
│ │
│ ┌─────────────┐ STARTTLS? ┌──────────────┐ TLS OK ┌─────────┐ │
│ │ Sender │───────────────▶│ MITM │─────────────▶│ Your MX │ │
│ │ Server │ │ Attacker │ │ Server │ │
│ └─────────────┘ └──────────────┘ └─────────┘ │
│ │ │
│ │ Strips "250 STARTTLS" │
│ │ from server response │
│ ▼ │
│ ┌──────────────┐ │
│ │ Sender sees │ │
│ │ "TLS not │ │
│ │ supported" │ │
│ │ │ │
│ │ Email sent │ │
│ │ in PLAINTEXT │ │
│ └──────────────┘ │
│ │
│ ⚠️ Attacker reads and/or modifies email content! │
│ │
├─────────────────────────────────────────────────────────────────────────────┤
│ THE SOLUTION: MTA-STS (Enforced TLS) │
├─────────────────────────────────────────────────────────────────────────────┤
│ │
│ WITH MTA-STS: │
│ │
│ ┌─────────────┐ 1. Fetch policy ┌──────────────────────────────────┐ │
│ │ Sender │───────────────────▶│ https://mta-sts.example.com/ │ │
│ │ Server │ │ .well-known/mta-sts.txt │ │
│ └─────────────┘ │ │ │
│ │ │ version: STSv1 │ │
│ │ │ mode: enforce │ │
│ │ │ mx: mail.example.com │ │
│ │ │ max_age: 604800 │ │
│ │ └──────────────────────────────────┘ │
│ │ │
│ │ 2. Require TLS + valid cert │
│ │ matching policy │
│ ▼ │
│ ┌─────────────┐ TLS REQUIRED ┌──────────────┐ │
│ │ Sender │─────────────────────│ MITM │ │
│ │ Server │ CAN'T STRIP │ Attacker │ │
│ └─────────────┘─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ▶│ BLOCKED! │ │
│ │ └──────────────┘ │
│ │ │
│ │ 3. TLS connection directly │
│ │ to legitimate MX │
│ ▼ │
│ ┌─────────────┐ ENCRYPTED ┌─────────────┐ │
│ │ Sender │════════════════════▶│ Your MX │ │
│ │ Server │ TLS 1.3 │ Server │ │
│ └─────────────┘ └─────────────┘ │
│ │
│ ✅ Email encrypted, attacker cannot intercept! │
│ │
└─────────────────────────────────────────────────────────────────────────────┘
MTA-STS Components
┌─────────────────────────────────────────────────────────────────────────────┐
│ MTA-STS ARCHITECTURE │
├─────────────────────────────────────────────────────────────────────────────┤
│ │
│ COMPONENT 1: DNS TXT Record │
│ ┌───────────────────────────────────────────────────────────────────────┐ │
│ │ _mta-sts.example.com. IN TXT "v=STSv1; id=20250108120000" │ │
│ └───────────────────────────────────────────────────────────────────────┘ │
│ Purpose: Signals MTA-STS is enabled, ID changes trigger policy refresh │
│ │
│ COMPONENT 2: HTTPS Policy File │
│ ┌───────────────────────────────────────────────────────────────────────┐ │
│ │ URL: https://mta-sts.example.com/.well-known/mta-sts.txt │ │
│ │ │ │
│ │ Contents: │ │
│ │ version: STSv1 │ │
│ │ mode: enforce │ │
│ │ mx: mail.example.com │ │
│ │ mx: mail2.example.com │ │
│ │ max_age: 604800 │ │
│ └───────────────────────────────────────────────────────────────────────┘ │
│ Purpose: Defines allowed MX hosts and policy strictness │
│ │
│ COMPONENT 3: TLS-RPT DNS Record (Optional but Recommended) │
│ ┌───────────────────────────────────────────────────────────────────────┐ │
│ │ _smtp._tls.example.com. IN TXT "v=TLSRPTv1; rua=mailto:[email protected]"│ │
│ └───────────────────────────────────────────────────────────────────────┘ │
│ Purpose: Receive reports about TLS connection successes and failures │
│ │
└─────────────────────────────────────────────────────────────────────────────┘
Step-by-Step Implementation
Step 1: Verify Prerequisites
Before implementing MTA-STS:
┌─────────────────────────────────────────────────────────────────────────────┐
│ PREREQUISITES CHECKLIST │
├─────────────────────────────────────────────────────────────────────────────┤
│ │
│ ☐ MX servers have valid TLS certificates │
│ • Certificate matches MX hostname exactly │
│ • Certificate is not expired │
│ • Certificate is issued by trusted CA (not self-signed) │
│ • Certificate chain is complete │
│ │
│ ☐ MX servers support TLS 1.2 or higher │
│ • TLS 1.0 and 1.1 are deprecated │
│ • TLS 1.3 recommended if possible │
│ │
│ ☐ Ability to host HTTPS content at mta-sts.yourdomain.com │
│ • Valid TLS certificate for mta-sts subdomain │
│ • Web server, CDN, or static hosting available │
│ │
│ ☐ DNS management access │
│ • Can add TXT records │
│ • Can add A/CNAME records for mta-sts subdomain │
│ │
└─────────────────────────────────────────────────────────────────────────────┘
Verify MX TLS Certificate:
# Check TLS certificate on MX server
openssl s_client -connect mail.example.com:25 -starttls smtp < /dev/null 2>/dev/null | \
openssl x509 -noout -dates -subject
# Expected output:
# notBefore=Jan 1 00:00:00 2024 GMT
# notAfter=Dec 31 23:59:59 2025 GMT
# subject=CN = mail.example.com
Step 2: Create the Policy File
Create mta-sts.txt with your policy:
version: STSv1
mode: testing
mx: mail.example.com
mx: mail2.example.com
max_age: 86400
Policy Fields:
| Field | Required | Description |
|---|---|---|
version | Yes | Always STSv1 |
mode | Yes | none, testing, or enforce |
mx | Yes | MX hostnames (one per line, can use wildcards) |
max_age | Yes | Policy cache time in seconds (max: 31557600) |
Mode Options:
| Mode | Behavior |
|---|---|
none | MTA-STS disabled, ignore policy |
testing | Log failures, deliver anyway |
enforce | Reject delivery on TLS failure |
Example Policies:
# Single MX server
version: STSv1
mode: enforce
mx: mail.example.com
max_age: 604800
# Multiple MX servers
version: STSv1
mode: enforce
mx: mail1.example.com
mx: mail2.example.com
mx: mail3.example.com
max_age: 604800
# Wildcard MX (Google Workspace, Microsoft 365)
version: STSv1
mode: enforce
mx: *.mail.google.com
mx: *.outlook.com
max_age: 604800
# Google Workspace specific
version: STSv1
mode: enforce
mx: aspmx.l.google.com
mx: *.googlemail.com
max_age: 604800
Step 3: Host the Policy File
The policy must be served at:
https://mta-sts.yourdomain.com/.well-known/mta-sts.txt
Option A: Cloudflare Pages
- Create a repository with:
.well-known/
mta-sts.txt
- Connect to Cloudflare Pages
- Add custom domain:
mta-sts.yourdomain.com
Option B: AWS S3 + CloudFront
# Create S3 bucket
aws s3 mb s3://mta-sts-example-com
# Upload policy file
aws s3 cp mta-sts.txt s3://mta-sts-example-com/.well-known/mta-sts.txt \
--content-type "text/plain"
# Configure CloudFront with custom domain and ACM certificate
Option C: Nginx
server {
listen 443 ssl http2;
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;
}
}
Option D: GitHub Pages
- Create repository
mta-sts - Add file
.well-known/mta-sts.txt - Enable GitHub Pages
- Configure custom domain
mta-sts.yourdomain.com - Add DNS CNAME:
mta-sts.yourdomain.com → youruser.github.io
Step 4: Add DNS Records
MTA-STS TXT Record:
_mta-sts.example.com. IN TXT "v=STSv1; id=20250108120000"
The id field should be a unique identifier that changes when the policy changes (timestamp works well).
TLS-RPT TXT Record:
_smtp._tls.example.com. IN TXT "v=TLSRPTv1; rua=mailto:[email protected]"
Or send reports to a web endpoint:
_smtp._tls.example.com. IN TXT "v=TLSRPTv1; rua=https://report.example.com/tlsrpt"
MTA-STS Subdomain (A or CNAME):
; If hosting on same server
mta-sts.example.com. IN A 203.0.113.10
; If using CDN/external hosting
mta-sts.example.com. IN CNAME yourcdn.example.net.
Step 5: Verify Configuration
Check DNS Records:
# Check MTA-STS TXT record
dig +short TXT _mta-sts.example.com
# Expected: "v=STSv1; id=20250108120000"
# Check TLS-RPT record
dig +short TXT _smtp._tls.example.com
# Expected: "v=TLSRPTv1; rua=mailto:[email protected]"
Verify Policy File:
# Fetch and verify policy
curl -sS https://mta-sts.example.com/.well-known/mta-sts.txt
# Expected output:
# version: STSv1
# mode: testing
# mx: mail.example.com
# max_age: 86400
Online Validators:
TLS-RPT Reports
Report Structure
TLS-RPT reports are JSON files containing TLS connection information:
{
"organization-name": "Google LLC",
"date-range": {
"start-datetime": "2025-01-07T00:00:00Z",
"end-datetime": "2025-01-08T00:00:00Z"
},
"contact-info": "[email protected]",
"report-id": "2025-01-08T00:00:00Z_example.com",
"policies": [
{
"policy": {
"policy-type": "sts",
"policy-string": [
"version: STSv1",
"mode: enforce",
"mx: mail.example.com",
"max_age: 604800"
],
"policy-domain": "example.com",
"mx-host": "mail.example.com"
},
"summary": {
"total-successful-session-count": 1547,
"total-failure-session-count": 3
},
"failure-details": [
{
"result-type": "certificate-expired",
"sending-mta-ip": "209.85.220.41",
"receiving-mx-hostname": "mail.example.com",
"receiving-ip": "203.0.113.10",
"failed-session-count": 2,
"additional-information": "Certificate expired 2025-01-05"
},
{
"result-type": "starttls-not-supported",
"sending-mta-ip": "209.85.220.42",
"receiving-mx-hostname": "mail.example.com",
"receiving-ip": "203.0.113.11",
"failed-session-count": 1
}
]
}
]
}
Failure Types
| Result Type | Description | Action |
|---|---|---|
certificate-expired | MX certificate expired | Renew certificate immediately |
certificate-not-trusted | Unknown CA or self-signed | Use trusted CA certificate |
certificate-host-mismatch | Cert doesn't match hostname | Reissue cert with correct hostname |
starttls-not-supported | Server doesn't support TLS | Enable STARTTLS on mail server |
validation-failure | General validation error | Check certificate chain |
sts-policy-invalid | Malformed policy file | Fix policy file syntax |
sts-webpki-invalid | Policy host certificate issue | Fix mta-sts subdomain certificate |
dns-error | DNS lookup failed | Check DNS configuration |
Processing Reports
Simple Email Processing:
#!/usr/bin/env python3
"""Parse TLS-RPT reports from email"""
import json
import gzip
import email
import sys
from email import policy
def parse_tlsrpt(msg_file):
with open(msg_file, 'rb') as f:
msg = email.message_from_binary_file(f, policy=policy.default)
for part in msg.walk():
content_type = part.get_content_type()
if content_type in ['application/json', 'application/tlsrpt+json',
'application/tlsrpt+gzip']:
payload = part.get_payload(decode=True)
# Handle gzip compression
if content_type.endswith('+gzip') or payload[:2] == b'\x1f\x8b':
payload = gzip.decompress(payload)
report = json.loads(payload)
analyze_report(report)
def analyze_report(report):
print(f"Report from: {report.get('organization-name', 'Unknown')}")
print(f"Date range: {report['date-range']['start-datetime']} - "
f"{report['date-range']['end-datetime']}")
for policy in report.get('policies', []):
domain = policy['policy'].get('policy-domain', 'Unknown')
summary = policy.get('summary', {})
success = summary.get('total-successful-session-count', 0)
failure = summary.get('total-failure-session-count', 0)
print(f"\nDomain: {domain}")
print(f" Success: {success}, Failures: {failure}")
for failure in policy.get('failure-details', []):
print(f" - {failure['result-type']}: {failure['failed-session-count']} sessions")
print(f" MX: {failure.get('receiving-mx-hostname', 'Unknown')}")
if __name__ == '__main__':
parse_tlsrpt(sys.argv[1])
Using Third-Party Services:
Several services aggregate and visualize TLS-RPT reports:
- Postmark - Free TLS-RPT processing
- Report URI - TLS-RPT + DMARC aggregation
- EasyDMARC - Full email security monitoring
- URIports - TLS reporting service
Deployment Strategy
┌─────────────────────────────────────────────────────────────────────────────┐
│ MTA-STS DEPLOYMENT TIMELINE │
├─────────────────────────────────────────────────────────────────────────────┤
│ │
│ WEEK 1: Setup │
│ ┌───────────────────────────────────────────────────────────────────────┐ │
│ │ Day 1-2: Verify MX TLS certificates are valid │ │
│ │ Day 3-4: Create and host policy file (mode: testing) │ │
│ │ Day 5-7: Add DNS records, verify with online tools │ │
│ │ │ │
│ │ Policy: mode: testing, max_age: 86400 (1 day) │ │
│ └───────────────────────────────────────────────────────────────────────┘ │
│ │
│ WEEKS 2-4: Monitoring │
│ ┌───────────────────────────────────────────────────────────────────────┐ │
│ │ • Monitor TLS-RPT reports daily │ │
│ │ • Investigate any failure-details │ │
│ │ • Fix certificate or configuration issues found │ │
│ │ • Verify all MX servers are working correctly │ │
│ │ │ │
│ │ Policy: mode: testing, max_age: 86400 (1 day) │ │
│ └───────────────────────────────────────────────────────────────────────┘ │
│ │
│ WEEK 5: Enforcement Preparation │
│ ┌───────────────────────────────────────────────────────────────────────┐ │
│ │ Day 1: Review all TLS-RPT reports - ensure near-zero failures │ │
│ │ Day 2: Update policy to mode: enforce │ │
│ │ Day 3-7: Monitor closely for delivery issues │ │
│ │ │ │
│ │ Policy: mode: enforce, max_age: 86400 (1 day) │ │
│ └───────────────────────────────────────────────────────────────────────┘ │
│ │
│ WEEKS 6+: Production │
│ ┌───────────────────────────────────────────────────────────────────────┐ │
│ │ • Increase max_age to 604800 (1 week) or 1209600 (2 weeks) │ │
│ │ • Continue monitoring TLS-RPT reports weekly │ │
│ │ • Set up automated alerting for failures │ │
│ │ │ │
│ │ Policy: mode: enforce, max_age: 604800 (1 week) │ │
│ └───────────────────────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────────────────┘
Platform-Specific Configuration
Google Workspace
Google Workspace domains can use MTA-STS with Google's MX servers:
version: STSv1
mode: enforce
mx: aspmx.l.google.com
mx: alt1.aspmx.l.google.com
mx: alt2.aspmx.l.google.com
mx: alt3.aspmx.l.google.com
mx: alt4.aspmx.l.google.com
mx: *.aspmx.l.google.com
max_age: 604800
Or use wildcard:
version: STSv1
mode: enforce
mx: *.google.com
mx: *.googlemail.com
max_age: 604800
Microsoft 365
version: STSv1
mode: enforce
mx: *.mail.protection.outlook.com
max_age: 604800
Proofpoint
version: STSv1
mode: enforce
mx: *.pphosted.com
max_age: 604800
Self-Hosted
version: STSv1
mode: enforce
mx: mail.example.com
mx: backup-mail.example.com
max_age: 604800
Troubleshooting
Common Issues
| Issue | Symptom | Solution |
|---|---|---|
| Policy not found | "No MTA-STS policy" | Verify HTTPS URL works, check DNS |
| Certificate mismatch | TLS-RPT shows certificate-host-mismatch | MX cert must match MX hostname |
| Policy fetch fails | sts-webpki-invalid | Fix mta-sts subdomain certificate |
| Invalid policy syntax | sts-policy-invalid | Check policy file format (no extra spaces) |
| DNS issues | Policy ID mismatch | Update DNS TXT record ID when policy changes |
Debug Commands
# Test full MTA-STS flow
# 1. Check DNS TXT record
dig +short TXT _mta-sts.example.com
# 2. Verify policy is reachable
curl -v https://mta-sts.example.com/.well-known/mta-sts.txt
# 3. Check MX server TLS
openssl s_client -connect mail.example.com:25 -starttls smtp < /dev/null 2>/dev/null | \
openssl x509 -noout -subject -dates -issuer
# 4. Verify certificate matches MX hostname
echo | openssl s_client -connect mail.example.com:25 -starttls smtp 2>/dev/null | \
openssl x509 -noout -text | grep -A1 "Subject Alternative Name"
# 5. Check TLS-RPT record
dig +short TXT _smtp._tls.example.com
Policy Update Procedure
When updating your policy:
- Update the policy file at mta-sts.yourdomain.com
- Update DNS TXT record ID to force cache refresh:
_mta-sts.example.com. IN TXT "v=STSv1; id=20250108150000"
- Wait for propagation (DNS TTL + sender cache)
Best Practices
Security Recommendations
- Start with testing mode - Monitor before enforcing
- Keep certificates updated - Automated renewal recommended
- Use short max_age initially - 86400 seconds for flexibility
- Monitor TLS-RPT reports - Set up automated alerting
- Document all MX servers - Ensure all are in policy
- Test certificate changes - Before renewal/replacement
Policy File Best Practices
# DO: Use exact hostnames when possible
mx: mail.example.com
mx: backup.example.com
# DO: Include all MX servers
mx: mx1.example.com
mx: mx2.example.com
mx: mx3.example.com
# CAUTION: Wildcards can be too permissive
mx: *.example.com
# DON'T: Forget backup MX servers
# (will cause mail rejection if primary fails)
# DON'T: Set max_age too high initially
# (makes fixing issues slow)
Automated Certificate Monitoring
#!/bin/bash
# Check MX certificate expiration
DOMAIN="example.com"
MX_SERVERS=$(dig +short MX $DOMAIN | awk '{print $2}')
for mx in $MX_SERVERS; do
expiry=$(echo | openssl s_client -connect ${mx}:25 -starttls smtp 2>/dev/null | \
openssl x509 -noout -enddate 2>/dev/null | cut -d= -f2)
expiry_epoch=$(date -d "$expiry" +%s)
now_epoch=$(date +%s)
days_left=$(( ($expiry_epoch - $now_epoch) / 86400 ))
echo "$mx: $days_left days until expiration"
if [ $days_left -lt 30 ]; then
echo "WARNING: Certificate for $mx expires in $days_left days!"
fi
done
Related Resources
- Email Authentication Complete Guide - Hub article
- TLS Configuration Hardening - TLS best practices
- SSL vs TLS Difference - Protocol basics
- Email Delivery Troubleshooting - Fix delivery issues
Tools
- DNS Lookup - Verify MTA-STS DNS records
- SSL Certificate Checker - Verify MX certificates
- MX Record Checker - Verify mail routing