TLS 1.3 on Postfix and Dovecot Without Breaking Inbound Mail
Cranking the cipher floor everywhere quietly makes inbound mail less secure. The correct 2026 posture: lock TLS 1.3 hard on submission and IMAP, keep port 25 deliberately permissive, and get inbound guarantees from MTA-STS, DANE and TLS-RPT instead.
EvilMail TeamJuly 23, 202611 min read
An admin sets smtpd_tls_mandatory_ciphers = high, disables everything below TLS 1.3 across the whole server, watches the SSL Labs-style scanner flip to A+, and closes the ticket. A week later a legacy sender — a government tax authority, a bank's dunning system, some appliance still linked against OpenSSL 1.0.2 — tries to hand mail to the MX, fails the cipher negotiation, and falls back to plaintext. Or worse, bounces. Mail that used to arrive weakly encrypted now arrives in the clear, and the "hardening" made confidentiality *worse*.
The ciphers were never the mistake. Applying a submission-grade policy to the MX was. Port 25 talks to strangers and has to stay opportunistic; 587, 465 and IMAP talk to your own users and should be locked to TLS 1.3. One policy per path — not one policy for the server.
Two mail paths, two TLS policies
Most hardening guides blur the distinction that decides everything: a server accepting mail from the entire internet is not the same as a server accepting connections from clients you control.
Inbound MX on port 25 is a server role under RFC 7435 opportunistic TLS. Encryption there is best-effort. If the handshake fails — wrong protocol, no common cipher — the sender does not retry with something stronger. It delivers in cleartext, because to an opportunistic sender *any TLS beats no TLS, and no TLS beats no mail*. That is the whole semantic. So smtpd_tls_security_level = may
is correct on 25, and raising the cipher floor there converts "weak but encrypted" into "plaintext." You lose.
Submission (587 STARTTLS), smtps (465 implicit TLS), and Dovecot IMAP/POP are a different animal. Both ends are yours: your users, your MUAs, your credentials crossing the wire. There is no "fall back to plaintext or lose the mail" tradeoff — a client that can't meet the bar should be *refused*, because a refused connection just means the user fixes their client, not that mail silently leaks. That path gets encrypt / required and TLS 1.3.
Postfix: lock down submission, leave the MX alone
main.cf governs the smtpd on port 25, and you keep it permissive. Everything strict lives in master.cf as per-service -o overrides. That separation is the entire architecture.
ini
# /etc/postfix/main.cf — governs port 25 (inbound MX)
smtpd_tls_security_level = may # opportunistic, do NOT raise
smtpd_tls_loglevel = 1
smtpd_tls_ciphers = medium # leave the MX floor alone
# outbound client
smtp_tls_security_level = dane
smtp_dns_support_level = dnssec
smtp_tls_mandatory_ciphers = high
# curated list used by the "high" grade on enforced services
tls_high_cipherlist = ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256
tls_eecdh_auto_curves = X25519:X448:prime256v1
Three things worth explaining. smtpd_tls_mandatory_protocols=>=TLSv1.2 uses the Postfix 3.6+ minimum/maximum syntax — clean >= / <= operators that replace the error-prone !SSLv3, !TLSv1, !TLSv1.1 exclusion lists you still see copy-pasted from 2016. Set the floor, not the blocklist.
The opinionated call: keep 1.2 as the floor on submission, prefer 1.3. Real Outlook and Thunderbird fleets in 2026 still open sessions at 1.2 depending on the OpenSSL build underneath them, and refusing them doesn't push those users to 1.3 — it pushes them off TLS and onto a support ticket. Go 1.3-only on submission *only* if you own every client (a managed fleet, an app that speaks to your API). tls_preempt_cipherlist=yes belongs on submission too — you're the authority there, so your ordering wins. It does not belong on port 25.
There's no TLS 1.3 cipher knob, and there shouldn't be. OpenSSL fixes the 1.3 suite set and Postfix exposes no parameter for it — which is fine, because all of them (TLS_AES_256_GCM_SHA384, TLS_CHACHA20_POLY1305_SHA256, TLS_AES_128_GCM_SHA256) are AEAD. There is nothing to curate.
Postfix as a sender: opportunistic DANE outbound
The symmetry most posts skip: your outbound smtp(8) client faces the same "don't break delivery" problem the MX does, just in the other direction. So you use dane, notencrypt:
ini
smtp_tls_security_level = dane
smtp_dns_support_level = dnssec
dane is opportunistic-with-teeth. When the remote publishes a valid TLSA record under a DNSSEC-signed zone, Postfix *upgrades that specific delivery to mandatory, authenticated TLS* and refuses to fall back. When the remote has no TLSA, it degrades gracefully to plain opportunistic TLS. You get authenticated encryption to everyone who opted in, without bouncing mail to everyone who didn't. It's the outbound mirror of MTA-STS.
Dovecot: TLS 1.3 for IMAP, POP and submission
Dovecot terminates the connections where your users authenticate, so it gets the strict policy. The subtlety: the TLS 1.3 suites live in a *separate setting* from the 1.2 cipher list.
ssl_cipher_list controls TLS 1.2 and below; ssl_cipher_suites (added in 2.3.11) is the TLS 1.3 knob. Set both. Generate the DH parameters once for the 1.2 DHE suites — TLS 1.3 doesn't use them, but 1.2 clients still negotiate DHE:
bash
openssl dhparam -out /etc/dovecot/dh.pem 2048
The 2026 gotcha: on Dovecot 2.4 the settings layer was reworked. ssl_dh handling changed and a few names shifted, so don't trust a 2.3 config blindly. Confirm what the daemon actually parsed:
bash
doveconf -n ssl
If you run a Dovecot submission proxy on 587, its TLS policy has to match the Postfix submission stanza above — same floor, same enforcement — so a client can't get a weaker session by picking the "other" 587 listener.
Inbound guarantees without a gate
Here's the resolution to the apparent contradiction — "keep port 25 permissive" versus "guarantee inbound is encrypted." You don't enforce on your listener. You move enforcement to the *sender* and let capable senders upgrade themselves, while the ones that can't still get through.
MTA-STS publishes a policy that says "if you're a compliant sender, refuse to deliver to me in cleartext." Gmail, Outlook and Yahoo honor it. The DNS record announces the policy; the policy itself is served over HTTPS:
dns
_mta-sts.evilmail.pro. IN TXT "v=STSv1; id=20260704T120000;"
The enforcement runs on Gmail's side. Your port 25 still says may. A compliant sender refuses to fall back to plaintext when talking to you; a legacy sender that has never heard of MTA-STS is unaffected and still delivers. You got the guarantee without owning the failure mode.
TLS-RPT gives you the telemetry — daily aggregate reports of who failed to set up TLS to you and why:
dns
_smtp._tls.evilmail.pro. IN TXT "v=TLSRPTv1; rua=mailto:[email protected]"
DANE does the same job as MTA-STS but rooted in DNSSEC instead of the web PKI — the strongest option if your zone is signed:
dns
_25._tcp.mail.evilmail.pro. IN TLSA 3 1 1 <sha256-of-SPKI>
Run MTA-STS and DANE side by side. Senders honor whichever they support, and neither one touches your smtpd_tls_security_level.
Verify it, don't trust the config
A config that parses is not a config that works. Drive real handshakes.
bash
# live handshake report from Postfix's own client
posttls-finger -c -l secure mail.evilmail.pro
# implicit TLS on 465, force 1.3
openssl s_client -connect mail.evilmail.pro:465 -tls1_3
# STARTTLS on 587, force 1.3
openssl s_client -starttls smtp -connect mail.evilmail.pro:587 -tls1_3
# full sweep
testssl.sh --starttls smtp mail.evilmail.pro:587
nmap --script ssl-enum-ciphers -p 465,587 mail.evilmail.pro
# source of truth
postconf -n | grep tls
doveconf -n ssl
A healthy submission session logs like this:
text
postfix/submission/smtpd[2841]: ... TLSv1.3 with cipher TLS_AES_256_GCM_SHA384 (256/256 bits)
The counterintuitive pass condition: port 25 must still accept plaintext and negotiate down. If openssl s_client -starttls smtp -connect mail.evilmail.pro:25 succeeds at TLS 1.2, or a plaintext session on 25 goes through, that is *correct behavior*, not a finding. The MX is supposed to be permissive. The guarantee comes from MTA-STS and DANE, not from the listener refusing people.
Ship checklist
main.cf port 25 stays smtpd_tls_security_level = may — never raise the MX floor
submission + smtps enforce encrypt and mandatory_ciphers=high via master.cf -o overrides
tls_preempt_cipherlist=yes on submission/smtps only, never on 25
Dovecot: ssl_cipher_suites set (the 1.3 knob) *and* ssl_cipher_list (the 1.2 knob)
/etc/dovecot/dh.pem present; confirmed with doveconf -n ssl
MTA-STS mode: enforce live, policy file reachable over HTTPS on 443
TLS-RPT rua mailbox exists and someone actually reads it
DANE 3 1 1 TLSA matches the SPKI of the served cert
posttls-finger -c -l secure returns a clean secure handshake
port 25 confirmed *still* permissive — that's the pass, not a bug
A 3 1 1 TLSA pins the public key, so rotating to a cert with a fresh keypair breaks DANE the instant the new cert goes live — pre-publish the next TLSA before you swap, or reuse the key. MTA-STS max_age: 604800 means a broken policy is cached by senders for a full week; drop max_age low *before* a planned MX change, not after. And an OpenSSL point-release can quietly retire a cipher your curated tls_high_cipherlist still names, so your monitoring flags a suite the library no longer offers. The operating principle survives all three: match the TLS policy to who's on the other end of the wire. Strangers get best-effort; your users get the lock.