Back to articles
web servers
13 min readApril 25, 2026

Postfix and Dovecot Misconfigurations That Will Bite You in 2026

Most successful attacks against self-hosted mail aren't CVEs — they're config-file mistakes. Here are the eight misconfigurations that show up over and over, with the exact lines that fix each one.

Postfix and Dovecot Misconfigurations That Will Bite You in 2026

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.cf
for years and silently do the wrong thing.

This 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:

  • smtpd_relay_restrictions
    — evaluated first, decides "is this client allowed to relay?"
  • smtpd_recipient_restrictions
    — evaluated second, general per-recipient policy

If you put your relay logic in

smtpd_recipient_restrictions
(because you migrated from old Postfix, or because a tutorial was written before 2.10), an
OK
response from a
check_recipient_access
lookup can short-circuit the restriction list and bypass
reject_unauth_destination
. Result: open relay.

The 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.org

Note that

reject_unauth_destination
appears in both lists. That's intentional — defense in depth. Even if a future edit to
smtpd_recipient_restrictions
reorders something,
smtpd_relay_restrictions
catches it.

Verification. From an external IP, run:

bash
swaks --to victim@elsewhere.com --from attacker@external.test \
      --server your-mail-server:25

Expected response:

554 5.7.1 Relay access denied
. If you get
250 OK
, you have an open relay and you have minutes, not hours, to fix it.

2. 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,reject

smtpd_tls_security_level = may
means TLS is offered but not required. A client can authenticate and submit mail in plaintext if it's misconfigured to do so (or if a MITM stripped the STARTTLS offer).

The 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,reject

encrypt
means STARTTLS is required before any further commands.
smtpd_tls_auth_only = yes
ensures SASL auth mechanisms are only advertised over TLS.

For port 465 (SMTPS), enable

smtpd_tls_wrappermode = yes
instead. TLS is implicit, no STARTTLS needed.

3. 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 = no

The 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 password
in cleartext, on the open internet.

The 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 = yes
doesn't block port 143 — it just refuses to accept the
LOGIN
command until STARTTLS has been done. Clients connecting to 143 can still STARTTLS up to encrypted, then auth. Block port 143 entirely at the firewall if you want belt-and-braces.

4. 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.1

For 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 = yes

If your client base supports it, set

ssl_min_protocol = TLSv1.3
. The set of clients that don't support TLSv1.3 in 2026 is small — old Outlook installations on Windows 7 are about the only meaningful holdouts, and supporting Windows 7 is its own problem.

Gotcha: 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_protocols
too aggressively will break delivery to those servers. The
mandatory
variants only apply when TLS is mandatory (e.g., per-domain TLS policies). The base
smtp_tls_protocols
should be permissive enough for opportunistic encryption with old peers; the
mandatory
set should be strict.

5. SPF, DKIM, DMARC misalignment

The bug. Three misconfigurations bundled together because they almost always co-occur.

  • SPF too permissive. A record like
    v=spf1 +all
    (yes, this exists in production) authorizes every IP on earth to send mail for your domain. Even
    ~all
    (softfail) is weaker than it should be in 2026 — receivers may treat softfail as "give it a chance," whereas
    -all
    (hardfail) means "reject."
  • DKIM signing the wrong domain. A common error: signing with
    d=mail.example.com
    while your visible
    From:
    is
    @example.com
    . DMARC requires DKIM
    d=
    to align with the
    From:
    domain (either exactly or as a parent). Misalignment means DMARC ignores your DKIM signature.
  • DMARC stuck at
    p=none
    .
    p=none
    is 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 at
    p=none
    forever. Spoofers love this.

The fix.

dns
; 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=s
— strict alignment. The defaults (
r
for relaxed) allow subdomain sending; strict requires exact match.

Process. Set up DMARC reporting (

rua=mailto:...
), watch the reports for a few weeks to confirm legitimate senders are passing. Then move to
p=quarantine
. Watch for a few more weeks. Then
p=reject
. Don't skip the monitoring phase — you will discover legitimate senders (your CRM, your invoice system, that one cron job from a server everyone forgot about) that aren't currently aligned, and
p=reject
will block their mail.

6. 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:

php
mail($to, $subject, $body, "From: " . $_POST['email']);

If

$_POST['email']
is
attacker@evil.com\r\nBcc: spamlist@evil.com
, the attacker has injected a
Bcc:
header. Now your Postfix relay is sending mail to attacker-supplied destinations using your IP and your domain's DKIM signature.

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
    mail()
    post-5.6 does some filtering but it's not sufficient.
  • 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
    From:
    header server-side to a fixed, known address. Treat user-supplied email only as data within the body.

At the Postfix layer, you can mitigate by setting:

smtpd_data_restrictions = reject_unauth_pipelining
header_checks = pcre:/etc/postfix/header_checks

…with

header_checks
rules that reject messages with suspicious header patterns. This is defense in depth — it doesn't replace fixing the web app, but it catches mistakes.

7. 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 = 60s

These set per-client (per IP) limits.

smtpd_client_auth_rate_limit = 5
means an attacker gets 5 auth attempts per minute per source IP before further attempts are throttled.

Dovecot 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

dovecot
and
postfix-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 = 3600

Three 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.log
. Successful logins from new countries also look like a line in
mail.log
. Unless you're actively reading the logs, the attack is invisible.

The 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:

journalctl
+
awk
is enough for a small server.
rsyslog
shipping to a central log server, plus a few
grafana
panels on
loki
, scales further. Don't try to alert on everything — you'll mute the alerts within a week.

A minimum viable monitoring set:

bash
# 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 who

If 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:

bash
#!/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.com

If 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 -d
to see all the defaults, comparing against your
main.cf
, and asking "do I actually want this?" for every line. It's slow. It's the work. There's no shortcut.

Post 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!