Postfix and Dovecot Misconfigurations That Will Bite You in 2026
The CVE history (post 6) gets the headlines. The reality of compromise on real-world mail servers, though, is dominated by something quieter: operator error. Misconfigurations don't have CVE numbers. They don't show up in patch notes. They sit in your
main.cfThis post walks through eight misconfigurations that I keep seeing in tutorial-installed mail servers in 2026. Each one comes with the exact config lines that fix it. None of them are exotic — every single one would be caught by the pen-test checklist in post 8 — but each one has shipped to production, often for years, before anyone noticed.
1. Open relay through misordered restrictions
Already covered in post 6 as a CVE pattern, but it's so common as a misconfiguration that it's worth repeating with the fix code.
The bug. Modern Postfix has two restriction lists:
- — evaluated first, decides "is this client allowed to relay?"
smtpd_relay_restrictions - — evaluated second, general per-recipient policy
smtpd_recipient_restrictions
If you put your relay logic in
smtpd_recipient_restrictionsOKcheck_recipient_accessreject_unauth_destinationThe fix. Use both lists, with the right things in each:
# /etc/postfix/main.cf
smtpd_relay_restrictions =
permit_mynetworks,
permit_sasl_authenticated,
reject_unauth_destination
smtpd_recipient_restrictions =
permit_mynetworks,
permit_sasl_authenticated,
reject_unauth_pipelining,
reject_unauth_destination,
reject_unverified_recipient,
reject_rbl_client zen.spamhaus.orgNote that
reject_unauth_destinationsmtpd_recipient_restrictionssmtpd_relay_restrictionsVerification. From an external IP, run:
swaks --to victim@elsewhere.com --from attacker@external.test \
--server your-mail-server:25Expected response:
554 5.7.1 Relay access denied250 OK2. STARTTLS optional on submission
The bug. Submission (port 587) is for authenticated user submission of mail. Authentication credentials must be transmitted over TLS — they're sensitive, and they're plain-text if intercepted. But the default Postfix submission config in some Debian/Ubuntu releases has historically been:
# /etc/postfix/master.cf — broken
submission inet n - y - - smtpd
-o syslog_name=postfix/submission
-o smtpd_sasl_auth_enable=yes
-o smtpd_tls_security_level=may # <-- "may" means "TLS optional"
-o smtpd_relay_restrictions=permit_sasl_authenticated,rejectsmtpd_tls_security_level = mayThe fix. Require TLS on submission, period:
submission inet n - y - - smtpd
-o syslog_name=postfix/submission
-o smtpd_sasl_auth_enable=yes
-o smtpd_tls_security_level=encrypt # <-- TLS mandatory
-o smtpd_tls_auth_only=yes # <-- no auth without TLS
-o smtpd_relay_restrictions=permit_sasl_authenticated,rejectencryptsmtpd_tls_auth_only = yesFor port 465 (SMTPS), enable
smtpd_tls_wrappermode = yes3. Plaintext IMAP auth allowed on the internet
The bug. Dovecot's default historically allowed plaintext authentication over unencrypted IMAP if you'd ever set:
disable_plaintext_auth = noThe reason this gets set: someone testing locally, or someone with a webmail server on the same host that wants to skip the TLS handshake to localhost. Then it never gets unset, and now your IMAP-on-port-143 listener is happily accepting
LOGIN user@domain passwordThe fix. Set:
# /etc/dovecot/conf.d/10-auth.conf
disable_plaintext_auth = yes
# /etc/dovecot/conf.d/10-master.conf
service imap-login {
inet_listener imap {
port = 143
}
inet_listener imaps {
port = 993
ssl = yes
}
}If a webmail server on the same host genuinely needs plaintext auth (its connection is local-only), you can scope the exception with:
remote 127.0.0.1 {
disable_plaintext_auth = no
}…and not the global setting.
Gotcha:
disable_plaintext_auth = yesLOGIN4. Weak TLS protocols and ciphers
The bug. Postfix and Dovecot configs migrated from older versions (or copied from old tutorials) often still allow TLSv1.0, TLSv1.1, or even SSLv3. They also frequently allow weak ciphers — RC4, 3DES, export-grade — that have been broken for years.
The fix. For Postfix:
# /etc/postfix/main.cf
smtpd_tls_protocols = !SSLv2,!SSLv3,!TLSv1,!TLSv1.1
smtpd_tls_mandatory_protocols = !SSLv2,!SSLv3,!TLSv1,!TLSv1.1
smtpd_tls_mandatory_ciphers = high
smtpd_tls_exclude_ciphers = aNULL, eNULL, EXPORT, DES, RC4, MD5, PSK, SRP, DSS
smtp_tls_protocols = !SSLv2,!SSLv3,!TLSv1,!TLSv1.1
smtp_tls_mandatory_protocols = !SSLv2,!SSLv3,!TLSv1,!TLSv1.1For Dovecot:
# /etc/dovecot/conf.d/10-ssl.conf
ssl_min_protocol = TLSv1.2
ssl_cipher_list = ECDHE+AESGCM:ECDHE+CHACHA20:DHE+AESGCM:!aNULL:!MD5:!DSS:!3DES
ssl_prefer_server_ciphers = yesIf your client base supports it, set
ssl_min_protocol = TLSv1.3Gotcha: Server-to-server SMTP (port 25) is a different question. Many MTAs in the wild still don't support TLS 1.2; restricting
smtp_tls_mandatory_protocolsmandatorysmtp_tls_protocolsmandatory5. SPF, DKIM, DMARC misalignment
The bug. Three misconfigurations bundled together because they almost always co-occur.
- SPF too permissive. A record like (yes, this exists in production) authorizes every IP on earth to send mail for your domain. Even
v=spf1 +all(softfail) is weaker than it should be in 2026 — receivers may treat softfail as "give it a chance," whereas~all(hardfail) means "reject."-all - DKIM signing the wrong domain. A common error: signing with while your visible
d=mail.example.comisFrom:. DMARC requires DKIM@example.comto align with thed=domain (either exactly or as a parent). Misalignment means DMARC ignores your DKIM signature.From: - DMARC stuck at .
p=noneis monitoring mode — DMARC reports come in but nothing is enforced. It's where you start, but it's not where you stop. Many organizations spin up DMARC, get DMARC reports for six months, never analyze them, and stay atp=noneforever. Spoofers love this.p=none
The fix.
; SPF — only your mail servers, hardfail everyone else
example.com. TXT "v=spf1 mx ip4:1.2.3.4 -all"
; DKIM — generated key, public part as TXT
default._domainkey.example.com. TXT "v=DKIM1; k=rsa; p=MIGfMA0...QIDAQAB"
; DMARC — start at p=none, move to p=quarantine, eventually p=reject
_dmarc.example.com. TXT "v=DMARC1; p=quarantine; rua=mailto:dmarc@example.com; aspf=s; adkim=s"Note
aspf=s; adkim=srProcess. Set up DMARC reporting (
rua=mailto:...p=quarantinep=rejectp=reject6. Header injection via web-to-mail bridges
The bug. Many web applications have a "contact us" form that constructs an email and hands it to the local Postfix. The classic vulnerable pattern is a contact form that takes a user-supplied email and sticks it directly into headers:
mail($to, $subject, $body, "From: " . $_POST['email']);If
$_POST['email']attacker@evil.com\r\nBcc: spamlist@evil.comBcc:The fix. This is a web application bug, not a Postfix bug, but it shows up at the mail layer. Defenses:
- Validate inputs. Reject any email-form input that contains CR or LF. PHP post-5.6 does some filtering but it's not sufficient.
mail() - Use a library. PHPMailer, SwiftMailer, Symfony Mailer, Nodemailer — all of them parameterize header values rather than concatenating. The same defense as parameterized SQL.
- Don't trust the form. Always set the header server-side to a fixed, known address. Treat user-supplied email only as data within the body.
From:
At the Postfix layer, you can mitigate by setting:
smtpd_data_restrictions = reject_unauth_pipelining
header_checks = pcre:/etc/postfix/header_checks…with
header_checks7. No rate limiting on auth or submission
The bug. Out-of-the-box Postfix and Dovecot don't aggressively rate-limit authentication attempts. An attacker can hammer SASL AUTH on port 587 thousands of times per second, or LOGIN on port 993, until they hit a working credential.
The fix. Multi-layered:
Postfix anvil rate limiter:
smtpd_client_connection_count_limit = 10
smtpd_client_connection_rate_limit = 30
smtpd_client_message_rate_limit = 100
smtpd_client_recipient_rate_limit = 100
smtpd_client_auth_rate_limit = 5
anvil_rate_time_unit = 60sThese set per-client (per IP) limits.
smtpd_client_auth_rate_limit = 5Dovecot login rate limit:
# /etc/dovecot/conf.d/10-master.conf
service imap-login {
client_limit = 1000
process_limit = 100
}
service auth {
client_limit = 5000
}Fail2ban for the brute-force layer. Configure jails for
dovecotpostfix-sasl# /etc/fail2ban/jail.local
[dovecot]
enabled = true
filter = dovecot
logpath = /var/log/mail.log
maxretry = 5
bantime = 3600
[postfix-sasl]
enabled = true
filter = postfix-sasl
logpath = /var/log/mail.log
maxretry = 5
bantime = 3600Three layers (Postfix anvil, Dovecot service limits, fail2ban) is appropriate — each catches what the others miss.
8. Logging that doesn't catch the attack
The bug. Default mail server logging captures what happened but rarely what shouldn't have happened. Successful logins look like a line in
mail.logmail.logThe fix. Pick a few things to alert on, not everything:
- Successful auth from an IP that's never authed before for that user
- Sudden increase in outbound message volume (a compromised account starting to spam)
- Postfix queue growing without bound (delivery failures piling up — often the first sign your IP got listed)
- Auth failures spiking from a single source (brute force in progress, even if fail2ban catches it later)
Tools:
journalctlawkrsysloggrafanalokiA minimum viable monitoring set:
# Daily summary of auth failures by source IP
grep -E "authentication failure|sasl_method=PLAIN" /var/log/mail.log | \
awk '{print $NF}' | sort | uniq -c | sort -rn | head -20
# Outbound queue size
postqueue -p | tail -n 1 # "-- N Kbytes in M Requests."
# Currently active SMTP/IMAP sessions
doveadm whoIf you can't run these manually for a week and understand what's normal for your server, you can't write good alerts for it. Spend the week first.
A sanity-check session
Run this script on your server. It's not a substitute for the post-8 checklist, but it catches the loudest of the misconfigurations above:
#!/bin/bash
echo "=== Postfix relay restrictions ==="
postconf smtpd_relay_restrictions smtpd_recipient_restrictions
echo "=== TLS settings ==="
postconf smtpd_tls_protocols smtpd_tls_mandatory_protocols \
smtp_tls_protocols smtp_tls_mandatory_protocols
echo "=== Submission requires TLS? ==="
postconf -P submission/inet/smtpd_tls_security_level
echo "=== Dovecot plaintext auth ==="
doveconf disable_plaintext_auth ssl_min_protocol auth_mechanisms
echo "=== Listen ports ==="
ss -tlnp | grep -E ':(25|110|143|465|587|993|995)'
echo "=== DNS records (replace example.com) ==="
dig +short TXT example.com # SPF
dig +short TXT _dmarc.example.com # DMARC
dig +short MX example.comIf any of those outputs make you uncomfortable, fix it. Post 8 takes this from "sanity check" to "real probe."
What I'd Tell My Past Self
The pattern across all eight of these is the same: the default settings of your distribution's package were tuned for compatibility, not security. That's a reasonable choice — Debian and Ubuntu can't ship a broken-by-default mail server because every legitimate install would file a bug report. But the consequence is that you, the operator, have to walk through every default and decide whether to keep or harden it.
This is the part of running mail that doesn't fit on a checklist. It's reading
postconf -dmain.cfPost 8 turns this into a structured probe — a checklist you can run in an hour to verify your server isn't vulnerable to any of the above. But the checklist won't replace the underlying habit of reading your own config.
Discussion
0 comments
Share your thoughts
No comments yet. Be the first to share your thoughts!