Host an OpenBSD Mail Server With Virtual Email Accounts

WIP!

Most of this site is incomplete, and the current state is available as an open draft. Most of the text here is likely incomplete, misinformed, or just plain wrong. I'm looking for feedback on my website, so that I can:

To anyone who wants to send me feedback, thank you, and shoot me an email!

The OpenSMTPD virtuals table enables mutliple “virtual” email accounts across various domains to be bound to a single “real” BSD user. This has a couple advantages:

  1. Each mail account doesn’t require creating a new unprivileged BSD user.
  2. The same mail server can serve multiple domains without conflating accounts with the same “user” portion of the address.

One main disadvantage is that, because OpenSMTPD and Dovecot will not read user details from the same file, there will be some level of redundancy across both services.

Preparation

This is a continuation of the parent article for hosting a mail server on OpenBSD.

This tutorial is tailored towards mail servers who will deliver mail for multiple domains, however you can just as easily follow if you only want mail for one domain.

My mail server will be called mail.example.org for this tutorial, and it will serve the domains example.org as well as websteading.net.

Setup DNS and rDNS records

Just like single-domain mail servers, each domain will need MX/SPF/DKIM/DMARC DNS keys for the Internet to tell from your sites’ address’ domains where to send mail to, which servers to trust mail from, how to check how mail came from there, and who to send reports to if anything went awry.

Create the DKIM keys

If you’ve followed the main tutorial, you already have one DKIM key. You may use the same key for all domains, but because a DKIM record has to exist for each domain, I’ll make a unique key for each domain anyways.

Last tutorial already has example.org.20220118.key, so I’ll make websteading.net.20230131.key.

Create the key(s):

$ cd /etc/mail
$ #doas mkdir -m 700 dkim
$ #doas openssl genrsa -out dkim/example.org.20220118.key 2048
$ doas openssl genrsa -out dkim/websteading.net.20230131.key 2048
$ doas chmod 400 dkim/*.key
$ doas chown -R _rspamd dkim

Create the public key(s), and keep its contents to the side. It will be the p field of your DKIM DNS record:

$ doas openssl rsa -in /etc/mail/dkim/example.org.20220118.key -pubout | grep -v ^--- | tr -d '\n'
writing RSA key
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA0wwm......TNOPwG1//++gfl/hwIDAQAB
$ doas openssl rsa -in /etc/mail/dkim/websteading.net.20230131.key -pubout | grep -v ^--- | tr -d '\n'
writing RSA key
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAqiX7......dFmToxOlXaZ1qWhtJ7taQID

Setup DNS

For each domain, add these 4 DNS records:

example.org.                    MX      "mail.example.org"    10
20220118._domainkey.example.org TXT     "v=DKIM1; k=rsa; p=MIIBIj......hwIDAQAB"
example.org.                    TXT     "v=spf1 mx -all"
_dmarc.example.org.             TXT     "v=DMARC1; p=reject; rua=mailto:reports@example.org; fo=1"

Setup rDNS

If you haven’t already, add a reverse DNS record to your server that matches your domains’ MX records. How to do this depends entirely on your ISP (or VPS provider).

Setup the vmail user

I mention virtual users will remove the need to create unix accounts, but there’s one exception: _vmail will be the one unix account that owns all users’ mail. It will be an unprivileged user, so it will have no home folder, no login shell, no password to login with, and no access to any files besides the users’ mail.

Create the user:

$ doas useradd -s /sbin/nologin -c "Virtual Mail User" _vmail

While this user will have no home, there should still be a “home” for all the mail. Create the directory and give the vmail user ownership:

$ doas mkdir -m 700 /var/vmail
$ doas chown _vmail:_vmail /var/vmail

Reconfigure Rspamd

Rspamd will need to know which DKIM keys it will need to sign for which domain. You may configure Rspamd to use the same key for all domains if you’d like. I’m going to make them use one key for each domain because I can, also since I have to add a DKIM DNS record for each domain anyways.

My /etc/rspamd/local.d/dkim_signing.conf looks like this:

allow_username_mismatch = true;
path = "/etc/mail/dkim/$domain.$selector.key"

domain {
  example.org {
    selector = "20220118"
  }

  websteading.net {
    selector = "20230131"
  }
}

Restart Rspamd

Start and check Rspamd:

$ doas rcctl restart rspamd
rspamd(ok)
$ doas rcctl check rspamd
rspamd(ok)

Reconfigure Dovecot

Dovecot’s user and password database sources will need to be rerouted from BSD’s standard passwd file and auth system to a fresh file dedicated to email users. Change the userdb and passdb sections, and auth_username_format, in /etc/dovecot/dovecot.conf:

auth_username_format = %Lu
userdb {
        driver = passwd-file
        args = /etc/mail/imap.passwd
        override_fields = uid=_vmail gid=_vmail home=/var/vmail/%u
}

passdb {
        drivr = passwd-file
        args = /etc/mail/imap.passwd
}

Create the new passwd file

The file should be read-writable only by Dovecot:

$ doas touch /etc/mail/imap.passwd
$ doas chmod 600 /etc/mail/imap.passwd
$ doas chown _dovecot /etc/mail/imap.passwd

Restart Dovecot

Check and restart Dovecot:

$ doas doveconf -n
$ doas rcctl restart dovecot

Reconfigure OpenSMTPD

To support virtual users, OpenSMTPD needs a new file that maps them to real users, and another file that maps each virtual users with the password they need to authenticate.

Create both files and limit their access only to _smtpd:

$ doas touch /etc/mail/{virtuals,credentials}
$ doas chown _smtpd /etc/mail/{virtuals,credentials}
$ doas chmod 700 /etc/mail/{virtuals,credentials}

Open /etc/mail/smtpd.conf. Alongside the <virtuals> table, add in the table for virtual users, credentials, and domains you wish to serve:

table aliases file:/etc/mail/aliases
table virtuals file:/etc/mail/virtuals
table credentials file:/etc/mail/credentials
table domains { websteading.net, example.org }

Move down to the Inbound Mail rules, and replace the action "inbound" and match directives with:

#action "inbound" maildir "~/Mail/Inbox" alias 
#match from any for local action "inbound"

# The format specifier %{dest:lowercase|strip} would transform
# an address like "Someone+tag@EXAMPLE.COM" to "someone@example.com"
# (the tag is stripped and is sent into th user's mail folder).
action "inbound" maildir "/var/vmail/%{dest:lowercase|strip}/Mail/Inbox" virtual 
match from any for domain  action "inbound"

Scroll further down to the Outbound Mail rules, and replace the auth option in the listen directives with auth <credentials>:

#listen on egress	port msa tls-require pki mail.example.org auth filter "rspamd"
#listen on egress	smtps pki mail.example.org auth filter "rspamd"

listen on egress	port msa tls-require pki mail.example.org auth  filter "rspamd"
listen on egress	smtps pki mail.example.org auth  filter "rspamd"

Finally, in between inbound mail and outbound mail, add a Local Mail section to ensure real users can still send local mail to each other.

Move the listen on socket directive up to this section and add one more action and match:

# ---Local Mail---

listen on socket	filter "rspamd"
action "local_mail" mbox alias <aliases>
match from local for local action "local_mail"

Restart smtpd

Check the configuration is OK, and restart smtpd:

$ doas smtpd -n
$ doas rcctl restart smtpd

Create an Email Account

With all wiring set up, it’s time to add virtual users.

Generate the password

The password is stored in two places /etc/mail/credentials and /etc/mail/imap.passwd, and there are fittingly two methods to generate passwords. Both use the multicrypt format, and so both are interchangeable with each other.

Bcrypt is a safe and adjustable password scheme for accounts, and is the default scheme OpenBSD uses for unix accounts. To genrate a bcrypt password with Dovecot:

$ doveadm pw -s crypt
Enter new password:
Retype new password:
{CRYPT}$2y$05$LTOBmYA/hKr5n8D50AtF.eAoILnUBxljpTVC100j06OSuIP8x9oX2

OpenBSD provides encrypt and smtpctl encrypt which both works in the same way:

$ smtpctl encrypt
<type in the password here>
$2b$09$dlt4zAHrfJ9Pqw0phPUbhO08PaVfi6SyMXr17UM6Zpq1surTa1L8q

Add the user to the Virtuals and Crdentials tables

Adding a user to /etc/mail/virtuals is extraordinarily simple. Add this line:

someone@example.com      _vmail

And to /etc/mail/credentials (without the Dovecot {CRYPT} phrase):

someone@example.com      $2y$05$LTOBmYA/hKr5n8D50AtF.eAoILnUBxljpTVC100j06OSuIP8x9oX2

Inform smtpd the tables are updated without restarting it:

$ doas smtpctl update table virtuals
$ doas smtpctl update table credentials

Add the user to imap.passwd

As far as Dovecot is concerned, the main structure of the passwd file should look like this:

user:{SCHEME}password::::::extra args

For example:

someone@example.org:{CRYPT}$2y$05$uqzC8dLUpKaft1xC4S7OKOzO1yAX9nlBweB.q7.qmZ27gBv7UCuhq::::::

Add the line like above, and tell Dovecot it should flush the authentication cache for the user:

$ doas doveadm auth cache flush someone@example.org

Optional: Add Well-known Aliases

There are a few well-known aliases for each domain often used for legitimate business, networking-related or service-related reasons. You may funnel all these emails to a single user, let’s say pat@example.org.

Prepend these aliases to the beginning of /etc/mail/virtuals:

# RFC2142 Well-Known Mailbox Names
#   Business
info        pat@example.org
marketing   pat@example.org
sales       pat@example.org
support     pat@example.org
#   Networking
abuse       pat@example.org
noc         pat@example.org
security    pat@example.org
#   Service
postmaster  pat@example.org
reports     pat@example.org
webmaster   pat@example.org
www         pat@example.org

Inform smtpd to update the table:

$ doas smtpctl update table virtuals

Appendix: Troubleshooting

Both smtpd and Dovecot forwards its logs to /var/log/maillog.

Issue: dovecot: imap-login: Disconnected: Connection closed (auth failed)

Double-check in /etc/dovecot/dovecot.conf that auth_username_format = %Lu, or auth_username_format is unset. Also double-check that there is no username_format option in either of the passdb/userdb args.

The format %Lu matches the full canonical email address lowercased, such as someone@example.org, while %l matches only the name part of the address, someone. %l is a common configuration setting for single-domain mail servers.