Arch Linux mail server tutorial - part 2 - OpenSMTPD, Dovecot, DKIMproxy, and Let's Encrypt

Published on 2017-12-28. Modified on 2021-08-11.

OpenSMTPD has changed the syntax for smtpd.conf since I wrote this tutorial. I haven't had a chance to update the tutorial so please note that you need to refer to the man page for opensmtpd.conf to get it right. Specifically the accept option, which used to be a single option, has been changed into action and match in order to better suit the rest of the OpenBSD configuration style from PF. I have planed to make an updated version of the tutorial using OpenBSD when time permits.

This is the second part of the mail server tutorial in which we'll install and setup OpenSMTPD, Dovecot, DKIMproxy, and Let's Encrypt.

Setting up OpenSMTPD, Dovecot, DKIMproxy, and Let's Encrypt

I have worked with both Exim and Postfix and in the past Postfix has always been my first choice when choosing an SMTP server. Postfix is amazing. However, since the release of OpenSMTPD from some of the developers of OpenBSD, OpenSMTPD has become my favorite SMTP server and it is an absolute fantastic feat of engineering.

If you have never used OpenBSD for anything you wouldn't know, but the guys from OpenBSD has an amazing ability to develop not only secure software, but also extremely well designed software with fantastic configuration management. They always manage to get the configuration files "human readable" and very logical.

Managing an SMTP server like Postfix or Exim is demanding because of the overly complicated setup. Not only is getting the correct pieces to fit together difficult to setup, but making sure you avoid mistakes is difficult too. With OpenSMTPD this is not so.

OpenSMTPD is mainly developed by Gilles Chehade, Eric Faurot and Charles Longeau as part of the OpenBSD project. The design goals include being secure, reliable, easy to configure, supporting the most common use-cases and with source code that can be distributed under an ISC license. Its portable version, like that of OpenSSH, was initiated by Charles Longeau and adds supports for multiple operating systems including NetBSD, FreeBSD, DragonFlyBSD and multiple Linux distributions.

The development of OpenSMTPD was motivated by issues with current SMTP daemons: Difficult configuration, complicated and difficult to audit code, and unsuitable licensing. OpenSMTPD is an attempt by the OpenBSD team to produce an SMTP daemon implementation that is secure, reliable, performant, simple to security audit and trivial to set up and administer. Code is designed to keep the memory, CPU and disk requirements as low as possible. Security in OpenSMTPD is achieved by robust validity check in the network input path, use of bounded buffer operations via strlcpy, and privilege separation to mitigate the effects of possible security bugs exploiting the daemon through privilege escalation. In order to simplify the use of SMTP, OpenSMTPD implements a smaller set of functionalities than those available in other SMTP daemons, the objective is to provide enough features to satisfy typical usage at the risk of unsuitability for esoteric or niche requirements.

Normally I would recommend that you run OpenSMTPD on OpenBSD in order to take advantage of the extra security features of the operating system, but in this case I am going to use Arch Linux because not all hardware is supported by OpenBSD.

You can easily run this setup on a Raspberry PI 3 or another small board as long as you're not dealing with tons of email for hundreds of users daily.

You should read all the documentation for OpenSMTPD and Dovecot on Arch Linux and the Dovecot documentation as you need to know and understand what you're doing. Don't just copy and paste stuff. Take the time to read everything and try to understand it. If there is something you don't understand, do a little research using your favorite search engine, or ask on IRC on a relevant channel or mailing list. Setting up a mail server the correct way requires time and effort and the more research you do, the more you understand the systems you put together.

Before you begin you should have a freshly installed Arch Linux system ready in which you have setup your domain name correctly. In this case we're going to use mail. Our FQDN will then be mail.example.com.

# hostnamectl set-hostname mail

To temporarily set the hostname (until reboot), use hostname from inetutils:

# pacman -S inetutils
# hostname mail

Then add your public IP address and domain to /etc/hosts/:

1.2.3.4 mail.example.com

OpenSMTPD

On most systems, including OpenBSD, OpenSMTPD is simply referred to as "smtpd", and this is true for Arch Linux as well, while on Debian GNU/Linux both the package name and the running binary is called "opensmtpd".

The OpenSMTPD package is in the official Arch Linux repository. Let us also install Dovecot while we're at it.

# pacman -S opensmtpd dovecot

We also need the table passwd filter, for parsing the passwd file, but this is sadly (as of writing) not in the official Arch Linux repository yet, but you can find opensmtpd-table-passwd in AUR in which the table passwd filter has been extracted, so install that.

Update 2019-07-06: Dovecot now supports having just username:password in the passwd table, which OpenSMTPD supports with the standard file: driver, rendering table-passwd useless. So we no longer need that.

After you have installed the packages, add the user and group vmail. The default behavior on Arch Linux is to create a group with the same name as the username, with GID equal to UID. We'll make a comment about the user with the -c option and make sure that the home directory is located in /var/vmail, which is where all virtual user email will be stored. We also prevent someone from actually logging in as the vmail user by the usage of the nologin option.

# useradd -m -c "Virtual Mail" -d /var/vmail -s /sbin/nologin vmail

Afterwards we find the following user added to the passwd file:

# cat /etc/passwd
...
vmail:x:1000:1000:Virtual Mail:/var/vmail:/sbin/nologin

If you have another user account besides the root account, which you really should have, the UID and GID will be a different number.

The directory /var/vmail is, as mentioned, used to store virtual users maildir folders, and is entirely managed by the IMAP server (Dovecot). Mail is delivered from OpenSMTPD to Dovecot via the Local Mail Transfer Protocol (LMTP), using the rcpt-to keyword.

Users submit emails via the submission port, which is port 587. Virtual users sending mail are authenticated via the /etc/mail/passwd file, which is shared between Dovecot and OpenSMTPD authentication systems.

Self-signed certificate

You don't need to use a self-signed certificate any longer as you can get real and free certificates from Let's Encrypt, so I suggest you skip ahead to the next section, but just in case you want to use a self-signed certificate here's how you can do that.

In this example the certificates are stored in /etc/ssl.

Generate an RSA private key:

# openssl genrsa -out /etc/ssl/private/mail.example.com.key 4096

This generates a 4096-bit RSA key stored in the file mail.example.com.key.

Once you have generated the RSA key, you can generate a certificate from it using the command:

# openssl req -x509 -new -key /etc/ssl/private/mail.example.com.key -out /etc/ssl/mail.example.com.crt -days 365

You can adjust the lifetime of the certificate via the -days parameter (one year in this example).

You can verify that the newly-generated certificate has the correct information with the following command:

# openssl x509 -in /etc/ssl/mail.example.com.crt -text

If you intend to use TLS for authentication you should install your certificate authority bundle as /etc/ssl/CAcert.pem, but if you don't intend to use TLS for authentication, you can simply link your new certificate to CAcert.pem:

# ln -s /etc/ssl/mail.example.com.crt /etc/ssl/CAcert.pem

Since the private key files are un-encrypted you need to make sure that they are readable and writable only by root:

# chmod -R go-rwx /etc/ssl/private

Let's Encrypt

Let's Encrypt is a certificate authority that launched on April 12, 2016 that provides free X.509 certificates for Transport Layer Security (TLS) encryption via an automated process designed to eliminate the hitherto complex process of manual creation, validation, signing, installation, and renewal of certificates for secure websites and other services. You can read more about the project on the Let's Encrypt Wikipedia page.

There exists several ways to obtain and renew certificates, and while the easiest method is to run a web server on port 80, I don't want to use that options as a mail server often cannot run anything on port 80 as that will conflict with any running web servers in the network.

Instead I am going to use the DNS validation method using the acme.sh client. It has build in API support for a lot of DNS providers and it is very easy to use. Several other clients exist, but I like the "acme.sh" script.

First install wget and download the script and make in executable:

# pacman -S wget
# cd (will take you to root's home directory)
# wget https://raw.githubusercontent.com/Neilpang/acme.sh/master/acme.sh
# chmod +x acme.sh

Next, you need to figure out how to use the script depending on your DNS provider. You need your DNS provider API AUTH ID and AUTH PASSWORD.

Then you export these for first time usage only and then you run the script:

# export FOODNS_AUTH_ID=XXXXX
# export DOODNS_AUTH_PASSWORD="YYYYYYYYY"
# ./acme.sh --issue --dns dns_mydnsprovider -d mail.example.com

After a while, if all goes well, you will have your certificates.

Remember the domain option -d is the FQDN, ie. mail.example.com, and not just example.com.

I like my certificates to be located in /etc/ssl/. Make sure the paths exist and make sure you enter the full filename and not just the directories.

Now, we need to install the certificates:

# ./acme.sh --install-cert -d mail.example.com --cert-file /etc/ssl/mail.example.com.crt --key-file /etc/ssl/private/mail.example.com.key --ca-file /etc/ssl/ca.crt --fullchain-file /etc/ssl/fullchain.crt

Since the private key files are un-encrypted you need to make sure that they are readable and writable only by root:

# chmod -R go-rwx /etc/ssl/private

Your exported API credentials will be stored in /root/acme.sh/account.conf and will be reused later on for auto renewal.

Edit /root/acme.sh/account.conf and uncomment the AUTO_UPGRADE="1" options. This way the acme script will auto update when it is run.

Since we're using systemd on Arch Linux I am setting up a timer for auto renewal. For further information please see: Using systemd units instead of cron.

Create a systemd unit for acme.sh:

# vi /etc/systemd/system/acme_letsencrypt.service

And populate it with:

[Unit]
Description=Renew Let's Encrypt certificates using acme.sh
After=network-online.target

[Service]
Type=oneshot
Environment="HOME=/root"
ExecStart=/root/acme.sh --renew --dns dns_mydnsprovider -d mail.example.com
# acme.sh returns 2 when renewal is skipped (i.e. certs up to date)
SuccessExitStatus=0 2

Test that it is working before you create the timer:

# systemctl daemon-reload
# systemctl start acme_letsencrypt --now
# journalctl -xe

Create a systemd timer unit for the service above:

# vi /etc/systemd/system/acme_letsencrypt.timer

And populate it with:

[Unit]
Description=Daily renewal of Let's Encrypt's certificates

[Timer]
OnCalendar=daily
RandomizedDelaySec=1h
Persistent=true

[Install]
WantedBy=timers.target

Then enable the timer with:

# systemctl enable acme_letsencrypt.timer

You now have a working set of Let's Encrypt certificates which we can use with both OpenSMTPD and Dovecot.

You can verify your certificate using OpenSSL (once OpenSMTPD us up and running):

$ openssl s_client -connect mail.example.com:25 -starttls smtp

Please note: When your certificate has expired and it has been auto-renewed you might need to manually restart Dovecot in order for it to notice. You can make a Bash script do this for you.

OpenSMTPD configuration

After installation of the certificates the mail server needs to be configured to accept TLS sessions and use the key and certificate. For OpenSMTPD, it's as simple as adding pki configuration to /etc/smtpd/smtpd.conf. The full setup of smtpd.conf will look like this:

# The pki setup.
# If you're using the self-signed certificates.
#pki mail.example.com cert "/etc/ssl/mail.example.com.crt"
#pki mail.example.com key "/etc/ssl/private/mail.example.com.key"

# Certificates from Let's Encrypt.
pki mail.example.com cert "/etc/ssl/fullchain.crt"
pki mail.example.com key "/etc/ssl/private/mail.example.com.key"

# Tables used for domains, users, passwords and aliases.
table aliases file:/etc/mail/aliases
table domains file:/etc/mail/domains
table passwd file:/etc/mail/passwd
table virtuals file:/etc/mail/virtuals

# Ports to listen on.
listen on lo
listen on enp0s3 port 25 tls pki mail.example.com

# We mask the source on port 587 for more privacy.
listen on enp0s3 mask-source port 587 tls-require pki mail.example.com auth <passwd>

# WARNING: THE FOLLOWING IS BASED ON THE OLD SYNTAX, YOU NEED TO
# CHANGE "accept" INTO "action" AND "match". PLEASE REFER TO THE
# MAN PAGE FOR smtpd.conf.

# Allow local delivery.
accept from local for local alias <aliases> deliver to lmtp "/var/run/dovecot/lmtp" rcpt-to
# Allow virtual domains.
accept from any for domain <domains> virtual <virtuals> deliver to lmtp "/var/run/dovecot/lmtp" rcpt-to
# Allow outgoing emails.
accept from local for any relay

Create the /etc/mail directory and populate it with the needed files:

# mkdir /etc/mail

Several table directives are specified in the example from the OpenSMTPD FAQ.

/etc/mail/aliases

The aliases table, set up from the /etc/mail/aliases file, contains aliases for local system users. Below the system user foo is an alias to the virtual user foo@example.com.

vmail:    /dev/null

root:     foo
foo:      foo@example.com

Explanation: On a normal running Linux or BSD system local email are often generated from various running scripts and send to the root user. In this case an alias is made to another local user called foo who will then normally receive the local root email instead of root. The local user foo has an alias to a virtual user foo@example.com, which is then the email address that will receive the email for root. So email generated by the system is "forwarded" from root to foo and from foo to foo@example.com.

So:

root -> foo -> foo@example.com

You can change this setup to suit your particular needs. If you don't want the local system email for root or anyone else to go to an alias, you can just leave the file with only the vmail setting.

vmail:    /dev/null

Please notice that the aliases table is only for local usernames, ie. users who can physically login to the machine. You cannot use the aliases file for forwarding between virtual email addresses.

/etc/mail/domains

The domains table is made from the /etc/mail/domains file and it contains a list of accepted virtual domains.

example.com
example.net

These are the domains you want to be able to receive email to. You need to make sure these domains are setup correctly at your DNS provider (more about that in part 3 of the tutorial).

/etc/mail/virtuals

Then we need the actual virtual users setup is in the /etc/mail/virtuals file as shown below.

abuse@example.com         foo@example.com
postmaster@example.com    foo@example.com
webmaster@example.com     foo@example.com
foo@example.com           vmail
bar@example.com           vmail

abuse@example.net         zoo@example.net
postmaster@example.net    zoo@example.net
webmaster@example.net     zoo@example.net
zoo@example.net           vmail

Notice that you don't use a colon : between the columns as you do in the aliases file!

In this example the virtual user foo@example.com is the domain owner of example.com, which is why we have setup the email addresses "abuse", "postmaster", and "webmaster". Email for these three email addresses will be forwarded to foo@example.com. It is generally a good idea when you are a domain owner to set these address up, and it is even required in some cases (for example by some SSL certificate vendors).

All the virtual users who are supposed to receive mail into a maildir folder are mapped to the single system user vmail.

/etc/mail/passwd

Then we have the /etc/mail/passwd file which is used to set up shared authentication for the virtual users between OpenSMTPD and Dovecot. The /etc/mail/passwd file must contain at least the virtual user names and the encrypted passwords.

foo@example.com:$6$encryptedpassword::::::
bar@example.com:$6$encryptedpassword::::::
zoo@example.net:$6$encryptedpassword::::::userdb_quota_rule=*:storage=1G

There are several tools you can use to generate the encrypted password strings for the passwd file.

On OpenBSD the passwords for the virtual users foo, bar, and zoo is generated manually using the tool smtpctl. You need to invoke smtpctl with the encrypt and no other option. It will then put you in an encryption shell where each line you enter will be encrypted when you hit the enter key. You use CTRL+c to exit. This also works on Arch Linux.

It is also possible to use smtpctl encrypt by specifying the password afterwards, on the same line like this smtpctl encryp mysecretpassword, but this will be visible in the system process list, so you shouldn't use it like that.

As of writing, on OpenBSD this generates a blowfish password, but on Arch Linux it results in a SHA-512 password. You have to make sure you match the password scheme you use to the settings in Dovecot. Password strings that begin with $2 are blowfish while strings that begin with $6 are sha-512. Both schemes are supported on OpenSMTPD and Dovecot.

If you prefer to use blowfish, you can use the doveadm tool to generate the wanted password scheme. The following command will provide you with a terminal for writing the password twice.

# doveadm pw -s BLF-CRYPT

In our example we're using SHA-512 and that's what we have setup in Dovevot too so either use the smtpctl encrypt command or use doveadm -pw -s SHA512-CRYPT.

Note that the multiple colons :::::: in the passwd file isn't a mistake and they aren't a part of the password generated string, they need to be there, and you need to insert them manually.

When you're done you can check your setup with:

# smtpd -n

OpenSMTPD will complain if you made any mistakes in your configuration file.

Don't start OpenSMTPD yet, we need to setup Dovecot first.

Dovecot

In this example, Dovecot is used as an IMAP server as I don't care much for POP. On Arch Linux the dovecot package has all we need, both for IMAP and LMTP.

Virtual users access and read their mails via IMAP. Dovecot listens on a LMTP socket in /var/dovecot/lmtp for mail delivery from OpenSMTPD. Passwords are shared between OpenSMTPD and Dovecot in the /etc/mail/passwd file and emails are delivered to /var/vmail subfolders.

The Dovecot configuration files are split between several files making it impossible to keep track of what is where. However, Dovecot is pretty easy to setup using mainly the default settings, as such you can move all the original files and concentrate on a single file setup.

First, move the old files:

# mkdir /root/dovecot-org-config-files
# mv /etc/dovecot/* /root/dovecot-org-config-files/

Then generate a new DH parameters file (this will take very long):

# openssl dhparam -out /etc/ssl/dh.pem 4096

Then create a new file called /etc/dovecot/dovecot.conf and insert the following:

listen = *

ssl = required

ssl_cert = </etc/ssl/fullchain.crt
ssl_key = </etc/ssl/private/mail.example.com.key
ssl_dh = </etc/ssl/dh.pem

mail_location = maildir:~/Maildir

passdb {
    # This is where you define your password scheme.
    # If you have used blowfish it needs to be 'BLF-CRYPT'.
    args = scheme=sha512-crypt /etc/mail/passwd
    driver = passwd-file
}

userdb {
    args = uid=vmail gid=vmail home=/var/vmail/%d/%n
    driver = static
}

protocols = imap lmtp

service imap-login {
  inet_listener imaps {
    port = 993
    ssl = yes
  }
  # Disable imap
  inet_listener imap {
    port = 0
  }
}

You can see what Dovecot sees with the command:

# dovecot -n

Running and debugging OpenSMTPD and Dovecot

Last you need to enable and start Dovecot:

# systemctl enable dovecot
# systemctl start dovecot

Check the status and the log:

# systemctl status dovecot
# journalctl -xe

And verify that it is running on the IMAP port 993:

# ss -tapn
...
LISTEN    0    100    192.168.1.1:993    0.0.0.0:*    users:(("dovecot",pid=926,fd=33))

Then enable and start OpenSMTPD:

# systemctl enable smtpd
# systemctl start smtpd

Check the status and log of OpenSMTPD with:

# systemctl status smtpd
# journalctl -xe

Also make sure that it is listening on port 25 and port 587:

# ss -tapn
...
LISTEN    0    5     192.168.1.1:25    0.0.0.0:*    users:(("smtpd",pid=644,fd=16))

You can always debug OpenSMTPD by shutting it down and starting in manually in verbose mode:

# systemctl stop smtpd
# smtpd -vvvd -f /etc/smtpd/smtpd.conf

If you ever need to, you can flush everything in OpenSMTPD queues with:

# rm -rf /var/spool/smtpd/*

And if you need to, perhaps during testing, to remove all email accounts, you can just do:

# rm -rf /var/vmail/*

DKIMproxy

DomainKeys Identified Mail (DKIM) is a method for you to have your SMTP server sign all outgoing email with a private key. The receiving SMTP server can then use your public key, which you setup in the DKIM DNS record, to verify that the domain owner actually is the original sender of the email.

DKIM, together with SPF and DMARC, is a requirement from many big email hosters like Google, Microsoft, Yahoo!, and others. If these records aren't setup correctly, you email will most likely be filtered or rejected.

The DKIM is specified in the RFC6376.

OpenSMTPD can be setup to work with several DKIM applications, but I prefer DKIMproxy and it can be installed using pacman:

# pacman -S dkimproxy

When you use DKIMproxy, OpenSMTPD will first forward email that is going out to the Internet to DKIMproxy. DKIMproxy will then sign the email and send it back to OpenSMTPD, which then will send the mail out unto the Internet.

Let's setup DKIMproxy and then afterwards update our OpenSMTPD configuration file.

# cd /etc/dkimproxy
# vi dkimproxy_out.conf

Insert the following to dkimproxy_out.conf:

# specify what address/port DKIMproxy should listen on
listen    127.0.0.1:10027
# specify what address/port DKIMproxy forwards mail to
relay     127.0.0.1:10028
# specify what domains DKIMproxy can sign for (comma-separated, no spaces)
domain    mail.example.com
# specify what signatures to add
signature dkim(c=relaxed)
signature domainkeys(c=nofws)
# specify location of the private key
keyfile   /etc/mail/dkim/private.key
# specify the selector (i.e. the name of the key record put in DNS)
selector  selector1

You'll see how to use the "selector" in part 3 of the tutorial.

Generate the key for signing (at least 1024 bits):

# mkdir /etc/mail/dkim
# openssl genrsa -out /etc/mail/dkim/private.key 1024
# openssl rsa -in /etc/mail/dkim/private.key -pubout -out /etc/mail/dkim/public.key

The public key we have just generated will be used in part 3 of the tutorial in which we setup DNS.

Fix permissions:

# chown root:dkimproxy /etc/mail/dkim/private.key
# chmod 440 /etc/mail/dkim/private.key

Then let's update /etc/smtpd/smtpd.conf:

# The pki setup.
# If you're using the self-signed certificates.
#pki mail.example.com cert "/etc/ssl/mail.example.com.crt"
#pki mail.example.com key "/etc/ssl/private/mail.example.com.key"

# Certificates from Let's Encrypt.
pki mail.example.com cert "/etc/ssl/fullchain.crt"
pki mail.example.com key "/etc/ssl/private/mail.example.com.key"

# Tables used for domains, users, passwords and aliases.
table aliases file:/etc/mail/aliases
table domains file:/etc/mail/domains
table passwd file:/etc/mail/passwd
table virtuals file:/etc/mail/virtuals

# Ports to listen on.
listen on lo
listen on lo port 10028 tag DKIM_OUT # DKIMproxy.
listen on enp0s3 port 25 tls pki mail.example.com

# We mask the source on port 587 for more privacy.
listen on enp0s3 port 587 tls-require pki mail.example.com auth <passwd>

# WARNING: THE FOLLOWING IS BASED ON THE OLD SYNTAX, YOU NEED TO
# CHANGE "accept" INTO "action" AND "match". PLEASE REFER TO THE
# MAN PAGE FOR smtpd.conf.

# Allow local alias delivery to Dovecot.
accept from local for local alias <aliases> deliver to lmtp "/var/run/dovecot/lmtp" rcpt-to
# Allow virtual domain delivery to Dovecot.
accept from any for domain <domains> virtual <virtuals> deliver to lmtp "/var/run/dovecot/lmtp" rcpt-to
# Allow outgoing mails to pass to DKIMproxy.
accept tagged DKIM_OUT for any relay
accept from local for any relay via smtp://127.0.0.1:10027

Enable and start DKIMproxy:

# systemctl enable dkimproxy_out
# systemctl start dkimproxy_out

Then restart OpenSMTPD:

# systemctl restart smtpd

Check that all services are listening on their assigned ports:

# ss -atpn

All we have left now is to setup DNS. Will do that in the part 3 of the tutorial.

Observations and debugging errors

Server certificate verification failed

If you ever see this in your mail logs when you send out email:

smtp-out: Server certificate verification failed on session 7c5f86cd236a0d26

Then it's not your certificate that is failing its verification, but the receiving SMTP server.

550 Access denied - Invalid HELO name (See RFC5321 4.1.1.1)

When sending email, if you get this kind of messages, set your FQDN in the file /etc/smtpd/mailname. Otherwise, the server name is derived from the local hostname returned by gethostname, either directly if it is a fully qualified domain name, or by retrieving the associated canonical name through getaddrinfo.

# cat /etc/smtpd/mailname
mail.example.com