MTA-STS and TLS-RPT in 2026: complete implementation guide with Postfix self-hosted setup, DANE coexistence, and the testing-to-enforce migration that does not break mail
The honest self-hosted MTA-STS and TLS-RPT implementation: STARTTLS downgrade attack diagram, real adoption data 2020-2026, interactive decision tool for mode selection, complete Postfix self-hosted code, DANE coexistence (BSI TR-03108), self-hosted TLS-RPT JSON parsing in Python, common failure modes, day-by-day migration runbook, and when MTA-STS is NOT the right answer.
MTA-STS and TLS-RPT solve a specific problem: SMTP transport encryption between mail servers is optional by default, vulnerable to downgrade attacks, and historically invisible to domain owners when it fails. Microsoft, Google and Yahoo all support both protocols as senders, and increasing numbers of mail receivers (including Exchange Online via the recent rollout) enforce TLS connections for inbound mail.
Most implementation guides published in 2025-2026 assume you will use a managed SaaS — RedSift OnDMARC, PowerDMARC, Mailhardener, Mimecast — that hosts the MTA-STS policy file for you and parses TLS-RPT reports through their dashboard. That works, and for many operators it is the right choice. But the SaaS path obscures what is actually happening, costs $50-500/month, and creates a vendor dependency for what is fundamentally a static text file plus a JSON-parsing pipeline.
This post is the self-hosted implementation. How to publish the MTA-STS policy on your own infrastructure (no SaaS), how to coexist with DANE on Postfix when you have it, the testing-to-enforce migration that does not break inbound mail when something is misconfigured, and the JSON report parsing that makes TLS-RPT useful instead of decorative. Written for operators who want to understand the full stack and own the implementation without recurring SaaS cost.
The real problem — why MTA-STS exists
STARTTLS in SMTP is opportunistic by default: when an MTA initiates a connection to deliver mail, the receiving server announces TLS support via 250-STARTTLS, and the sender upgrades to encrypted transport. The vulnerability: an active MITM attacker can strip the 250-STARTTLS line from the EHLO response, forcing the sender to fall back to plaintext, and the sender has no way to know an attack is happening. The following diagram shows the attack and the protection MTA-STS adds:
Three critical observations from the diagram. First: the downgrade attack is invisible to the sender without MTA-STS. There are no alerts, no concerning logs, the sender simply concludes “this receiver does not support TLS” and delivers in plaintext. Operators who assume “STARTTLS is active, I am protected” are structurally wrong because they have no visibility on whether STARTTLS was stripped in transit. Second: MTA-STS combines two independent channels to defend against the attack. The policy is published via DNS TXT (channel 1) and via HTTPS GET to the dedicated subdomain (channel 2). For the attacker to bypass MTA-STS they would need to compromise both channels simultaneously, which dramatically raises the technical bar of the attack. Third: TLS-RPT provides post-facto visibility when something fails. Without TLS-RPT, MTA-STS in enforce silently blocks mail when there are configuration errors, and the operator discovers problems via end-user complaints. With TLS-RPT, the operator receives daily JSON reports that diagnose exactly what is failing.
Real adoption 2026 — what verifiable data shows
The narrative of “everyone is adopting MTA-STS” does not hold up against the real data. Hardenize, Cloudflare Radar and Mailhardener publish quarterly monitoring data showing more modest adoption than managed vendors would suggest:
| Categoría | SPF (%) | DKIM (%) | DMARC any policy (%) | MTA-STS testing (%) | MTA-STS enforce (%) | TLS-RPT publication (%) |
|---|---|---|---|---|---|---|
| 2020 Q4 | 65 | 45 | 35 | 6 | 3 | 4 |
| 2021 Q4 | 68 | 48 | 38 | 7 | 5 | 5 |
| 2022 Q4 | 71 | 52 | 42 | 9 | 8 | 7 |
| 2023 Q4 | 73 | 55 | 45 | 10 | 12 | 9 |
| 2024 Q4 | 75 | 58 | 47 | 12 | 16 | 11 |
| 2025 Q4 | 77 | 60 | 49 | 13 | 19 | 12 |
| 2026 Q1 | 78 | 62 | 51 | 14 | 21 | 13 |
Three critical data observations. First, MTA-STS in enforce is growing faster than SPF/DKIM/DMARC (21% in 2026 vs 3% in 2020 = 7x growth) but from a much lower base, which means operators adopting now are ahead of the curve. Second, the gap between testing and enforce is closing — historically there were many more domains in testing than in enforce, but operators are moving to enforce as TLS-RPT improves operational confidence. Third, TLS-RPT publication trails MTA-STS (13% vs 21%), which means many operators publish MTA-STS policy without report visibility — an operational antipattern we cover in the common errors section.
Interactive decision tool — which MTA-STS mode applies to your situation
MTA-STS and TLS-RPT solve a specific problem: SMTP transport encryption between mail servers is optional by default, vulnerable to downgrade attacks, and historically invisible to domain owners when it fails. Microsoft, Google and Yahoo all support both protocols as senders, and increasing numbers of mail receivers (including Exchange Online via the recent rollout) enforce TLS connections for inbound mail.
Most implementation guides published in 2025-2026 assume you will use a managed SaaS — RedSift OnDMARC, PowerDMARC, Mailhardener, Mimecast — that hosts the MTA-STS policy file for you and parses TLS-RPT reports through their dashboard. That works, and for many operators it is the right choice. But the SaaS path obscures what is actually happening, costs $50-500/month, and creates a vendor dependency for what is fundamentally a static text file plus a JSON-parsing pipeline.
This post is the self-hosted implementation. How to publish the MTA-STS policy on your own infrastructure (no SaaS), how to coexist with DANE on Postfix when you have it, the testing-to-enforce migration that does not break inbound mail when something is misconfigured, and the JSON report parsing that makes TLS-RPT useful instead of decorative. Written for operators who want to understand the full stack and own the implementation without recurring SaaS cost.
What MTA-STS and TLS-RPT actually do
The two protocols are complementary and both target the same threat surface: SMTP transport security between mail servers.
MTA-STS (RFC 8461): a domain publishes an HTTPS-served policy file declaring which mail servers receive its mail and that TLS encryption is mandatory. Sending MTAs that support MTA-STS fetch this policy and refuse to deliver mail if their connection cannot be encrypted with a valid certificate.
TLS-RPT (RFC 8460): a domain publishes a DNS TXT record declaring an email address (or HTTPS endpoint) where sending MTAs should send daily JSON reports about TLS connection successes and failures. This gives the receiving domain visibility into who is failing to connect securely and why.
The two protocols address different parts of the security gap that SPF/DKIM/DMARC do not cover:
- SPF/DKIM/DMARC: prove sender identity, prevent spoofing of From: header
- MTA-STS: enforce that mail to your domain arrives over encrypted, authenticated transport
- TLS-RPT: tell you when MTA-STS or general TLS encryption is failing for senders
A domain with full DMARC enforcement and no MTA-STS proves identity but transmits content over potentially-unencrypted connections that anyone in the network path can read. The two protocol families complement, not compete.
What you publish — the three components
The complete implementation requires three components on your DNS and infrastructure.
Component 1 — DNS TXT record at _mta-sts.yourdomain.com
This advertises that MTA-STS is enabled and provides a policy ID:
_mta-sts.yourdomain.com. IN TXT "v=STSv1; id=20260425120000Z"
The id value must change every time you update the policy file. Senders cache the policy keyed by this ID; without an ID change, they keep using the old cached policy. Convention: ISO 8601 timestamp of the policy change.
Component 2 — HTTPS-served policy file at https://mta-sts.yourdomain.com/.well-known/mta-sts.txt
This is the policy file proper:
version: STSv1
mode: testing
mx: mx1.yourdomain.com
mx: mx2.yourdomain.com
max_age: 604800
The web server hosting this file:
- Must serve over HTTPS (HTTP is rejected by sending MTAs)
- Must present a valid TLS certificate for
mta-sts.yourdomain.com(self-signed certificates are rejected) - Must serve the file at exactly
/.well-known/mta-sts.txt
The mode value progresses from testing to enforce as you gain confidence in your TLS configuration. We cover the migration sequence below.
The mx lines must include all MX hosts that can receive mail for the domain. Wildcards (*.yourdomain.com) are supported. Missing an MX host in the policy means MTA-STS-aware senders will refuse to deliver to that host once mode is enforce.
The max_age value is the cache duration in seconds. 604800 (7 days) is typical for production; 86400 (1 day) is appropriate during testing or migration when you want changes to propagate quickly.
Component 3 — DNS TXT record at _smtp._tls.yourdomain.com for TLS-RPT
This declares where TLS reports should be sent:
_smtp._tls.yourdomain.com. IN TXT "v=TLSRPTv1; rua=mailto:[email protected]"
The rua (reporting URI for aggregate reports) value can be:
mailto:— reports arrive as email attachments (most common)https:— reports POSTed to an HTTPS endpoint
Use a dedicated mailbox for TLS reports. The volume is significant for active domains (one report per sender MTA per day) and you do not want it cluttering operational mailboxes.
Self-hosted policy file — the simple Nginx pattern
The MTA-STS policy file is a static text file served over HTTPS. You do not need a SaaS for this; any web server with TLS capability handles it. The simplest production-quality pattern with Nginx:
server {
listen 443 ssl http2;
server_name mta-sts.yourdomain.com;
ssl_certificate /etc/letsencrypt/live/mta-sts.yourdomain.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/mta-sts.yourdomain.com/privkey.pem;
# MTA-STS-specific hardening
ssl_protocols TLSv1.2 TLSv1.3;
ssl_ciphers HIGH:!aNULL:!MD5;
location = /.well-known/mta-sts.txt {
alias /var/www/mta-sts/mta-sts.txt;
default_type text/plain;
add_header Cache-Control "max-age=86400";
}
# Redirect everything else
location / {
return 404;
}
}
# HTTPS redirect (optional but recommended)
server {
listen 80;
server_name mta-sts.yourdomain.com;
return 301 https://$host$request_uri;
}
The certificate comes from Let’s Encrypt via certbot — no special handling needed, the mta-sts.yourdomain.com subdomain is a regular DNS host that certbot can validate. Cost: $0 for the certificate, $0 for Nginx, $0 for the DNS records. The only ongoing cost is server hosting (which you already have if you run any web property), and certbot cert renewal cron (which you already have if you use Let’s Encrypt).
The static file /var/www/mta-sts/mta-sts.txt contains your policy:
version: STSv1
mode: testing
mx: mx1.yourdomain.com
mx: mx2.yourdomain.com
max_age: 604800
When you change the policy, update both the file and the DNS TXT record’s id value. We use a simple shell script for this:
#!/bin/bash
# update-mta-sts.sh
NEW_ID=$(date -u +%Y%m%d%H%M%SZ)
echo "Updating policy ID to $NEW_ID"
# Update DNS via your provider's API (example for Cloudflare)
curl -X PUT "https://api.cloudflare.com/client/v4/zones/$ZONE_ID/dns_records/$RECORD_ID" \
-H "Authorization: Bearer $CF_TOKEN" \
-H "Content-Type: application/json" \
--data "{\"type\":\"TXT\",\"name\":\"_mta-sts\",\"content\":\"v=STSv1; id=$NEW_ID\"}"
echo "Done. Allow up to TTL minutes for DNS propagation."
The testing-to-enforce migration that does not break mail
The MTA-STS specification is intentional about progression: testing mode reports failures without blocking mail, enforce mode actually enforces TLS. Jumping directly to enforce without testing is the most common cause of MTA-STS-induced mail loss.
The recommended migration sequence:
Phase 1 — DNS records and policy in none or testing mode (Day 0)
Publish the DNS TXT record and policy file with mode: testing. Set max_age: 86400 (24 hours) so changes propagate quickly.
Set up TLS-RPT reception. The aggregator receiving reports can be:
- A dedicated mailbox you check manually (smallest scale)
- A commercial parser (Mailhardener, RedSift) for managed parsing
- A self-hosted parser (we cover the pattern below)
Verify the policy is reachable:
curl https://mta-sts.yourdomain.com/.well-known/mta-sts.txt
dig TXT _mta-sts.yourdomain.com +short
dig TXT _smtp._tls.yourdomain.com +short
Phase 2 — Monitor TLS reports (Days 1-30)
Daily TLS-RPT reports arrive from sender MTAs. Review for:
- MX hosts in your policy that report failures → indicates MX certificate or TLS configuration issue
- High volume of failures from specific senders → indicates something specific to that sender’s TLS implementation
- Successful delivery counts → confirms the protocol is working for compliant senders
Common issues caught in this phase:
- Self-signed certificates on backup MX hosts
- Certificate hostname mismatches (cert is for
mail.example.combut MX record points tomx.example.com) - TLS versions below 1.2 still in use somewhere
- Cipher suite incompatibilities with stricter senders
The 30-day monitoring period is the protective layer. Issues identified here would have caused mail loss in enforce mode but only generate reports in testing mode.
Phase 3 — Increase max_age, observe (Days 30-60)
Once 30 days of clean reports show no recurring TLS failures, increase max_age to 604800 (7 days) and update the policy ID. This signals to senders that the policy is stable.
Continue monitoring TLS-RPT reports. Some sender MTAs check the policy infrequently; you want to see consistent successful delivery patterns from a wide range of senders before progressing.
Phase 4 — Switch to enforce mode (Day 60+)
Update the policy file to mode: enforce. Update the DNS TXT record’s id to invalidate cached policies.
Continue monitoring TLS-RPT reports daily for at least 30 more days. In enforce mode, any TLS configuration failure causes actual mail loss; you want immediate visibility into anomalies.
The total migration timeline is typically 60-90 days for a production domain. Operators who jump to enforce mode in week 1 frequently see mail loss within days from edge cases their normal monitoring did not catch.
DANE coexistence on Postfix
DANE (DNS-based Authentication of Named Entities) is an alternative TLS validation mechanism for SMTP. Where MTA-STS validates certificates against public CA infrastructure, DANE validates them against TLSA records published in DNSSEC-signed DNS zones.
The two protocols can coexist and both protect inbound mail. Many EU receivers and German government systems require DANE specifically; Microsoft, Google and Yahoo support both as senders.
Postfix supports both natively (DANE since 2.11, MTA-STS plus TLS-RPT via plugins for Postfix 3.10+).
Postfix DANE configuration
DANE for outbound (your Postfix as sender) requires a DNSSEC-validating resolver and these main.cf directives:
smtp_dns_support_level = dnssec
smtp_tls_security_level = dane
DANE for inbound (your Postfix as receiver) requires:
- DNSSEC-signed DNS zone for your domain
- TLSA records published for your MX hosts
- TLS configured on the receiving Postfix
The TLSA record format:
_25._tcp.mx.yourdomain.com. IN TLSA 3 1 1 <hash of cert>
Generate the TLSA record from your certificate:
openssl x509 -in /etc/letsencrypt/live/mx.yourdomain.com/fullchain.pem \
-pubkey -noout -outform der | openssl pkey -pubin -inform der -outform der | \
openssl dgst -sha256 -binary | hexdump -ve '/1 "%02x"'
Why both DANE and MTA-STS
The combination provides defense in depth:
- DANE: works for senders that use DNSSEC-validating resolvers (most modern MTAs do)
- MTA-STS: works for senders that do not use DNSSEC but support MTA-STS (Microsoft, Google, Yahoo)
Senders with both DANE and MTA-STS support typically prefer DANE when both are available. Senders without DNSSEC capability fall back to MTA-STS. Together, they cover near-100% of modern SMTP traffic.
The German national security standard BSI TR-03108 requires all three (DANE, MTA-STS, TLS-RPT) for compliant mail systems. For operators serving German government or financial customers, the triple is mandatory.
Postfix MTA-STS plugin
For Postfix 3.10+ acting as sender (validating MTA-STS policies of receivers you send to), the postfix-mta-sts-resolver package provides the integration:
# Install
apt install postfix-mta-sts-resolver
# Configure in /etc/postfix/main.cf
smtp_tls_policy_maps = socketmap:unix:/var/spool/postfix/mta-sts/socket:postfix
The resolver runs as a daemon, fetches MTA-STS policies from receiving domains, caches them, and tells Postfix whether to enforce TLS for each delivery.
For Postfix as receiver, MTA-STS does not require additional Postfix configuration — it is the sending MTA’s responsibility to fetch and honour your policy. You just need the policy file accessible via HTTPS.
Self-hosted TLS-RPT report parsing
TLS-RPT reports arrive as JSON, either inline in email bodies or as application/tlsrpt+json attachments. The structure is well-defined and parseable without commercial tooling.
Report structure
Example aggregate report:
{
"organization-name": "Sending Org",
"date-range": {
"start-datetime": "2026-04-24T00:00:00Z",
"end-datetime": "2026-04-24T23:59:59Z"
},
"contact-info": "[email protected]",
"report-id": "2026-04-24T00:00:00Z_yourdomain.com",
"policies": [
{
"policy": {
"policy-type": "sts",
"policy-string": ["version: STSv1", "mode: enforce", "mx: mx.yourdomain.com", "max_age: 604800"],
"policy-domain": "yourdomain.com",
"mx-host": ["mx.yourdomain.com"]
},
"summary": {
"total-successful-session-count": 1247,
"total-failure-session-count": 3
},
"failure-details": [
{
"result-type": "certificate-expired",
"sending-mta-ip": "192.0.2.1",
"receiving-mx-hostname": "mx.yourdomain.com",
"failed-session-count": 3
}
]
}
]
}
Python parser for daily aggregation
A working pattern for self-hosted TLS-RPT processing:
import json
import imaplib
import email
from datetime import datetime
from collections import defaultdict
def fetch_tls_reports(imap_server, username, password):
"""Fetch TLS-RPT reports from a dedicated mailbox."""
mail = imaplib.IMAP4_SSL(imap_server)
mail.login(username, password)
mail.select('INBOX')
_, messages = mail.search(None, 'UNSEEN')
reports = []
for msg_id in messages[0].split():
_, msg_data = mail.fetch(msg_id, '(RFC822)')
msg = email.message_from_bytes(msg_data[0][1])
# Extract JSON payload (inline or attachment)
for part in msg.walk():
if part.get_content_type() == 'application/tlsrpt+json':
payload = part.get_payload(decode=True)
reports.append(json.loads(payload))
elif part.get_content_type() == 'application/json':
# Some senders use generic JSON content-type
try:
reports.append(json.loads(part.get_payload(decode=True)))
except json.JSONDecodeError:
pass
mail.close()
mail.logout()
return reports
def aggregate_failures(reports):
"""Aggregate failure patterns across reports."""
failures_by_type = defaultdict(int)
failures_by_sender = defaultdict(int)
successes_total = 0
failures_total = 0
for report in reports:
for policy in report.get('policies', []):
summary = policy.get('summary', {})
successes_total += summary.get('total-successful-session-count', 0)
failures_total += summary.get('total-failure-session-count', 0)
for failure in policy.get('failure-details', []):
result_type = failure.get('result-type', 'unknown')
sender_ip = failure.get('sending-mta-ip', 'unknown')
count = failure.get('failed-session-count', 0)
failures_by_type[result_type] += count
failures_by_sender[sender_ip] += count
return {
'total_successes': successes_total,
'total_failures': failures_total,
'failure_rate': failures_total / (successes_total + failures_total) if (successes_total + failures_total) > 0 else 0,
'by_type': dict(failures_by_type),
'by_sender': dict(failures_by_sender)
}
def alert_if_anomaly(aggregated, failure_rate_threshold=0.01):
"""Alert if failure rate exceeds threshold."""
if aggregated['failure_rate'] > failure_rate_threshold:
print(f"ALERT: TLS failure rate {aggregated['failure_rate']:.2%}")
print(f" Top failure types: {sorted(aggregated['by_type'].items(), key=lambda x: -x[1])[:5]}")
print(f" Top failing senders: {sorted(aggregated['by_sender'].items(), key=lambda x: -x[1])[:5]}")
return True
return False
# Daily processing
reports = fetch_tls_reports('imap.yourdomain.com', '[email protected]', PASSWORD)
aggregated = aggregate_failures(reports)
alert_if_anomaly(aggregated, failure_rate_threshold=0.01)
This gives you the same essential information that commercial TLS-RPT parsers provide: failure rate, failure types, senders with issues. Total cost: $0 for the parser, daily cron execution time. The commercial parsers offer prettier dashboards and alert routing; the self-hosted version handles the data.
Common failure modes (the testing phase catches them)
Issues we see during MTA-STS testing-mode rollouts that would have broken mail in enforce mode:
Failure 1 — Backup MX with self-signed certificate: organisations frequently have a primary MX with proper Let’s Encrypt cert and a backup MX with self-signed cert “because nobody actually uses it”. MTA-STS in enforce mode rejects the backup MX, and senders that fail over to it fail entirely. Fix: proper certificate on all MX hosts in the policy.
Failure 2 — Certificate hostname mismatch: MX record points to mail.yourdomain.com, but the cert was issued for yourdomain.com. SAN field does not include the MX hostname. MTA-STS validation fails. Fix: certificate must cover the exact MX hostname.
Failure 3 — Wildcard certificate gotcha: certificate is *.yourdomain.com. MX is mx.yourdomain.com. Wildcards do not cover the apex domain or grandchildren — mx.yourdomain.com is covered, but mx1.mail.yourdomain.com would not be. Fix: explicit certs or wildcard at the right level.
Failure 4 — TLS 1.0 / 1.1 still enabled: stricter senders (Microsoft enforces TLS 1.2+) reject older TLS versions. Fix: update Postfix/Exim TLS configuration to require minimum TLS 1.2.
Failure 5 — DNSSEC misconfiguration breaks DANE without breaking MTA-STS: domains running both DANE and MTA-STS sometimes have DNSSEC issues that break DANE validation. Senders preferring DANE see failures; senders falling back to MTA-STS work fine. Fix: validate DNSSEC zone signing regularly with dig +dnssec.
What we recommend at Blue Spirit
For transparency: we run MTA-STS and TLS-RPT for our own infrastructure using the self-hosted Nginx pattern described above. For PowerMTA hosting clients, MTA-STS is part of the standard onboarding — we publish the policy on their behalf and parse TLS-RPT reports via our internal pipeline.
For operators considering the implementation:
Self-hosted path is appropriate if:
- You already run web infrastructure with HTTPS
- You have a Linux operator who can write the Python parser (or use the one above)
- You want to avoid recurring SaaS cost
- You operate at scale where the SaaS cost adds up
SaaS path (RedSift OnDMARC, PowerDMARC, Mailhardener, EasyDMARC) is appropriate if:
- You want managed certificate handling and DNS automation
- Your team does not have Linux ops capacity
- You operate at small scale where the SaaS cost is below the engineering time cost
- You need integration with broader DMARC/BIMI tooling
The honest cost comparison: SaaS pricing for MTA-STS+TLS-RPT typically runs $30-200/month per domain. Self-hosted is $0 incremental ($5-10/month if you set up a dedicated VPS) but requires 4-8 hours initial engineering work and ~30 minutes/month maintenance.
Related guides — completing the authentication and observability picture
MTA-STS and TLS-RPT do not exist in isolation — they are layer 1 (transport) of the modern email authentication stack covered comprehensively in our 2026 email authentication guide, which walks through SPF, DKIM, DMARC, MTA-STS, BIMI, and ARC with the dependency graph between layers explicit. For the DMARC enforcement context that makes MTA-STS pressure operationally relevant in 2026, see our DMARC enforcement 2026 survival guide. For self-hosted DMARC aggregate report processing (which complements TLS-RPT processing using nearly identical infrastructure patterns), see our self-hosted parsedmarc + Elasticsearch guide. For unified observability across all email reputation signals (Postmaster Tools, SNDS, DMARC, TLS-RPT in single Grafana dashboard), see our unified email observability guide. For the broader monitoring stack decision (budget vs enterprise tooling for deliverability operations), see our monitoring stack budget vs enterprise comparison.
If you want help architecting MTA-STS deployment for production scale or coexistence with DANE on Postfix, our deliverability audit covers exactly this work. Most clients we audit either have no MTA-STS at all or have it stuck in testing mode for years because the testing-to-enforce migration was never completed.
The honest summary of MTA-STS and TLS-RPT in 2026: they are mature protocols with broad support, the implementation is genuinely simple if you understand the components, and the SaaS dependency that most guides assume is unnecessary for operators with basic Linux competence. The 60-90 day testing-to-enforce migration is the actual project; the protocol setup itself takes an afternoon. Skipping the testing phase is the failure mode that produces mail loss; respecting the phase produces a transport security layer that complements your existing SPF/DKIM/DMARC at zero recurring cost.
Something we should write about? Reply with your topic at [email protected].