Postfix SASL Relay Auth Can Silently Share Your IMAP Mailbox Password¶
The trap¶
A common self-hosted mail setup uses Postfix as the SMTP server and Dovecot as the SASL backend for authenticating relay clients (other hosts sending mail through your mail server, e.g. cron jobs, monitoring alerts, Watchtower/Arcane notifications, Mastodon SMTP, etc.):
# /etc/postfix/main.cf
smtpd_sasl_type = dovecot
smtpd_sasl_path = private/auth
smtpd_sasl_auth_enable = yes
Dovecot's passdb is commonly a SQL lookup keyed on email address:
# dovecot-sql.conf.ext
password_query = SELECT email AS user, password FROM virtual_users WHERE email='%u'
This is the exact same passdb Dovecot uses for IMAP login. If you configure a
relay client's SASL credentials as youraddress@example.com / your-actual-password,
that is not a separate "relay credential" — it is your real mailbox password,
because both auth paths resolve through the identical SQL row.
Why this matters¶
It's easy to end up here without realizing it: you pick an existing mailbox (often your own) as the "relay user" because it's the account you already have, and the setup works immediately. Nothing in Postfix's or Dovecot's config visibly warns you that the two are now the same secret. The consequences only show up later:
- Rotating "the relay password" breaks your actual mail client. If you ever need to
rotate the relay credential (e.g. because it leaked — see the companion article on
secrets exposure), a straight rotation locks you out of IMAP everywhere that mailbox is
configured (Thunderbird, mobile mail apps,
mail-mcp-style tooling, etc.) with no warning until the next login attempt fails. - The blast radius of a relay-credential leak is much larger than "some cron jobs can't send mail." It's full mailbox access — reading mail, not just sending it.
- It's not obvious from the relay-client side. A host's
/etc/postfix/sasl_passwdjust showsuser:passwordfor an SMTP relay; nothing there tells you it doubles as an IMAP login until you trace the server's auth backend.
How to check if you're affected¶
On the mail server, look at what backs Postfix's SASL auth:
If it says dovecot, check Dovecot's passdb:
If the password_query (or equivalent for your passdb driver) is keyed only on the
username/email — with no separate table, no scope/purpose column, no distinct row for
"relay-only" accounts — then any account you use for SMTP relay auth is a full mailbox
account, full stop.
The fix: a dedicated relay-only account¶
Create a mailbox account that exists only for SMTP relay authentication — never used for IMAP login by any human or client:
INSERT INTO virtual_users (domain_id, email, password)
VALUES (<domain_id>, 'relay@yourdomain.com', '<[REDACTED 2026-10-11] hash>');
Generate the hash with Dovecot's own tool so the scheme matches your default_pass_scheme:
Then point every relay client's SASL config at this new account instead of a real mailbox:
# /etc/postfix/sasl_passwd (on each relay client)
[mailserver.example.com]:465 relay@yourdomain.com:the-relay-password
Now rotating the relay credential never touches anyone's actual mail access, and a leak of the relay password only grants send-as-relay capability — not IMAP access to a real inbox.
A quoting gotcha when scripting the hash insert over SSH¶
If you generate the hash on one host and insert it via a second ssh call using nested
double quotes, e.g.:
HASH=$(ssh mailserver "doveadm pw -s [REDACTED 2026-10-11] -p 'thepassword'")
ssh mailserver "mysql -e \"UPDATE virtual_users SET password='$HASH' WHERE email='...'\""
...the second command is broken. [REDACTED 2026-10-11] hashes are of the form
{[REDACTED 2026-10-11]}$6$salt$hash — full of $-delimited segments. Because the -e argument
above is double-quoted, the remote shell (not just the local one) will try to
expand $6, $salt, $hash, etc. as variables — most of which don't exist and silently
expand to empty strings, corrupting the hash. Dovecot then rejects the mangled value,
often reporting it as a weak/unrecognized scheme (e.g. DES-CRYPT) rather than failing
loudly with something obviously related to the real cause.
The fix is to avoid double-quoting anything containing an already-generated hash. Use a
single-quoted remote command (bash never expands $ inside single quotes) and use double
quotes for the SQL string literal itself, since MySQL accepts "..." string literals just
like '...' by default:
ssh mailserver bash -s <<EOF
mysql -D maildb -e 'UPDATE virtual_users SET password="$HASH" WHERE email="relay@yourdomain.com"'
EOF
Here the local heredoc substitutes $HASH exactly once before anything reaches the
remote host, and the single-quoted -e argument means the remote shell never tries to
re-expand the $ characters already baked into that value.
Related¶
- Fleet Backups with restic + B2
- Watchtower SMTP via Localhost Postfix Relay
- Companion article: never
SELECT *a secrets table — see the troubleshooting section.