đŸ Archived View for gemini.omarpolo.com âș post âș opensmtd-dovecot-virtual-users.gmi captured on 2022-06-03 at 22:54:14. Gemini links have been rewritten to link to archived content
âŹ ïž Previous capture (2022-01-08)
âĄïž Next capture (2023-01-29)
-=-=-=-=-=-=-
Written while listening to âRifareâ by Tre Allegri Ragazzi Morti.
Published: 2021-06-05
Tagged with:
I just switched my mailserver from a setup with a single UNIX user to a slightly more complex one with virtual users. I donât know how other admins manages their virtual users, but in this entry Iâm going to discuss the method Iâm using.
This is *not* a tutorial on how to install and configure OpenSMTPD or Dovecot or anything else, as I donât feel like Iâm the most qualified to do so. Instead, if youâre looking on how to deploy your own mail server, Iâm going to recommend the tutorial from Gilles Chehade:
Setting up a mail server with OpenSMTPD, Dovecot and Rspamd
In the past Iâve used a shared SQLite database to store users authentication data, but this time I wanted to manage the data differently. I donât need to handle hundreds of users, and every user needs to be manually added by me, so a database is overkill.
A more text-centric approach requires five configuration files:
The tables are needed to load data into OpenSMTPD, while for Dovecot a single â/etc/passwdâ-like file is enough.
Keeping the information in sync between these five files definitely not hard, but Iâm particularly lazy, so Iâve wrote a simple AWK script to parse a custom âuserdbâ file and populate all those files. But before going into that, letâs see an excerpt from my OpenSMTPD configuration:
# these are the paths on a FreeBSD host, on OpenBSD theyâre # just /etc/mail. table aliases file:/usr/local/etc/mail/aliases table domains file:/usr/local/etc/mail/domains table passwd file:/usr/local/etc/mail/passwd table virtuals file:/usr/local/etc/mail/virtuals # pki, filters and listen directives omitted action "remote_mail" lmtp "/var/run/dovecot/lmtp" rcpt-to virtual <virtuals> action "local_mail" lmtp "/var/run/dovecot/lmtp" rcpt-to alias <aliases> action "outbound" relay helo example.com match from any for domain <domains> action "remote_mail" match from local for local action "local_mail" match from any auth for any action "outbound" match for any action "outbound"
The four âmatchâ rules matches in order
Two of the three actions deliver the mail over LMTP to Dovecot. An important bit there that I was missing on my first try was the ârcpt-toâ keyword: as weâll see in a moment, all the mail are handled by a local user, but we need to use the recipient email address instead of the local user in the LMTP session, so Dovecot can save the email in the correct maildir.
Dovecot needs only a single file for the authentication. One of the supported format, and the one Iâm using, is a âpasswdâ-like format, like the following:
op@example.com:<hash>::::::
On the Dovecot site, things are a bit easier because there is no aliasing, resolving or expansions to do on the received emails.
An alias table looks like this:
root: op op: op@example.com
It maps *local* users to other local or remote users. In the example above, mail for the UNIX root user are forwarded to the user op, that in turns redirects his mail to op@example.com.
Holds all the domains weâre accepting mails from. It can be specified in-line in the configuration file:
table domains { "example.com", "foo.bar.net", ⊠}
or in a file with one domain name per line
example.com foo.bar.net
A credentials table file looks like this:
user@doma.in password-hash user2@example.com password-hash
just a simple user â hash mapping. Hashes can be computed with the encrypt subcommand of smtpctl
$ smtpctl encrypt p4ssw0rd $2b$10$jpdOj8WPIMABsMs.LzFbiuSpgZ1TlGUj2ztBxEimoaQylQD/jhelS ^D
NB: on OpenBSD-CURRENT (and as of a couple of releases already at least) the âsmtpctl encryptâ computes the BLF-CRYPT hash of the password, but for some reason on FreeBSD it uses SHA512-CRYPT. Dovecot needs to be told the default hashing scheme in âconf.d/auth-passwdfile.conf.extâ. Hereâs mine
passdb { driver = passwd-file # adjust SHA512-CRYPT eventually! args = scheme=SHA512-CRYPT username_format=%u /usr/local/etc/dovecot/users } userdb { driver = passwd-file args = username_format=%u /usr/local/etc/dovecot/users override_fields = home=/var/vmail/%d/%n }
Refer to the Dovecot documentation:
âPassword Schemesâ in the Dovecot documentation.
The virtual table is used to map address to other addresses (i.e. alias) or addresses to local users (to allow the delivery.) It looks like this
postmaster@example.com: op@example.com aaa@example.com: op@example.com op@example.com: vmail otheruser@example.com: vmail
Since maintaining this whole bunch of files may not be the easiest thing ever. To be a bit more declarative, Iâve come up with the following âuserdbâ file. Itâs an invented syntax that gets parsed by a super-simple AWK script and generates all the other files. Hereâs an example:
# local alias alias root op alias op op@example.com # per virtual-domain config example.com: # Indentation is optional, but improves legibility. # The following defines the user op@example.com; # <hash> is the hash of the password computed # with `smtpctl encrypt` user op <hash> # and define an arbitrary number of aliases alias service1 alias other-alias user otheruser <hash> # aliases can be to virtual users on other hosts alias abuse someone@example2.com example2.com: user someone <hash> # âŠ
The syntax is as simple as possible, to make the parsing easier. Itâs also open for additions: for instance, adding a âquotaâ keyword to define custom quotas shouldnât be too hard.
All the code examples are available in a git repository.
The AWK implementation that parses the file is also pretty simple:
#!/usr/bin/env awk # expects action to be defined, like -v action=aliases /^[[:space:]]*$/ { next } /^[[:space:]]*#/ { next } /:$/ { # drop the : gsub(":", "", $1); domain = $1; domains[domainslen++] = domain; next; } $1 == "user" { user = sprintf("%s@%s", $2, domain); users[user] = $3 # change âvmailâ to match the local user that # delivers the mail aliases[user] = "vmail"; next; } $1 == "alias" { if ($3 != "") { target = $3; } else { target = user; } if (domain != "") { alias = sprintf("%s@%s", $2, domain); } else { alias = $2; } aliases[alias] = target; } # output in the correct format END { if (action == "aliases") { for (alias in aliases) { if (match(alias, "@")) continue; printf("%s: %s\n", alias, aliases[alias]); } } else if (action == "virtuals") { for (alias in aliases) { if (!match(alias, "@")) continue; printf("%s %s\n", alias, aliases[alias]); } } else if (action == "domains") { for (domain in domains) { printf("%s\n", domains[domain]); } } else if (action == "users") { for (user in users) { printf("%s %s\n", user, users[user]); } } else if (action == "users.passwd") { for (user in users) { # user@doma.in:hash:::::: # user@doma.in:hash::::::userdb_quota_rule=*:storage=1G printf("%s:%s::::::\n", user, users[user]); } } else if (action == "users.mdirs") { for (user in users) { split(user, m, "@"); # adjust the maildir path printf("/var/vmail/%s/%s/Maildir\n", m[2], m[1]); } } else { print "unknown action!\n" > "/dev/stderr" exit 1 } }
The AWK script needs the variable âactionâ to be defined to dump the correct information. It can be provided with the â-vâ flag, but for extra-comfort I wrote also the following wrapper script:
#!/bin/sh if [ ! -f "userctl.awk" ]; then echo "Can't find userctl.awk!" >&2 exit 1 fi if [ ! -f "userdb" ]; then echo "Can't find userdb!" >&2 exit 1 fi # run <action> run() { awk -f userctl.awk -v action="$1" userdb } case "$1" in aliases) run "aliases" ;; virtuals) run "virtuals" ;; domains) run "domains" ;; users) run "users" ;; users.passwd) run "users.passwd" ;; users.mdirs) run "users.mdirs" ;; help) echo "USAGE: $0 <action>" echo "where action is one of" echo " - aliases" echo " - virtuals" echo " - domains" echo " - users" echo " - users.passwd" echo " - users.mdirs" ;; *) echo "Unknown action $1" >&2 exit 1 ;; esac
Now that the framework is in place, the only missing piece is to use it to generate the files. I wrote yet another script to (re-)generate the tables and to create the maildir when a user is added.
#!/bin/sh set -e # On OpenBSD these are only /etc/mail/⊠./userctl aliases > /usr/local/etc/mail/aliases ./userctl virtuals > /usr/local/etc/mail/virtuals ./userctl domains > /usr/local/etc/mail/domains ./userctl users > /usr/local/etc/mail/passwd ./userctl users.passwd > /usr/local/etc/dovecot/users m() { if [ ! -d "$1" ]; then mkdir "$1" chown vmail:vmail "$1" fi } # ensure the maildirs exists for dir in $(./userctl users.mdirs); do homedir=$(dirname "$dir") domdir=$(dirname "$homedir") m "$domdir" m "$homedir" m "$dir" done # eventually add something like # service dovecot restart # service smtpd restart # for FreeBSD or # rcctl restart dovecot smtpd # for OpenBSD.
I donât have a proper conclusion for this entry. Tools like this are usually almost always âwork in progressâ, as they are changed/extended over the time depending on what I need to do. One thing for sure, designing simple database files and managing them with AWK is lots of fun.
As always, if you have comment, tips or noticed something thatâs missing or not explained properly, donât refrain from notifying me, so I can update this entry accordingly.
-- text: CC0 1.0; code: public domain (unless specified otherwise). No copyright here.
For comments, write at < blog at omarpolo dot com > or @op@bsd.network in the fediverse.