Skip to content

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_passwd just shows user:password for 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:

postconf smtpd_sasl_type smtpd_sasl_path

If it says dovecot, check Dovecot's passdb:

doveconf -n | grep -A3 '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:

doveadm pw -s [REDACTED 2026-10-11] -p 'your-generated-password'

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.