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/releasesWhy 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 auxfdovecot/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 collectorThe 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:
- (running as
imap-login, in a chroot, with all capabilities dropped) accepts the connection.dovenull - does the TLS handshake and reads the IMAP command stream.
imap-login - When it sees or
LOGIN, it forwards the credentials over a Unix socket toAUTHENTICATE.auth - checks the credentials against whatever passdb is configured (PAM, LDAP, SQL, passwd-file).
auth - If auth succeeds, returns the user's UID, GID, and home directory.
auth - hands the now-authenticated connection to
imap-login, which forks anmasterprocess.imap - The process setuids to the authenticated user and only then opens the user's mailbox.
imap
The implication: a bug in
imap-logindovenullimapA 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
vmailvmailimapimapuserdbYou can read this in
src/login-common/main.csrc/login-common/client.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_envsrc/lib/restrict-access.csetuidsetgidchrootsetrlimitprctl(PR_SET_NO_NEW_PRIVS)The auth subsystem
dovecot/authThe 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
rootThe auth socket protocol is simple and is documented in
src/auth/auth-master-connection.c/var/run/dovecot/auth-loginAUTH 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=fooFAIL 1socatA 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.
openssl s_client -connect mail.example.com:993 -crlfOnce 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 LOGOUTRun that in one terminal, and in another:
sudo doveadm log find /
sudo strace -f -p $(pidof imap-login) -e trace=read,writeYou'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>: ConnectedNote the format:
imap(user)<pid><session-id>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~/Maildir/
├── cur/
├── new/
├── tmp/
├── dovecot.index
├── dovecot.index.log
├── dovecot.index.cache
└── dovecot-uidlistTwo security-relevant points about these:
- 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.
- 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-loginpop3-login
authimapCommon Dovecot misconfigurations
disable_plaintext_auth = no
LOGINyesauth_mechanisms = plain login
disable_plaintext_auth = noWeak ssl_min_protocol
TLSv1.2TLSv1SSLv3ssl_min_protocol = TLSv1.2TLSv1.3mail_location
mail_location = maildir:~/Maildirmail_location = maildir:/var/vmail/%d/%nmaster_users
master_usersGotcha: Dovecot's
auth_verbose_passwordsplainWhat 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!