MTA-STS and TLS-RPT
Purpose. Tell sending servers that mail for your domains must arrive over verified TLS to your MX (MTA-STS, RFC 8461), and get reports when they could not (TLS-RPT, RFC 8460). Closes the downgrade / MX-spoofing hole that SPF, DKIM and DMARC leave open.
Behaviour.
- With
tls.mode = acmeandmtaSts.mode ≠ none(defaulttesting), every active domain routed to this server (routing: local) gets three more DNS rows:_mta-sts.<domain> TXT "v=STSv1; id=<id>",mta-sts.<domain> CNAME <hostname>(the policy host) and_smtp._tls.<domain> TXT "v=TLSRPTv1; rua=mailto:dmarc-reports@<domain>", and the server answershttps://mta-sts.<domain>/.well-known/mta-sts.txtwithCaddy routesversion: STSv1mode: testingmx: <hostname>max_age: 604800mta-sts.<domain>hosts to the API and issues themta-sts.<domain>certificate on demand at the first request; before issuing, it asks the API (/api/v1/internal/tls-ask?domain=) which says yes only formta-sts.,autoconfig.orautodiscover.on an active domain routed to this server (none of the three for a remote domain), and for the server’s own two hosts,postmaster.andwebmail.of the server’s domain — no certificates for arbitrary names. The admin publishes the generatedCNAMEformta-sts.<domain>→ the mail hostname (anAto the same address works as well) plus the two TXT rows. Caddy’s host matcher needs an equal label count and*stands for exactly one label, so the route is not a singlemta-sts.*—serviceHostPatterns()emits one pattern per domain depth, up to a five-label domain. A deeper domain gets no route, and because certificates are issued by SNI rather than by route it would answer an empty 200 behind a valid certificate rather than an error.scripts/ci/caddy-routes.shasserts the whole set against a real Caddy — but only whatrenderCaddyemits, not what a running server has loaded. Those are different things: an upgrade swaps images and does not re-render, so a server installed on an older build kept serving that build’s Caddy config — this route was dead on our own host after upgrading to a version containing the fix. The API now re-applies at boot when the build that rendered the live configuration is not the one running, which closes that gap. See Upgrade → Configuration is re-rendered after an upgrade (and what to check if it is not). - The policy
idis a hash of the policy text: it changes when mode, max_age or the hostname changes, which is what tells receivers to re-fetch. DNS verification checks both TXT rows exactly. - Settings → MTA-STS: MTA-STS policy mode (
none/testing/enforce) and max_age (between 86400, a day, and 31557600, a year; default 604800). Usetestinguntil the TLS reports section shows no failures, thenenforce— that tab is what makes the advice actionable rather than aspirational.
Configuration. ServerConfig.mtaSts.{mode,maxAgeSeconds}; requires tls.mode = acme (BYO and none cannot obtain per-domain certificates — the rows and the endpoint are simply absent then).
Limits & edge cases.
-
TLS-RPT reports (JSON,
.json.gz) arrive in the samedmarc-reports@mailbox as DMARC reports and are parsed by the same ingest job, every 30 minutes. Stored per domain intls_report, shown under Domains → a domain → TLS reports, and — the point of the whole thing — a report carrying failed sessions raises an alert (tls-rpt:<domainId>, warning).The routing between the two report kinds is by label first (
application/tlsrpt+gzip,.json.gz) and by the decoded body second, because both kinds are commonly gzipped with a generic content type: a DMARC report starts<, a TLS-RPT one{. An attachment that decodes to nothing is ignored, exactly as before.The alert resolves only on a later clean report that saw traffic. Both conditions matter. A reporter with nothing to say says nothing about whether the problem is fixed, and a clean report covering the same window as the failures is a second opinion about the same hours rather than evidence they stopped — resolving on either would make the alert flap and teach people to ignore it.
-
On-demand issuance needs the
mta-sts.<domain>name to resolve to this host and ports 80/443 reachable; the DNS row generated for it isCNAME mta-sts.<domain> → <hostname>(anAtoPUBLIC_IPworks too) — add it at your provider. -
A remote-routed domain gets no policy from here: its mail is received by its own server, so
/.well-known/mta-sts.txtanswers 404 for it andtls-askrefuses a certificate for itsmta-sts.name, rather than serve a policy naming this host as its MX. The same refusal covers itsautoconfig.andautodiscover.names, whose endpoints answer nothing for a remote domain either. -
enforcewith a wrongmx:line blocks inbound mail from MTA-STS-aware senders formax_age; the policy always names the configured hostname, so only change the hostname withmode = testing.
Why the reports are read, and not just kept
Section titled “Why the reports are read, and not just kept”This feature exists because of what happened without it. MTA-STS was ticked as delivered in this product and did not work for its entire life — the routes served nothing, no sender could fetch a policy, and nobody knew. The channel that would have said so, daily and from every major sender, was TLS-RPT: arriving in a mailbox, unparsed.
So two things are deliberate in the TLS reports section. A window with no reports at all says so
loudly rather than rendering an empty table, because silence is exactly how a broken policy stays
invisible and an empty table reads like “no problems”. And a no-policy-found policy type is
surfaced as a finding rather than filtered out — it means that sender could not fetch a policy at all,
which is the same failure this feature was built to catch.