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.
# /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# /etc/postfix/master.cf — per-service overrides (enforce here)
submission inet n - y - - smtpd
-o syslog_name=postfix/submission
-o smtpd_tls_security_level=encrypt
-o smtpd_tls_mandatory_protocols=>=TLSv1.2
-o smtpd_tls_mandatory_ciphers=high
-o tls_preempt_cipherlist=yes
-o smtpd_sasl_auth_enable=yes
smtps inet n - y - - smtpd
-o syslog_name=postfix/smtps
-o smtpd_tls_wrappermode=yes
-o smtpd_tls_security_level=encrypt
-o smtpd_tls_mandatory_protocols=>=TLSv1.2
-o smtpd_tls_mandatory_ciphers=high
-o tls_preempt_cipherlist=yes
-o smtpd_sasl_auth_enable=yesThree 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, not encrypt:
smtp_tls_security_level = dane
smtp_dns_support_level = dnssecdane 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.
# /etc/dovecot/conf.d/10-ssl.conf (2.3 syntax)
ssl = required
ssl_min_protocol = TLSv1.2
ssl_prefer_server_ciphers = yes
ssl_cipher_list = ECDHE+AESGCM:ECDHE+CHACHA20:DHE+AESGCM:!aNULL:!MD5:!DSS
ssl_cipher_suites = TLS_AES_256_GCM_SHA384:TLS_CHACHA20_POLY1305_SHA256:TLS_AES_128_GCM_SHA256
ssl_dh = </etc/dovecot/dh.pemssl_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:
openssl dhparam -out /etc/dovecot/dh.pem 2048The 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:
doveconf -n sslIf 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:
_mta-sts.evilmail.pro. IN TXT "v=STSv1; id=20260704T120000;"# https://mta-sts.evilmail.pro/.well-known/mta-sts.txt
version: STSv1
mode: enforce
mx: mail.evilmail.pro
max_age: 604800The 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:
_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:
_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.
# 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 sslA healthy submission session logs like this:
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.cfport 25 stayssmtpd_tls_security_level = may— never raise the MX floor- submission + smtps enforce
encryptandmandatory_ciphers=highviamaster.cf -ooverrides tls_preempt_cipherlist=yeson submission/smtps only, never on 25- Dovecot:
ssl_cipher_suitesset (the 1.3 knob) *and*ssl_cipher_list(the 1.2 knob) /etc/dovecot/dh.pempresent; confirmed withdoveconf -n ssl- MTA-STS
mode: enforcelive, policy file reachable over HTTPS on 443
Ways this bites you later
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.


