TLS-Only SMTP AUTH: Kill Plaintext Authentication Before It Leaks Your Credentials
Any MTA that answers AUTH LOGIN before the TLS handshake completes is one downgraded connection away from spilling a customer password in the clear. Here is exactly how the sniff works and the load-bearing config in Postfix, Dovecot, and Exim that makes AUTH physically impossible before TLS.
EvilMail TeamJuly 24, 202611 min read
Run this against your own submission port right now, before you read another paragraph:
If 250-AUTH PLAIN LOGIN shows up before you issue STARTTLS — or if a plain nc mail.example.com 587 followed by EHLO probe advertises AUTH on the raw socket — your server is willing to take a username and password over a channel that is not encrypted. That is not a theoretical exposure. Credential-stuffing bots probe 25 and 587 for exactly this posture before they try anything else, because a plaintext AUTH offer is the cheapest credential they will ever harvest.
The mistake underneath almost every instance of this is conceptual, not typographical: admins treat "we support TLS" and "we require TLS for login" as the same decision. They are two separate policies, and only the second one protects passwords.
The sniff, step by step — base64 is not encryption
Here is a real AUTH PLAIN exchange as it crosses the wire on an unencrypted session:
That token looks like ciphertext to a human. It is not. SASL PLAIN is authzid \0 authcid \0 passwd base64-encoded, nothing more. Anyone on-path decodes it in one command:
The ^@ are NUL separators. The login is user@example, the password is s3cret, and base64 did precisely nothing to protect either. "On-path" is not exotic: open Wi-Fi at a conference, a compromised access switch, a mis-scoped SPAN/port-mirror that a junior netops person set up for troubleshooting and forgot, a rogue transit hop, a residential router with vendor malware. Every one of those reads base64 -d fluently.
Three ports carry submission-adjacent traffic, and their historical baggage is why this keeps happening:
25 — RFC 5321 relay, MX-to-MX. Never meant for authenticated user submission; providers block it outbound precisely because of MSA-abuse legacy. AUTH here is almost always wrong.
465 — implicit ("wrapper") TLS. Revoked by IANA in the late 1990s, then formally restored by RFC 8314 (2018) as the preferred submission port. TLS starts before the first SMTP byte, so there is no plaintext phase to strip.
587 — RFC 6409 submission with STARTTLS. The channel starts in the clear and *upgrades* on request — which is the whole problem.
The vulnerability is not "no TLS available." It is "AUTH offered while TLS is still optional."
STARTTLS is strippable, so "we use STARTTLS" is not the answer
STARTTLS on 587 begins as a cleartext SMTP session and negotiates encryption mid-conversation. That negotiation happens over the same unauthenticated channel it is supposed to protect, which is the crack an on-path attacker pries open. The server sends its EHLO capabilities in the clear; the attacker rewrites the response, deleting the 250-STARTTLS line before it reaches the client:
S: 250-mail.example.com
S: 250-PIPELINING
S: 250-STARTTLS <-- attacker strips this line
S: 250-AUTH PLAIN LOGIN
S: 250 8BITMIME
A client that is not configured for "TLS required" now sees no STARTTLS on offer, shrugs, and — if it is willing to fall back — sends AUTH PLAIN over the plaintext session. The 2021 "No Need for STARTTLS" research catalogued a long list of mail clients that did exactly this fallback, plus command-injection bugs that let an attacker prepend commands into the post-TLS buffer. You cannot fix every client in the world, and you should stop trying.
The defensive conclusion is not subtle, and it is the whole point of this piece: the server must refuse AUTH on any non-TLS session regardless of what the client attempts. If AUTH is never advertised and never accepted before the TLS handshake completes, stripping 250-STARTTLS gains the attacker nothing — the downgraded session simply cannot authenticate, and a correctly configured client either upgrades or fails loudly. Prefer implicit TLS (465) for new clients so there is no negotiation to tamper with at all.
Postfix + Dovecot: make AUTH impossible before TLS
The load-bearing line in Postfix main.cf is one directive:
ini
# main.cf — the line that actually protects credentials
smtpd_tls_auth_only = yes
That single setting tells smtpd to refuse SASL AUTH on any session where TLS is not active, on every port, no matter what the client sends. Everything else is supporting hardening:
ini
# main.cf
smtpd_tls_security_level = may # opportunistic TLS for inbound MX on :25
smtpd_tls_auth_only = yes # NEVER advertise/accept AUTH without TLS
smtpd_tls_cert_file = /etc/letsencrypt/live/mail.example.com/fullchain.pem
smtpd_tls_key_file = /etc/letsencrypt/live/mail.example.com/privkey.pem
smtpd_tls_protocols = >=TLSv1.2
smtpd_tls_mandatory_protocols = >=TLSv1.2
smtpd_tls_mandatory_ciphers = high
tls_preempt_cipherlist = yes
smtpd_tls_exclude_ciphers = aNULL, eNULL, RC4, 3DES, MD5, DES
Then the per-service overrides in master.cf. Port 25 keeps opportunistic TLS and no user AUTH; 587 requires TLS via STARTTLS; 465 wraps the whole session in TLS from byte zero:
ini
# master.cf
submission inet n - y - - smtpd
-o syslog_name=postfix/submission
-o smtpd_tls_security_level=encrypt
-o smtpd_tls_auth_only=yes
-o smtpd_sasl_auth_enable=yes
-o smtpd_client_restrictions=permit_sasl_authenticated,reject
smtps inet n - y - - smtpd
-o syslog_name=postfix/smtps
-o smtpd_tls_wrappermode=yes
-o smtpd_tls_auth_only=yes
-o smtpd_sasl_auth_enable=yes
-o smtpd_client_restrictions=permit_sasl_authenticated,reject
smtpd_tls_security_level=encrypt on 587 hard-requires STARTTLS before the conversation can proceed; smtpd_tls_wrappermode=yes on 465 is implicit TLS. With smtpd_tls_auth_only=yes set both globally and per-service, no code path offers AUTH in the clear.
Now Dovecot, which is where the SASL backend actually validates the password. In 10-auth.conf and 10-ssl.conf:
disable_plaintext_auth = yes is Dovecot's equivalent of the Postfix line: PLAIN and LOGIN are refused unless the connection is already encrypted (or looping back on localhost, which Postfix's AUTH proxy does). And to head off a recurring objection — you do not need CRAM-MD5 or DIGEST-MD5. PLAIN and LOGIN are completely fine *once the channel is TLS*, because TLS is doing the confidentiality work. The challenge-response mechanisms exist to avoid sending the password in the clear; inside a TLS tunnel that problem is already solved. Worse, CRAM-MD5 requires the server to hold a reversible copy of the password to compute the challenge, which breaks the moment you store hashes.
That last point is not academic for this stack. On evilmail's infrastructure the Dovecot passdb scheme is PLAIN (uid/gid 5000) — passwords sit on disk in plaintext-equivalent form. When the stored secret is already at rest in the clear, the *only* barrier between a customer credential and an attacker is wire encryption. TLS-only AUTH is not a hardening nicety here; it is the entire security boundary.
Exim and the submission-only pattern
Exim shops express the same rule differently: advertise AUTH only when a cipher is negotiated on the inbound connection. Gate the advertisement with auth_advertise_hosts and guard the authenticator itself on tls_in_cipher:
server_advertise_condition = ${if def:tls_in_cipher} is the Exim analogue of smtpd_tls_auth_only. The general principle is MTA-agnostic: advertise and accept AUTH only when the TLS cipher variable is set. For 2026, follow RFC 8314 posture — steer new clients to 465 implicit TLS, keep 587 alive for legacy clients but still TLS-gated, and stop advertising 587-with-optional-TLS as a supported configuration.
TLS floor: versions, ciphers, and the cert
Encryption you can trivially downgrade is not much better than none. Concrete floor values as of 2026:
`smtpd_tls_mandatory_ciphers = high` and exclude aNULL, eNULL, RC4, 3DES, MD5, DES. Anonymous suites defeat the entire point; RC4 and 3DES are museum pieces.
One cert chain. Use the same Let's Encrypt (or commercial) chain your web frontend serves; on 465, SNI works normally, so a single cert covering mail.example.com is enough.
One scope guardrail so nobody conflates two different problems: DANE (TLSA records, RFC 7672) and MTA-STS (RFC 8461) secure inbound MX-to-MX server transport. They are a separate concern from submission AUTH — worth deploying, but not what stops a customer's password from leaking on port 587. Keep the two mental models apart.
Prove it from the wire
Config you have not verified is config you are guessing about. Check each port:
bash
# 587 — AUTH must be ABSENT before STARTTLS, PRESENT after
openssl s_client -starttls smtp -connect mail.example.com:587 -quiet
EHLO probe # look: no 250-AUTH here
# 465 — implicit TLS, tunnel comes up before any SMTP
openssl s_client -connect mail.example.com:465 -quiet
EHLO probe # 250-AUTH appears only inside TLS
# enumerate what every port advertises
nmap --script smtp-commands -p25,465,587 mail.example.com
# positive/negative with swaks — the negative MUST fail
swaks --server mail.example.com:587 -tls --auth PLAIN \
--auth-user [email protected] --auth-password 's3cret' # succeeds
swaks --server mail.example.com:587 --auth PLAIN \
--auth-user [email protected] --auth-password 's3cret' # must fail:
# no AUTH advertised pre-TLS
# eyeball the raw socket — no base64 blob may cross in the clear
sudo tcpdump -A -i any 'port 587 and tcp[13] & 8 != 0'
If the no--tls swaks run authenticates, stop and fix the config — that is your credential-exposure window, live.
Go-live checklist
smtpd_tls_auth_only = yes in main.cfand repeated per-service in master.cf.
disable_plaintext_auth = yes and ssl = required in Dovecot.
Port 465 in smtpd_tls_wrappermode=yes; port 587 at smtpd_tls_security_level=encrypt.
auth_mechanisms = plain login — no CRAM-MD5/DIGEST-MD5.
AUTH advertised only post-TLS on every listening port (Exim: server_advertise_condition = ${if def:tls_in_cipher}).
Verified with openssl s_client (both ports), nmap --script smtp-commands, and the swaks positive/negative pair.
Log audit: grep the Postfix log for any accepted AUTH on a session where TLS was not negotiated. Every hit is a leaked credential — rotate any password that could have transited a pre-fix plaintext session.
The rule fits in one sentence: authentication and encryption are two decisions, and the server — not the client — must be the one that refuses to combine login with plaintext. Wire that in once, and STARTTLS stripping, credential-stuffing probes, and the rogue port-mirror all hit the same 530 Must issue STARTTLS first wall.