DANE TLSA for SMTP: Publishing, DNSSEC, and Proving the Cert Binding
STARTTLS is opportunistic and trivially strippable — DANE is the only thing that makes SMTP TLS non-downgradeable and the receiving certificate actually authenticated. Here is the record-to-cert binding done right: the openssl one-liners, the DNSSEC prerequisite, the rollover choreography timed against TTL, and the exact commands that prove the binding before you trust it in production.
EvilMail TeamJuly 17, 202611 min read
Send a message to any mailserver on port 25 and the security story is a lie you tell yourself. The receiving MTA advertises 250-STARTTLS, your sender upgrades the plaintext session to TLS, and everyone feels encrypted. Now put an attacker on the path. All they have to do is delete the 250-STARTTLS line from the EHLO response. The sender sees no STARTTLS on offer, silently falls back to cleartext, and the whole conversation — headers, bodies, credentials in bounce reports — crosses the wire in the clear. No error, no deferral, nothing in a log to notice.
Even when TLS *does* negotiate, SMTP authenticates nothing. RFC 3207 STARTTLS accepts self-signed certificates and never checks the name. A man-in-the-middle presents any certificate at all, the handshake completes, and you have an encrypted tunnel straight to the attacker. Opportunistic TLS on SMTP buys you privacy against a passive eavesdropper and precisely zero protection against anyone active.
DANE (DNS-Based Authentication of Named Entities, RFC 6698, profiled for SMTP by RFC 7672) closes both holes at once. It pins the receiving certificate to a record in DNS, and DNSSEC signs that pin so it can't be forged. Two consequences fall out. Once a usable TLSA record exists, TLS becomes mandatory — a validating sender that finds one will not fall back to cleartext, so the STARTTLS-stripping attack turns into a delivery failure instead of a silent leak. And the certificate is now authenticated against the DNSSEC-signed pin, with no CA in the trust path. As of 2026 this finally matters on the big receivers: both Google (Gmail/Workspace) and Microsoft (Exchange Online) validate outbound DANE when the destination domain is signed. Publish it correctly and your inbound mail from the largest senders on earth becomes downgrade-resistant.
DANE TLSA Records for SMTP with DNSSEC — Setup, Rollover, Testing — EvilMail Blog
The stance here is narrow and opinionated, because nearly every broken DANE deployment dies from exactly two mistakes: publishing a TLSA record over an unsigned zone (every validating sender silently ignores it), and botching the cert-rollover choreography (renew the cert, forget to roll the pin, mail to you starts deferring). Get those two right and the rest is mechanical.
What actually goes in a TLSA record
The record looks like this:
dns
_25._tcp.mail.example.com. 3600 IN TLSA 3 1 1 a9f2...64hexchars...
The owner name is the part people fumble first. It is _25._tcp. — port 25, protocol tcp — prepended to the MX host, not the mail domain. If your domain's MX points at mail.example.com, the TLSA record lives at _25._tcp.mail.example.com and pins *that host's* certificate. The domain apex has nothing to do with it.
The three numeric fields are certificate usage, selector, and matching type:
Usage — 0 PKIX-TA, 1 PKIX-EE, 2 DANE-TA, 3 DANE-EE. RFC 7672 permits only 2 and 3 for SMTP. Usages 0 and 1 assume a CA trust path, and opportunistic DANE has none, so a validating sender ignores them outright. Publishing 1 1 1 for SMTP is the same as publishing nothing.
Selector — 0 hashes the full certificate, 1 hashes the SubjectPublicKeyInfo (the public key plus its algorithm identifier).
Matching — 0 exact match, 1 SHA-256, 2 SHA-512.
The default you should reach for is 3 1 1: DANE-EE, SPKI selector, SHA-256. DANE-EE pins the leaf directly, so no chain and no CA validation is involved — the connection succeeds if and only if the presented leaf's key matches. The SPKI selector is the operationally important choice: it hashes the *public key*, not the whole certificate. As long as you renew with the same keypair, the SPKI hash is stable, and you can reissue the leaf as often as Let's Encrypt will let you without ever touching DNS. Selector 0 (full cert) forces a DNS change on every single renewal — a self-inflicted wound.
The alternative worth knowing is 2 1 1 — DANE-TA, pinning an intermediate or CA public key instead of the leaf. Use it when many hosts share one issuer and you rotate leaves constantly: pin the issuer once and let leaves churn beneath it. It costs a chain-building step at validation time and ties your security to the CA's key, so for a single mailserver 3 1 1 is cleaner.
One sentence on the neighbour technology: MTA-STS (RFC 8461) also makes SMTP TLS mandatory, but it publishes policy over HTTPS and leans on the WebPKI rather than DNSSEC, so it stops an active attacker who can't forge a WebPKI cert but not one who can. DANE and MTA-STS coexist happily; many operators publish both and let the sender use whichever it supports.
DNSSEC first — no signed chain, no DANE
This is the prerequisite that separates a security control from decoration. If the zone isn't DNSSEC-signed, the record does nothing. A validating sender only trusts a TLSA record it can prove is authentic, and that proof is the DNSSEC chain to the root. Both the MX RRset and the TLSA RRset must sit under a valid signed chain. No signature, no trust, and the sender falls back to the same opportunistic TLS you were trying to escape — silently.
Verify the AD (Authenticated Data) bit is set on the lookup:
bash
dig +dnssec TLSA _25._tcp.mail.example.com @127.0.0.1
# look for the "ad" flag in the ;; flags: line of the header
delv @127.0.0.1 TLSA _25._tcp.mail.example.com
# expect: "; fully validated"
The @127.0.0.1 is not incidental. On the sender side, Postfix trusts the AD bit from its configured resolver, over an unauthenticated link. If you point smtp_dns_support_level = dnssec at 8.8.8.8, the AD bit arrives over a plaintext UDP hop that an on-path attacker can rewrite, forging "this is validated" for a record that isn't. That reopens the exact hole DANE is meant to close. Run a local validating resolver — unbound on 127.0.0.1 — so the trust boundary is a loopback socket, not the public internet.
Generating the record from your certificate
The 3 1 1 value is one openssl pipeline: pull the public key, re-encode it as DER SPKI, hash it.
bash
openssl x509 -in /etc/letsencrypt/live/mail.example.com/cert.pem -noout -pubkey \
| openssl pkey -pubin -outform DER \
| openssl sha256 | awk '{print $2}'
# → a9f2... (paste this as the final field)
If you prefer a tool that speaks TLSA natively, hash-slinger emits the whole record:
For the 2 1 1 issuer pin, run the same pubkey pipeline against the intermediate in chain.pem instead of the leaf. The finished BIND snippet:
dns
_25._tcp.mail.example.com. 3600 IN TLSA 3 1 1 a9f2c4...64hex
Keep the TTL modest — 3600 is the usual choice — because the rollover math below is measured in TTLs, and a day-long TTL makes every cert change a day-long operation.
Publishing and the rollover choreography
This is where most deployments break, and it breaks because people do the intuitive thing in the wrong order. The rule from RFC 7671 is current + next: the new pin must be live in DNS and cached everywhere *before* the certificate that matches it is served.
Concretely, when a renewal with a new keypair is coming:
1.Compute the SHA-256 SPKI hash of the incoming key and publish it as a *second* TLSA record alongside the existing one. Two TLSA records now answer the query; a sender matches if the leaf satisfies *either*.
2.Wait at least the old TTL (3600s) so every resolver cache that held the old record now also holds the new one.
3.Deploy the new certificate. Every sender already has a matching pin cached.
4.After another TTL, remove the stale record.
The naive path — renew the cert, *then* remember the pin — produces an instant 3 1 1 mismatch. Under smtp_tls_security_level = dane-only on the sending side, that is a hard deferral: mail to you queues and eventually bounces until you fix DNS. The whole point of the overlap window is that there is never a moment when the served leaf lacks a matching cached pin.
Automate it so a 3am renewal can't invalidate DANE. Wire a certbot deploy hook that pushes the next pin via your DNS provider's API before the cert goes live:
The hook computes the new SPKI hash, publishes it alongside the old via the API, and — on a subsequent run, once the TTL has passed — prunes the stale record. Never let certbot renew fire without one.
Testing the binding end to end
Prove it before you trust it. Postfix ships the tool:
bash
posttls-finger -c -l dane mail.example.com
A pass reads: Verified TLS connection established ... Matched DANE TLSA record 3 1 1 .... If the pin is stale you'll see the offered fingerprint next to a "no match" — that diff is exactly what to compare against your published hash.
Look for Verification: OK and DANE TLSA 3 1 1 ...matched EE certificate at the bottom of the handshake dump. A mismatch prints Verification error and reports the record as unmatched — same command, unambiguous verdict.
For an outside view that also checks the DNSSEC chain, run the domain through internet.nl/mail, dane.sys4.de/smtp, or Hardenize (Red Sift). internet.nl in particular distinguishes "record present but zone unsigned" from "record present and validated" — the failure people most often can't see from their own resolver.
When it breaks: the failure catalog
Cert renewed, pin not rolled — the offered leaf no longer matches 3 1 1. Under dane-only this is a deferral; under dane it silently downgrades. Root cause of most outages.
Zone not signed — validating senders ignore the record entirely and fall back to opportunistic TLS. Looks fine locally, does nothing.
MX target is a CNAME — the DANE lookup requires the MX host to be a real A/AAAA name; a CNAME breaks the base-domain derivation and DANE never engages.
Usage 0 or 1 published — ignored for SMTP per RFC 7672. Effectively no record.
Resolver not validating — no AD bit, so Postfix never treats the record as trusted and DANE stays off.
Multiple MX hosts — each host needs its own _25._tcp.<host> TLSA matching *that host's* certificate. One shared record for several distinct leaves fails on all but one.
Pre-flight checklist
Zone is DNSSEC-signed and the chain validates to root (delv says "fully validated").
TLSA published at _25._tcp.<mx-host> for every MX target, each with its own cert's hash.
Usage is 2 or 3 only; default 3 1 1 over stable SPKI.
MX targets are A/AAAA, never CNAME.
Sender side runs a local validating resolver; smtp_dns_support_level = dnssec points at 127.0.0.1, not a public resolver.
certbot renew carries a deploy hook that pre-publishes the next pin and waits a TTL before pruning the old one.
posttls-finger -c -l dane and openssl s_client -dane_tlsa_rrdata both report a match against the live cert.
TTL is 3600 and your rollover script's wait is at least one TTL.