Back to articles
web servers
16 min readApril 14, 2026

Dovecot Architecture: From Login to Maildir

Dovecot won the IMAP server wars in the 2010s for a reason — its architecture treats every login as untrusted code execution waiting to happen. Here's how it's actually structured, with the C source as the reference.

Dovecot Architecture: From Login to Maildir

Dovecot Architecture: From Login to Maildir

Dovecot is the IMAP server most of the internet has settled on. Cyrus is still around, Courier is mostly historical, and the various proprietary IMAP servers (Microsoft Exchange aside) never really challenged it in the open-source world. There's a reason Dovecot won, and that reason is its architecture — specifically, the way it handles the moment a user logs in.

This is the third post in the email security series. Post 2 covered Postfix's process layout; this one does the same for Dovecot, with the C source from

dovecot.org/releases
as the primary reference. I'll be talking about Dovecot 2.3 throughout, which has been the long-running stable line. Most of this applies to 2.4 with the same shape but slightly different file paths.

Why Dovecot exists at all

In the early 2000s, IMAP servers had a reputation for being slow, fragile, and routinely exploited. Cyrus had complex internal databases and a setup that was famously difficult. Courier was simpler but performed badly under load. Both had their share of pre-auth bugs.

Timo Sirainen started Dovecot with two design goals: it had to be fast (specifically, it had to handle massive concurrent connections without breaking), and it had to be safe by default in a way that survived its own bugs. The architecture reflects both.

The process model

Run

ps auxf
on a working Dovecot install and you'll see:

dovecot/anvil               # connection counter / rate limiter
dovecot/log                 # central log handler
dovecot/config              # config server
dovecot/auth                # authentication daemon (privileged enough to read passwords)
dovecot/auth -w             # auth worker (drops further down)
dovecot/imap-login          # pre-auth IMAP listener (heavily restricted)
dovecot/pop3-login          # pre-auth POP3 listener (heavily restricted)
dovecot/imap   user@dom     # authenticated session, runs AS the user
dovecot/pop3   user@dom     # same, for POP3
dovecot/lmtp                # local delivery from Postfix
dovecot/lda                 # local delivery via pipe (legacy)
dovecot/indexer             # background mailbox indexing
dovecot/indexer-worker
dovecot/stats               # metrics collector

The shape is similar to Postfix but with a critical difference: the post-auth IMAP and POP3 processes run as the authenticated user. That's not a coincidence — it's the central security argument.

The privilege drop on login

This is the single most important thing to understand about Dovecot's security model. When a connection comes in on port 143 or 993:

  1. imap-login
    (running as
    dovenull
    , in a chroot, with all capabilities dropped) accepts the connection.
  2. imap-login
    does the TLS handshake and reads the IMAP command stream.
  3. When it sees
    LOGIN
    or
    AUTHENTICATE
    , it forwards the credentials over a Unix socket to
    auth
    .
  4. auth
    checks the credentials against whatever passdb is configured (PAM, LDAP, SQL, passwd-file).
  5. If auth succeeds,
    auth
    returns the user's UID, GID, and home directory.
  6. imap-login
    hands the now-authenticated connection to
    master
    , which forks an
    imap
    process.
  7. The
    imap
    process setuids to the authenticated user and only then opens the user's mailbox.

The implication: a bug in

imap-login
(the only Dovecot process that ever sees pre-auth network traffic from the internet) cannot escape the
dovenull
user, the chroot, or the dropped capabilities. A bug in the post-auth
imap
process can only damage the data of one user — the user the process is running as.

A note on virtual users. The "runs as the authenticated user" model assumes each mail user is a real system account (a passwd/UID). Most modern self-hosted setups use virtual users instead — every mailbox lives under a single dedicated UID like

vmail
(5000:5000), with the IMAP process setuid'ing to
vmail
regardless of which user logged in. The privilege drop still happens (and the chroot still applies), but the per-user blast-radius isolation is weaker — a post-auth bug in one user's
imap
process runs with the same UID as every other user's
imap
process, so a filesystem race or memory leak across sessions could in principle leak between accounts. In practice the blast radius is still small (each session is a separate forked process), but the boundary is "the vmail UID can read all mail" rather than "user A can only read user A's mail." Worth knowing which mode you're in: check
userdb
in your Dovecot config.

You can read this in

src/login-common/main.c
and
src/login-common/client.c
. The privilege drops are explicit:

c
/* Conceptual — see src/login-common/main.c for the real code */
restrict_access_by_env(home, TRUE);   /* drop UID, chroot, etc. */
restrict_process_size(process_size_limit);

restrict_access_by_env
is in
src/lib/restrict-access.c
— it's where the
setuid
,
setgid
,
chroot
,
setrlimit
, and
prctl(PR_SET_NO_NEW_PRIVS)
calls happen. Reading that file is a useful exercise in "what does it actually take to drop privileges safely on Linux."

The auth subsystem

dovecot/auth
is the most interesting daemon from a security standpoint because it's the one that has to read sensitive material — password hashes, LDAP service credentials, SQL connection strings.

The configuration distinguishes:

  • passdb — the source of password verification (e.g.,
    passwd-file
    ,
    pam
    ,
    ldap
    ,
    sql
    )
  • userdb — the source of user metadata (UID, GID, home directory, mail location)

These can be the same source or different. A common setup is "passdb = LDAP" and "userdb = static" so user metadata comes from a hardcoded template.

Auth runs as

root
so it can read shadow-style files and bind to LDAP, but it forks auth-worker processes that drop privileges before doing the actual cryptographic work. So the long-running auth daemon doesn't itself do the password comparisons — it dispatches to short-lived workers.

The auth socket protocol is simple and is documented in

src/auth/auth-master-connection.c
. Login processes connect to
/var/run/dovecot/auth-login
, send a request like:

AUTH 1 PLAIN service=imap secured nologin lip=10.0.0.1 rip=1.2.3.4
CONT 1 base64-encoded-credentials

…and receive

OK 1 user=foo
or
FAIL 1
. The protocol is line-based and trivial to debug — you can
socat
to the socket and speak it manually if you're brave.

A live IMAP session, traced

The most useful thing you can do once is open an IMAP session by hand, no client. It tells you exactly what Dovecot expects.

bash
openssl s_client -connect mail.example.com:993 -crlf

Once connected:

* OK [CAPABILITY ...] Dovecot ready.
a1 LOGIN me@example.com mypassword
a1 OK [CAPABILITY ...] Logged in
a2 LIST "" "*"
* LIST (\HasNoChildren) "/" INBOX
* LIST (\HasNoChildren) "/" Sent
a2 OK List completed.
a3 SELECT INBOX
* 12 EXISTS
* 0 RECENT
* OK [UNSEEN 7] First unseen.
a3 OK [READ-WRITE] Select completed.
a4 FETCH 1 (BODY[HEADER])
* 1 FETCH (BODY[HEADER] {537}
From: ...
Subject: ...
)
a4 OK Fetch completed.
a5 LOGOUT

Run that in one terminal, and in another:

bash
sudo doveadm log find /
sudo strace -f -p $(pidof imap-login) -e trace=read,write

You'll see the login transition in the logs:

imap-login: Login: user=<me@example.com>, method=PLAIN, rip=10.0.0.5, ...
imap(me@example.com)<23456><AbCdEf>: Connected

Note the format:

imap(user)<pid><session-id>
. Every Dovecot log line for an authenticated session includes the session ID, which is your equivalent of Postfix's queue ID — grep on it and you have the full session history.

Index files (and why they matter for security)

Dovecot doesn't just store messages. It maintains per-mailbox indexes that record the message UID, flags, and a partial cache of headers so that LIST/STATUS/FETCH operations don't have to re-read every message file every time.

The index files live next to the mail (or in a dedicated index directory if

mail_location
configures it that way):

~/Maildir/
├── cur/
├── new/
├── tmp/
├── dovecot.index
├── dovecot.index.log
├── dovecot.index.cache
└── dovecot-uidlist

Two security-relevant points about these:

  1. Corruption. A corrupt index file can cause Dovecot to mis-deliver, lose flag state, or in older versions, crash. Dovecot is good at detecting and rebuilding corrupt indexes, but if you have an attacker who can write to the user's home directory through some other vector, they can prepare a malformed index that exercises parser bugs. Several Dovecot CVEs over the years have lived in index parsing.
  2. Information leakage. The index cache stores partial header data. If your mailbox is on shared storage (NFS, S3-backed cluster filesystem) and permissions slip, the index files leak almost as much information as the messages themselves.

Attack surface map

| Process | Network exposure | Privileges | Untrusted input | |---------|-----------------|------------|-----------------| | anvil, log, config | None | Limited | Internal IPC | | auth | None directly | Root (drops on workers) | Auth socket protocol | | imap-login, pop3-login | Yes — internet | dovenull, chroot, no caps | Full IMAP/POP3 + TLS | | imap, pop3 (post-auth) | Yes — but authenticated | Authenticated user | IMAP/POP3 commands | | lmtp | Yes — usually local Postfix | postfix or vmail | LMTP from MTA | | indexer, indexer-worker | None | vmail | Index files |

The bright line:

imap-login
and
pop3-login
are the only processes that touch unauthenticated internet traffic.
Every CVE worth caring about either lives there or in
auth
(which is reachable through the login processes). The post-auth
imap
process matters too, because a malicious authenticated user can still attack it, but the blast radius is constrained to that user's data.

Common Dovecot misconfigurations

disable_plaintext_auth = no
on a non-localhost listener. This allows the IMAP
LOGIN
command to send credentials in plaintext over an unencrypted connection. The default is
yes
for a reason — turn this off only on localhost-only listeners (think webmail behind a reverse proxy).

auth_mechanisms = plain login
. PLAIN and LOGIN both transmit credentials in cleartext (relying on TLS to protect them). That's fine if TLS is enforced. If you've also set
disable_plaintext_auth = no
, you've now configured Dovecot to accept passwords over plaintext IMAP. This is one of the most common exploitable misconfigs in tutorial-installed mail servers.

Weak

ssl_min_protocol
. Dovecot 2.3 defaults to
TLSv1.2
in current builds, but configs migrated from older installs may still allow
TLSv1
or even
SSLv3
. Set
ssl_min_protocol = TLSv1.2
minimum, and consider
TLSv1.3
if your client base supports it.

mail_location
with a path the auth user can manipulate. If
mail_location = maildir:~/Maildir
and the user can change their own home directory via some other channel, they can redirect Dovecot to read or write somewhere unexpected. Use
mail_location = maildir:/var/vmail/%d/%n
with a fixed prefix that the user cannot influence.

master_users
granting unintended impersonation. Dovecot's master-user feature lets one account log in as another (useful for migration tools and webmail). Anyone listed in
master_users
can read every other user's mail. The number of times I've seen migration credentials left in this list after migration completes is too high.

Gotcha: Dovecot's

auth_verbose_passwords
setting can be set to
plain
for debugging. Don't leave it on in production. It writes failed-login passwords to the log in cleartext, which means your log files now contain attacker-supplied password attempts (and, occasionally, real users who typed their password into the wrong field). Either way, it's a bad day if those logs leak.

What this gives you

Posts 2 and 3 together should leave you able to:

  • Read Postfix and Dovecot log lines and understand which daemon produced each
  • Reason about whether a given CVE is pre-auth, post-auth, or config-only — and therefore how urgent your patch should be
  • Trace a complete user session from "TCP SYN on port 993" through "message bytes in RAM" by reading just the right strace and log output
  • Identify the boundaries that a successful exploit would need to cross to escalate

Post 4 takes a different angle: instead of looking at processes, we open Wireshark and look at the actual bytes on the wire. SMTP, IMAP, and POP3 are old, simple, text-based protocols, and there's a particular kind of clarity you only get from staring at the hex dump.

Discussion

0 comments

Share your thoughts

No comments yet. Be the first to share your thoughts!