Shore Up
A doorman comparing a visitor's fingerprint against a fingerprint card pulled from a public filing drawer, nodding only when the two match exactly
LinuxMailSecurity

Automate TLSA Record Generation for DANE and Verify It

Ketan Aagja8 min read
No ratings yet

Before you run this

The script below reads an X.509 certificate and prints a TLSA record. That is all it does: it computes a SHA-256 hash of the certificate's public key and formats it as a DNS TLSA resource record you can paste into your zone. It changes nothing on the machine, and it needs no elevated privileges — except that if your certificate file is only readable by root you will need sudo to read it.

The danger is not the script; it is what you do with its output. Publishing a wrong TLSA record silently breaks inbound mail from every sending server that enforces DANE. Those senders will refuse to deliver rather than fall back to plaintext, and you will not see a bounce on your side — the mail just doesn't arrive. A TLSA record is only as reversible as your zone's TTL: once a bad record is cached, senders honour it until it expires.

So treat this as a change to production mail delivery:

  • Test first on a scratch record. Publish under a test hostname with a low TTL, verify it, and only then publish for your real MX.
  • DANE requires DNSSEC. If your zone is not signed and served with a valid chain, TLSA records are ignored at best and cause failures at worst. Confirm your zone validates before you rely on any of this.
  • Back up the zone file (or export the zone from your DNS provider) before editing it, so you can restore the exact prior state.
  • Roll records before you rotate certificates, not after — the ordering matters and is covered below.

I assume Debian 12 / Ubuntu 22.04, OpenSSL 3.x, a Postfix SMTP server on port 25 as the DANE-protected service, and a zone that is already signed with DNSSEC (whether by BIND, knot, or a provider that supports it). If you run RHEL/Alma the OpenSSL commands are identical; only package names differ.

What a TLSA record actually says

A TLSA record binds a certificate to a service name in DNS. The owner name encodes port and protocol, and the RDATA has three numeric fields plus the hash:

_25._tcp.mail.example.com. IN TLSA <usage> <selector> <matching-type> <data>
  • Usage3 (DANE-EE, the server's own leaf certificate) or 2 (DANE-TA, a trust anchor / issuer). For SMTP, 3 is the common, low-maintenance choice.
  • Selector1 (hash the SubjectPublicKeyInfo, i.e. the public key) or 0 (hash the full certificate).
  • Matching type1 (SHA-256) or 2 (SHA-512).

The mainstream recommendation for SMTP DANE (RFC 7672) is 3 1 1: the leaf certificate's public key, hashed with SHA-256. Its big advantage is that if you keep the same key across certificate renewals, the record never changes. 2 1 1 (hashing your issuer / intermediate) also exists and some operators publish both for resilience; I'll stick to 3 1 1 here.

Generate the record

Save this as gen-tlsa.sh and make it executable. It is read-only.

#!/usr/bin/env bash
set -euo pipefail

# Generate a "3 1 1" TLSA record (DANE-EE / SPKI / SHA-256)
# from a PEM certificate — the standard choice for SMTP DANE.
#
# Usage: ./gen-tlsa.sh /path/to/cert.pem mail.example.com 25 tcp

CERT="${1:?path to PEM certificate}"
HOST="${2:?hostname, e.g. mail.example.com}"
PORT="${3:-25}"
PROTO="${4:-tcp}"

# Extract the public key, render it as DER, hash it with SHA-256.
# awk '{print $NF}' takes the hex digest from OpenSSL's output line.
HASH=$(openssl x509 -in "$CERT" -noout -pubkey \
  | openssl pkey -pubin -outform DER \
  | openssl dgst -sha256 \
  | awk '{print $NF}')

echo "_${PORT}._${PROTO}.${HOST}. IN TLSA 3 1 1 ${HASH}"

Run it against the certificate Postfix actually serves — the leaf certificate, not the private key:

./gen-tlsa.sh /etc/postfix/tls/mail.example.com.crt mail.example.com 25 tcp

Replace the path, hostname, port, and protocol with your own. The output is one line ready to paste into your zone, for example:

_25._tcp.mail.example.com. IN TLSA 3 1 1 abc123...  ; example hash, not real

If you prefer a purpose-built tool over the OpenSSL pipeline, both hash-slinger (its tlsa command) and GnuTLS danetool (danetool --tlsa-rr) generate and verify TLSA records. Their usage/selector defaults and flags differ between versions, so check the respective man pages for exact syntax before trusting the output — getting the usage field wrong is exactly the kind of mistake that breaks delivery.

Publish it in DNS

Add the record to your DNSSEC-signed zone and re-sign / reload. With BIND and inline signing you typically edit the unsigned zone and let named re-sign; with a provider, add a TLSA record type through their panel. Set a short TTL (say 300–3600) while you are establishing the record so mistakes clear quickly. You can lengthen it once things are stable.

Publish for every MX host a sender might contact, and make sure the owner name, port, and protocol match the service. A TLSA record on the wrong name is invisible; a TLSA record with the wrong hash is a delivery outage.

Verify it

Do these three checks in order. Don't skip the DNSSEC one.

1. Confirm the record is present and DNSSEC-validated. Look for the ad (Authenticated Data) flag:

dig +dnssec TLSA _25._tcp.mail.example.com @1.1.1.1

You want to see your TLSA record in the ANSWER section and ad in the flags line. No ad flag means DNSSEC isn't validating for that name, and DANE will not work.

2. Confirm the served certificate matches. Fetch what Postfix presents and recompute the hash, then compare it byte-for-byte with what you published:

openssl s_client -connect mail.example.com:25 -starttls smtp </dev/null 2>/dev/null \
  | openssl x509 -pubkey -noout \
  | openssl pkey -pubin -outform DER \
  | openssl dgst -sha256 \
  | awk '{print $NF}'

That hex string must equal the data field in your published 3 1 1 record. If it differs, the certificate on the server isn't the one you generated the record from.

3. Do a real DANE handshake. Postfix ships posttls-finger, which performs an actual DANE-authenticated connection and tells you whether the TLSA match succeeded. It reads TLSA records itself and needs a validating resolver on the host. The security-level option that turns on DANE is documented in man posttls-finger (the level values include dane and dane-only); confirm the exact flag there for your Postfix version, then run it against your MX host. A successful run reports that the certificate was verified via TLSA. dane-only is the strict form — it fails rather than falling back, which is what you want when testing.

For an independent second opinion, run your domain through the mail test at internet.nl, which checks DANE for your MX and flags mismatches. It's a good sanity check that senders in the wild will see what you expect.

Rotating certificates without breaking mail

This is the part that bites people. Because DANE senders enforce the hash you published, you must publish the new record before the server starts serving the new certificate:

  1. Generate the TLSA record for the new certificate with the script above.
  2. Publish it alongside the existing record. Both are valid simultaneously.
  3. Wait at least the record's TTL (plus your DNSSEC signing/propagation time) so caches hold both.
  4. Deploy the new certificate on Postfix and reload.
  5. Verify with the checks above.
  6. After another TTL, remove the old record.

You can sidestep most of this by keeping the same key across renewals. With Let's Encrypt/certbot, reusing the key means the 3 1 1 SPKI hash never changes and your published record stays valid across renewals — check the certbot docs for the current key-reuse option rather than assuming a flag name.

Undo

TLSA records are ordinary DNS records, so rolling back is just removing them:

  1. Delete the TLSA record(s) from the zone.
  2. Re-sign and reload (or delete via your provider's panel).
  3. Confirm removal: dig TLSA _25._tcp.mail.example.com should return no answer.

Because senders may have cached the record, keep in mind that removal takes effect only after the TTL expires. If you published a bad record, the fastest safe recovery is to publish the correct record (with the low TTL you set earlier) rather than waiting on a delete — a correct record fixes delivery immediately for anyone querying fresh, while a delete just returns to no-DANE once caches clear.

Written by
Ketan Aagja

Runs enterprise networks and security for a living, and writes Shore Up to turn two decades of hands-on Linux, Windows and mail-server work into guides you can actually use.

More about the author →

Was this article helpful?

Tap a star — no sign-in needed.

Be the first to rate this article.

Correlate Fail2ban, Postfix and Dovecot Logs Into One Report

This is a read-only reporting script . It reads your Postfix/Dovecot mail log and your Fail2ban log, extracts the source IPs behind SMTP SASL failures, Postfix rejects and Dovecot auth failures, cross-references them against the IPs Fail2ban actually banned, and prints one ranked summary. It does not touch your firewall, your jails, your mail queue, or any config. There is nothing to undo except deleting the report file and removing the cron entry you add at the end.

10 min read

Automatically Ban Abusive IPs in Postfix with Fail2ban

The standard, boring way to block IPs that hammer your mail server is Fail2ban. It watches the mail log, counts matching failures per source IP inside a time window, and when a source crosses a threshold it inserts a firewall rule to drop that IP for a while. You could write a bash script that greps the log and pipes IPs into nft , and I'll say where that fits at the end — but reinventing Fail2ban is more error-prone than configuring it, so that's what this guide does.

7 min read

Automate DKIM Key Checks and Rotation on OpenDKIM

This guide has two parts. The check part is a read-only script that queries DNS for your published DKIM record and confirms it still matches the private key OpenDKIM signs with — safe to run any time, and safe to put on cron. The renewal part generates a new key under a new selector, has you publish a DNS record, and then switches signing over to it.

9 min read