Skip to main content
by Meysam Azad
14 min read

TLSA Mismatch Troubleshooting: Compute the 3 1 1 Hash and Find the Drift

Mail flowed for weeks. Then, right after a certificate renewal, DANE-validating senders started deferring with lines like Server certificate not trusted or Verification failed. The certificate differs. That timing is the signature of a DANE TLSA 3 1 1 mismatch: the SHA-256 hash published in DNS no longer matches the public key your MX actually serves.

This guide is the debugging session. You will decode the sender’s error message, compute the served hash with OpenSSL, compare it against the published record, identify which of five root causes you have, and roll over without breaking validation again. If you are setting up a record from scratch instead, start with our TLSA record setup guide — this post assumes the record already exists and has stopped matching.

What does a TLSA 3 1 1 mismatch mean — and what’s the fastest check?

The fastest TLSA record check is two commands, side by side. First, what DNS publishes:

dig +short TLSA _25._tcp.mx.example.com

A healthy answer looks like 3 1 1 followed by a single 64-character hex string — for example 3 1 1 0d2280aa14c3…6bbc3fdf — with no (stdin)= prefix, no whitespace inside the hash, and no truncation. Anything else in the rdata is already a finding. Mail actively bouncing right now? Jump straight to the emergency add-don’t-replace first aid and come back to diagnose afterward.

Second, what the MX actually serves — the live SPKI pipeline shown in the next section. If the two hex strings differ, you have a key or selector problem. If they match but senders still fail, the problem is DNSSEC or the certificate chain, not the hash.

What do those three digits assert? A 3 1 1 mismatch means the SHA-256 digest of your server’s SubjectPublicKeyInfo (SPKI) no longer equals the certificate-association data published at _25._tcp.<mx>. The fields come from RFC 6698: certificate usage 3 is DANE-EE — pin this end-entity certificate (§2.1.1); selector 1 hashes the public key rather than the whole certificate (§2.1.2); matching type 1 is SHA-256 (§2.1.3). RFC 7672 §3.1 recommends exactly this combination for SMTP.

Confirm DNSSEC before touching the record. Run dig +dnssec against a validating resolver and look for the ad (authenticated data) flag. RFC 7672 §2.2 is blunt about why: a TLSA lookup that is not DNSSEC-authenticated is unusable, so a perfect hash behind broken DNSSEC still fails. A 2022 measurement study of DANE SMTP servers (Lee et al., USENIX Security 2022) found that missing DS records at the parent zone caused roughly 99% of the DNSSEC failures it observed — check the DS delegation first.

Prefer one pass over five terminals? Paste your MX into our DANE/TLSA checker — it resolves the record, validates DNSSEC, and compares the live certificate in a single check.

How do you compute the TLSA hash with OpenSSL?

To compute the TLSA hash with OpenSSL, extract the SPKI, encode it as DER, and hash it with SHA-256. This is the canonical pipeline Viktor Dukhovni published on the IETF dane list in 2014, and the Postfix TLS_README endorses the resulting 3 1 1 association as best practice:

Hash the on-disk certificate's SPKI (3 1 1 value) bash
# Hash the SubjectPublicKeyInfo of the certificate on disk (3 1 1 data).
# -binary | hexdump emits clean hex — no "(stdin)=" prefix, no newline.
openssl x509 -in cert.pem -noout -pubkey \
  | openssl pkey -pubin -outform DER \
  | openssl dgst -sha256 -binary \
  | hexdump -ve '/1 "%02x"'

Each stage does one job: x509 -noout -pubkey extracts the SubjectPublicKeyInfo from the certificate, pkey -pubin -outform DER converts it to binary DER, and dgst -sha256 hashes those bytes. The generic pkey subcommand works for both RSA and ECDSA keys — Dukhovni noted on the same thread that the pipeline “is not RSA-specific and works equally well for ECDSA keys” — so prefer it over the algorithm-specific rsa/ec variants, which produce byte-identical output.

That hashes the certificate on disk. A mismatch investigation needs the certificate your MX serves on port 25, which may not be the same file. The drift check swaps the input for a live s_client session:

Hash the SPKI the MX serves live on port 25 bash
# Hash the SPKI the MX actually serves on port 25, then compare
# against: dig +short TLSA _25._tcp.mx.example.com
openssl s_client -connect mx.example.com:25 -starttls smtp </dev/null 2>/dev/null \
  | openssl x509 -pubkey -noout \
  | openssl pkey -pubin -outform DER \
  | openssl dgst -sha256 -binary \
  | hexdump -ve '/1 "%02x"'

One honest caveat: many ISPs and cloud hosts block outbound port 25, so this command hangs rather than fails — run it from the mail server itself, or let the DANE checker make the connection for you.

We verified this pipeline against a public DANE domain: the served SPKI hashed to 0d2280aa14c34b9f…6bbc3fdf, exactly matching one of the three published 3 1 1 records. When your output does not match any published record, you have found the drift.

Watch the output format. openssl dgst labels piped input — (stdin)= on older releases, SHA2-256(stdin)= on OpenSSL 3.x — and appends a newline. Paste that verbatim into a zone file and you have manufactured a malformed record. The -binary | hexdump -ve '/1 "%02x"' form in the snippet emits the bare 64-character hex digest, nothing else.

OpenSSL can also render the verdict itself. The verify one-liner hands s_client the published rdata and asks it to authenticate the connection the way a DANE-validating MTA would:

Verify the connection against your published TLSA rdata bash
# Ask OpenSSL to validate the connection the way a DANE MTA would.
# Repeat -dane_tlsa_rrdata to test current + next records during rollover.
openssl s_client -connect mx.example.com:25 -starttls smtp \
  -dane_tlsa_domain mx.example.com \
  -dane_tlsa_rrdata "3 1 1 <your-published-hash>" </dev/null

A match prints DANE TLSA 3 1 1 …<hash> matched the EE certificate at depth 0 followed by Verify return code: 0 (ok) (observed with OpenSSL 3.6.1). You can pass -dane_tlsa_rrdata multiple times to test the current and next records together during a rollover.

Cross-check with a second tool before you edit DNS: Postfix’s posttls-finger -c -Lsummary mx.example.com reports which published record matched the live chain, and GnuTLS’s danetool --check performs an independent fetch-and-verify. Once you have the correct hash, the TLSA record generator assembles the full record — the setup guide covers publishing it, so we won’t repeat that here.

Why doesn’t my computed hash match the published record?

Five root causes account for the mismatches that reach production. The evidence column is the differentiator — each cause leaves a distinct fingerprint in the hashes you just computed.

Root causeSymptom / sender errorEvidence to confirmFix
1. Renewal minted a new keyMail defers right after a certificate renewalOn-disk and live SPKI hashes agree with each other but not with DNScertbot --reuse-key, or the current + next rollover (next section)
2. Wrong selector or matching typeRecord never matched, even on day oneThe full-certificate digest equals the DNS value while the SPKI hash doesn’tRecompute with the SPKI pipeline and republish as 3 1 1
3. Leaf vs intermediate confusionPostfix logs no matching DANE TLSA recordsYour hash matches an intermediate block from s_client, not the leafHash the first certificate in the chain; for DANE-TA(2), serve the pinned CA in the chain
4. Stray artifact in the recordValidators report malformed or wrong-length dataRecord contains (stdin)=, whitespace, or fewer than 64 hex charactersRegenerate with -binary | hexdump and republish
5. Not a hash problem: DNSSEC or resolver failureTLSA lookup error (Postfix), tlsa lookup DEFER (Exim)No ad flag on dig +dnssec; the hashes actually agreeFix the DS record, RRSIG expiry, or resolver — leave TLSA alone
The five TLSA mismatch root causes, their evidence, and their fixes
Decision flowchart mapping TLSA hash-comparison evidence — which computed hash equals which published value — to the five mismatch root causes: renewal drift, wrong selector, leaf versus intermediate confusion, stray artifact, and DNSSEC failure
Follow the evidence: which hash equals which decides the root cause before you touch DNS.

Cause 1 dominates production incidents. certbot generates a fresh key on renewal by default, and Let’s Encrypt certificates live 90 days — so the record breaks at the first renewal, long after anyone associates the change with DNS. Certificate rotation is precisely the failure mode selector 1 was designed to survive, but only when the key is reused.

Cause 2 is the classic authoring error. To confirm it, hash the full certificate — openssl x509 -outform DER | openssl dgst -sha256 — and compare that digest against the DNS value. The sys4 DANE common mistakes page documents it verbatim: “Some domains publish TLSA records with a selector of SPKI(1), which indicates a digest of a public key, but the digest in the TLSA record is that of the containing certificate.” Hashing the PEM text instead of binary DER is the sibling mistake on the same page. The 2022 USENIX study found selector-0 records mismatched 34% of the time in one of its datasets — far more than selector 1 — because a full-certificate digest changes on every reissue even with key reuse.

Cause 3 bit DANE-TA(2) publishers hard when Let’s Encrypt stopped providing its cross-sign by default in February 2024: records pinning the ISRG root stopped validating because the root no longer appeared in the served chain.

The sender’s error message narrows things down before you type a single command:

  • Postfix: Server certificate not trusted and no matching DANE TLSA records mean DANE authentication ran and failed — a genuine mismatch (causes 1–3). TLSA lookup error means the DNS query itself failed — cause 5.
  • Exim: Verification failed. The certificate differs. is the post-renewal mismatch; DANE error: tlsa lookup DEFER is a resolver problem.
  • Microsoft 365: 4.7.323 tlsa-invalid — per Microsoft Learn, this code “can only be generated after a DNSSEC-authentic TLSA record has been returned,” so DNSSEC is intact and the certificate is the problem.

Rule out the causes in order: rotation drift first, then the wrong hashed object, then artifacts, then DNSSEC.

How do you roll over a TLSA record without breaking DANE?

RFC 7671 §8 updates the publisher rules: every usage/selector/matching-type combination in your RRset must include at least one record matching the current chain, at all times. §8.1 spells out the safe sequence — publish the record for the future key alongside the current one at least two TTLs before deploying the new chain, let caches age out, deploy the new certificate, verify, then remove the obsolete record. The Postfix TLS_README goes one step more conservative: wait until the DNSSEC signature on the previous TLSA RRset expires. We walk through the publish-before-swap zone file in the setup guide, so here we stay on the operational choice.

Two models keep renewals from ever reopening this incident:

  • Key reuse. certbot --reuse-key keeps the SPKI stable across renewals, so the 3 1 1 record never needs to change. Confirm reuse_key = True actually landed in the lineage’s renewal config — a Let’s Encrypt community thread documents operators who believed it was set while certbot kept minting fresh keys.
  • Current + next. Pre-generate the next key pair and publish its 3 1 1 record alongside the current one, so the RRset always contains both. Dukhovni presented this pattern at ICANN61 (2018); on renewal you switch to the already-trusted next key, then publish a fresh “next.”

Two caveats scale with your fleet. If your server offers both RSA and ECDSA certificates, the negotiated ciphersuite decides which one is served — the RRset needs a matching 3 1 1 for each key. And RFC 7672 §2.1 binds TLSA records to each MX hostname, so every MX host needs its own correct set.

One forward-looking date worth planning around: Let’s Encrypt announced on 2 December 2025 that certificate lifetimes shrink to 45 days by February 2028. That halves the drift window — manual TLSA maintenance stops being viable.

How do you catch TLSA drift before mail bounces?

Your own monitoring sees your DNS; only senders see your DNS, DNSSEC, and certificate the way validation does. TLS-RPT (RFC 8460) gives you that sender’s-eye view: publish a _smtp._tls TXT record and participating senders mail you daily JSON reports. Three RFC 8460 result types separate the failure classes this guide covered: tlsa-invalid means the record was DNSSEC-authentic but the certificate didn’t match (causes 1–4); dnssec-invalid means the resolver couldn’t validate the record (cause 5); and dane-required means the sender demanded DANE and found no usable record at all. New to the protocol? Start with our TLS-RPT guide, and drop a report into the TLS report analyzer to translate result types into the failing MX and cause.

Scheduled checks close the loop between renewals. Dukhovni’s danecheck walks every MX host, returns a non-zero exit code on any mismatch, and can verify chains at a future time offset to catch imminent breakage — cron-friendly by design. sys4’s smtp-dane-verify ships a Prometheus exporter for the same check. And the -dane_tlsa_rrdata one-liner from earlier belongs inside your ACME deploy hook, blocking the deployment when verification returns anything but 0.

If you run MTA-STS alongside DANE, note that a missing policy file fails in the same silent way a stale TLSA does — we cover that failure mode in fixing a missing MTA-STS policy.

DMARCguard monitors DANE and TLS-RPT among its 9 protocols: it performs the live-vs-published comparison continuously and tells you which record to fix, instead of leaving the diff to your next incident.

FAQ

How do I compare my live certificate against my published TLSA record?

Run two commands side by side: dig +short TLSA _25._tcp.<mx> shows what DNS publishes, and openssl s_client -starttls smtp piped through the SPKI pipeline hashes what the server actually serves. If the hex differs, the record has drifted. A DANE checker performs the same comparison in one pass.

Why does OpenSSL print (stdin)= before my TLSA hash?

openssl dgst labels piped input — older releases print (stdin)=, OpenSSL 3.x prints SHA2-256(stdin)=. Pasting that label into a zone file corrupts the record. Add -binary and pipe through hexdump -ve '/1 "%02x"' to emit the bare 64-character hex digest with no prefix or trailing newline.

What does Microsoft 365 error 4.7.323 tlsa-invalid mean?

Per Microsoft Learn, 4.7.323 can only be generated after a DNSSEC-authentic TLSA record was returned — so DNS and DNSSEC are fine, but the destination certificate does not match the authentic record. Recompute the SPKI hash from the live certificate and republish the TLSA record.

Can I fix a TLSA mismatch without waiting for the TTL?

Yes. Add a second TLSA record whose hash matches the certificate the server currently serves, and keep the old record in place. Adding a matching record validates as soon as resolvers fetch the RRset, because DANE passes when any published record matches. Replacing forces you to wait out cached copies.

Is my problem the TLSA record or DNSSEC?

Run dig +dnssec on the TLSA name against a validating resolver. No ad flag means DNSSEC is broken and senders discard the record unauthenticated (RFC 7672 §2.2). In TLS-RPT reports, tlsa-invalid points at the certificate; dnssec-invalid points at the DNS chain.

Conclusion

A DANE TLSA 3 1 1 mismatch is a diff, and diffs are debuggable: compute the served SPKI hash with OpenSSL, compare it against the published record, and let the evidence pick one of the five root causes. Fix it by adding — not replacing — a matching record, then make the fix permanent with key reuse or a current + next rollover per RFC 7671, and put TLS-RPT plus a scheduled DANE check in place so the next renewal is a non-event.

And when you want the comparison running every day instead of every incident: start monitoring your DMARC reports — free plan, no credit card.