Setting up a mail server using OpenSMTPD and Dovecot

There are a bunch of reasons why you really shouldn't host your own email server. Well, really there's one: it's sort of hard to figure everything out on your own. If you've never run a mail server before, it's easy to get lost in a bunch of nonsense terms like MTA, MDA, DKIM, and DNS.

So you can do what I did the first time I hosted a mail server: blindly follow a tutorial you don't understand. But if you're anything like me, part of the reason why you want to host your own email is that you enjoy understanding what's going on.

This tutorial is made using the simplest components and the simplest possible setup. Most of the steps are straightforward, and we'll use software that's easy to configure. No databases! No arcane spells! Human-readable config files are all we'll need.

In this post, I'll do my best to explain what's actually going on. I won't assume that you know anything, besides a basic understanding of how the command line works. Feel free to skip sections you already understand.

Overview

Email works thanks to SMTP — Send Mail Transfer Protocol. You might recognize the acronym from you email settings. Indeed, whenever you send someone an email, you connect to a relay hosted by your provider (gmail or outlook or whatever) with SMTP. In turn, the relay uses SMTP to connect to other relays until the message gets to the one the recipient is using.

SMTP is handled by MTA — Mail Transport Agents. The most widely-used MTA is Postfix, because it's versatile and fast and scalable. We don't really need these for a personal mail server. We'll use OpenSMTPD instead, because it's much easier to configure.

However, MTAs can only transport mail between relays. To save it on you server's disk, we need a MDA — Mail Delivery Agent. OpenSMTPD can take care of that for us. It'll use the Maildir format, which very easily maps emails to files on the hard drive.

So OpenSMTPD will get the mail from and to your server. The next step is to get mail from the server to your PC and phone. To do that, we'll need an IMAP server. We'll use Dovecot — which, as flexible as it is, has really sensible defaults and is surprisingly easy to configure.

Next, we'll install a webmail program — Rainloop. This means you'll be able to access your email even from other computers and without specific email clients.

Finally, I'll also install a CalDAV/CardDAV server to sync contacts and calendar entries.

My objective in building this server was not to make use of any databases. What I want is to manage my email accounts through GNU/Linux shell management commands.

During this tutorial, Hostname and usernames will be mine; you should, of course, take care to set them to whatever yours are. I'll be using mail.gstelluto.com as the hostname, gstelluto.com as the mail domain name (so I can send to me@gstelluto.com) and pinusc as my username.

I set this up on a Fedora 29 server; all the steps should be quite universal, however, if you try installing on a Ubuntu server. Just replace dnf with apt in all the commands, and take care to check the paths provided are the same, and you'll be all right!

Initial DNS Setup

In this section, I assume how DNS works and how to add records to your domain. If you don't know how that works, you can read this article from DigitalOcean.

First, we set an A record for mail.gstelluto.com to the correct address. An MX record should point to mail.gstelluto.com.

This is all we need to get started. Later, we'll be adding a few more records in order to authenticate our mail server and stay off blacklists.

Initial setup

In this section, we'll go through the initial steps that one should take upon installing any server… not just a mail one. This is the minimal initial setup you should undertake to have a functional and secure server.

The very first step is to create a user and giving it root privileges. One should never ever log in as root, as that's a security risk. I'll call my user "pinusc".

# useradd pinusc
# usermod -G wheel pinusc

We'll set up password-less sudo.

# visudo
%wheel ALL=(ALL:ALL) NOPASSWD: ALL

# usermod -s /bin/bash pinusc
# mkdir -p /home/pinusc/.ssh
# chown -R pinusc:pinusc /home/pinusc

Next, ssh keys setup:

# vim /home/pinusc/.ssh/authorized_keys

Here I insert the public key I generated on my local machine with # ssh-keygen -t ed25519 -a 100 (this creates a key that's more secure than the default settings).

Now I can login in the machine with $ ssh pinusc@mail.gstelluto.com.

At this point, I also copy the .bashrc and .bash_profile files I use on my machine. You don't have to do that, however.

From now on, I'll use the following convention when writing commands: lines starting with $ are to be executed from an unprivileged user (pinusc), lines starting with # are to be executed as root (hence with sudo). So # dnf install foo means sudo dnf install foo.

SSH hardening and firewall

As usual, it's good to # dnf update.

Then, we install a few useful programs:

# dnf install tmux vim wget unzip

If you don't know how to use vim, install nano instead and add export EDITOR=nano and export SUDOEDIT=nano to your ~/.profile.

We should set up the Hostname: # vi /etc/hostname to mail.gstelluto.com

First of all, we harden the SSH config with

/etc/ssh/sshd_config
PermitRootLogin no
PasswordAuthentication no

We set up a ufw firewall as follows:

# dnf install ufw
# ufw allow ssh
# ufw allow http
# ufw allow https
# ufw allow SMTPD
# ufw allow SMTPDS
# ufw allow IMAP
# ufw allow IMAPS
# ufw enable

Getting SSL certificates

While we could generate our SSL certificates manually, that would be a bad idea because they wouldn't be trusted by Central Authorities. We can get free trusted certificates from Let's Encrypt — and that's what we'll do through the help of certbot.

# apt install certbot

# certbot certonly -d mail.gstelluto.com and follow the instructions on screen.

This creates /etc/letsencrypt/live/mail.gstelluto.com/, containing the files fullchain.pem (the public key) and privkey.pem (the.. well, private key).

As we'll also run a CarDAV/CalDAV server here, we generate certificates for that as well with # certbot certonly -d dav.gstelluto.com

OpenSMTPD setup

Installation

# apt install opensmtpd and follow the on-screen instructions. It'll just ask to set up the domain name (gstelluto.org) and the postmaster email address.

…that is, if you'd like your server to crash upon authentication. Ubuntu LTS ships with a really old version. We need at least 6.3.1, so we'll have to compile the whole thing from source.

The config file

The config file turned out to be incredibly short. In understanding how to make this happen, the manpages smtpd.conf(5) and tables(5) were invaluable.

/etc/opensmtpd.conf
pki mail.gstelluto.com certificate "/etc/letsencrypt/live/mail.gstelluto.com/fullchain.pem"
pki mail.gstelluto.com key "/etc/letsencrypt/live/mail.gstelluto.com/privkey.pem"

table aliases file:/etc/aliases
table creds file:/etc/opensmtpd/creds
table mdomain { "gstelluto.com", "*.gstelluto.com" }

listen on 0.0.0.0 tls pki mail.gstelluto.com
listen on 0.0.0.0 port 587 tls-require pki mail.gstelluto.com auth <creds>
listen on lo port 10028 tag DKIM

accept from any for domain <mdomain> alias <aliases> deliver to maildir "/mail/%{user.username}"
accept tagged DKIM for any relay
accept from any authenticated for any relay via smtp://127.0.0.1:10027

Note: this includes DKIM set-up, which is detailed in a later section. If you're not interested in DKIM signing, you can remove the lines listen on lo ... and accept tagged..., and remove the via smtp:... bit on the last line.

We need to fill up a credentials table for each user.

# touch /etc/creds

Then, we fill it up. Each line is of the form username enc_pass, where enc_pass is the encrypted password, that we can obtain by running # smtpctl encrypt and typing the plaintext password. For example:

/etc/creds
user1   $2b$10$hIJ4QfMcp.90nJwKqGbKM.MybArjHOTpEtoTV.DgLYAiThuoYmTSe
user2   $2b$10$bwSmUOBGcZGamIfRuXGTvuTo3VLbPG9k5yeKNMBtULBhksV5KdGsK

Aliases

In /etc/aliases, specify a list of id: alias, where id is the aliased address and alias the actual user on the system. e.g. giuseppe: pinusc will enable mail for giuseppe@gstelluto.com to be delivered to pinusc@gstelluto.com, i.e. /var/mail/pinusc.

DKIM

We'll need the dkimproxy program. As it is not packaged in Fedora, we'll need to build from source.

# dnf install "perl(Mail:DKIM)" "perl(Net:Server)" 
# dnf install 
$ wget http://downloads.sourceforge.net/dkimproxy/dkimproxy-1.4.1.tar.gz
$ tar xzf dkimproxy*
$ cd dkimproxy-1.4.1
$ ./configure
# make install

The only useful documentation I could find is in the INSTALL file included in the tarball.

Let's create a config file! # mkdir /etc/dkimproxy

/etc/dkimproxy/dkimproxy_out.conf
listen 127.0.0.1:10027
relay 127.0.0.1:10028
domain gstelluto.com
signature dkim(c=relaxed)
signature domainkeys(c=nofws)
keyfile /etc/dkimproxy/private.key
selector mail

We need to generate RSA keys. We can (and should) generate them locally, without the aid of Let's Encrypt. This is because DKIM does not need a Certificate Authority; instead, the public key will be embedded in our DNS records. Let's generate the keys:

# openssl genrsa -out /etc/dkimproxy/private.key 1024
# openssl rsa -in private.key -pubout -out /etc/dkimproxy/public.key

Now we'll need to generate the DKIM record. For this, we need the public key, without header and footer, and with all lines concatenated. Run this to get it:

< public.key head -n -1 | tail -n +2 | tr -d $'\n'

Mine looks like this:

DKIM key example
MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQC6ScShyJQy0G0CaTEFdmOB7W8NfgOMPdwWyZum1TNN6x6UTXqE0QP9ELiP4gwnZOuHgeGy29dnYtim2bfmhBbvrB7A/fiJ0hDDhKOujO2FsCKATTFM22iC27XSiowJOI4H/83bkWpWzZAByY234c1StaI9vDATIlLOQ3oCA6TRWwIDAQAB

And add this line to your DNS zone file:

DNS zone file
mail._domainkey.gstelluto.com	604800	 IN 	TXT	"v=DKIM1; k=rsa; p=YOURKEY"

Where YOURKEY is, of course, your public key. Don't forget the closing " ! The TTL is 1 week long. Long TTL, I'm told, help you stay off blacklists.

The record's subdomain mail._domainkey... is determined by the selector mail line in dkimproxy_out.conf. If you set the selector to anything else, you can change the DNS subdomain accordingly.

You can check (from your local machine) that the DNS record is set with dig @1.1.1.1 mail._domainkey.gstelluto.com TXT +short. If it doesn't work right away, wait for a few hours for the DNS to propagate.

We'll also create a Systemd unit to run dkimproxy.out for us.

/etc/systemd/system/dkimproxy-out.service

[Unit]
Description=Run DKIMproxy service

[Service]
ExecStart=/usr/local/bin/dkimproxy.out --conf_file=/etc/dkimproxy/dkimproxy_out.conf
Restart=always
User=dkim

[Install]
WantedBy=multi-user.target

Other DNS Records

Now we should add three new records: a DMARC, a SPF, and a reverse DNS record.

Testing

To test the setup, we can manually connect to the server and write SMTP commands by hand.

nc mail.gstelluto.com 25 works to deliver mail without using SSL; We can test SSL with openssl, which will take care of handshaking and veryifing everything works. Run openssl s_client -host mail.gstelluto.com -port 25 -starttls smtp and follow the example below.

We have to use the "EHLO", "MAIL FROM:", "RCPT TO:", and "DATA" commands to send the message. Lines starting with < are sent by the server; those starting with > are typed by me and followed by a newline.

Unauthenticated SSL SMTP example
... (bunch of SSL lines)
> EHLO gstelluto.com
< 250-gstelluto.com Hello gstelluto.com [151.72.133.88], pleased to meet you
< 250-8BITMIME
< 250-ENHANCEDSTATUSCODES
< 250-SIZE 36700160
< 250-DSN
< 250 HELP
> MAIL FROM: <bob@example.com> 
< 250 2.0.0: Ok
> RCPT TO: <logins@gstelluto.com>
< 250 2.1.5 Destination address valid: Recipient ok
> DATA
< 354 Enter mail, end with "." on a line by itself
> From: [Bob] <bob@example.com>
> To: [Log] <logins@gstelluto.com>
> Date: Tue Jan 22 21:06:09 CET 2019
> Subject: Test msg 
> 
> Heyo, test here
> 
> .
< 250 2.0.0: 7b3b2f03 Message accepted for delivery
> QUIT
< 221 2.0.0: Bye

NOTE: The empty line after Subject is part of a RFC and you must write it.

This only tests if we're able to receive mail. In order to send it, we must authenticate through port 587. The credentials are sent in plaintext, encoded using base64: $ printf 'user\0user\0password' | base64.

Now, as before (but on port 587):

$ openssl s_client -host mail.gstelluto.com -port 587 -starttls smtp

And the sequence is the same, except for the addendum of an AUTH PLAIN command after the EHLO

Authenticated SSL SMTP example
> EHLO gstelluto.com
< 250-gstelluto.com Hello gstelluto.com [151.72.133.88], pleased to meet you
< 250-8BITMIME
< 250-ENHANCEDSTATUSCODES
< 250-SIZE 36700160
< 250-DSN
< 250-AUTH PLAIN LOGIN
< 250 HELP
> AUTH PLAIN
< 334 
> dXNlcgB1c2VyAHBhc3N3b3Jk
< 235 2.0.0: Authentication succeeded

And then we can go on sending a message, this time using a local email address as MAIL FROM: and any other as RCPT TO:

Dovecot setup

As of now, the mail gets delivered to maildirs in /mail/. Since I don't want to ssh in the server avery time I want to check my mail, we'll be installing a IMAP/POP3 server: Dovecot.

I don't want to mess around with databases and passwords. Authentication should be made using /etc/shadow usernames and passwords. Luckily, Dovecot sets this up out of the box!

Let's go ahead and install it!

# dnf install dovecot

Dovecot's configuration can be split across many ancillary files (in /etc/dovecot/conf.d). I'll reference those, and at the end provide the equivalent central config file.

I was pleasantly surprised to see that Dovecot is really easy to set up, and comes with sensible defaults.

First of all, we need to tell Dovecot where our mail is stored.

/etc/dovecot/conf.d/10-mail.conf
mail_location = maildir:/mail/%n

Dovecot automatically generates some SSL certificates, but we'll use the ones we got earlier from Let's Encrypt.

/etc/dovecot/conf.d/10-ssl.conf
ssl_cert = </etc/letsencrypt/live/mail.gstelluto.com/fullchain.pem
ssl_key = </etc/letsencrypt/live/mail.gstelluto.com/privkey.pem

As mentioned earlier, instead of changing these many ancillary files, you can just edit dovecot.conf and set it to this:

/etc/dovecot/dovecot.conf
mail_location = maildir:/mail/%u
mbox_write_locks = fcntl
namespace inbox {
  inbox = yes
  location = 
  mailbox Drafts {
    special_use = \Drafts
  }
  mailbox Junk {
    special_use = \Junk
  }
  mailbox Sent {
    special_use = \Sent
  }
  mailbox "Sent Messages" {
    special_use = \Sent
  }
  mailbox Trash {
    special_use = \Trash
  }
  prefix = 
}
passdb {
  driver = pam
}
ssl = required
ssl_cert = </etc/letsencrypt/live/mail.gstelluto.com/fullchain.pem
ssl_cipher_list = PROFILE=SYSTEM
ssl_key = # hidden, use -P to show it
userdb {
  driver = passwd
}

This was obtained with # doveconf -n, which prints all the settings that have been changed from the defaults.

Rainloop setup

Next, we need to configure webmail with Rainloop. It's a PHP app, so the install process it's relatively simple. We'll use nginx as a server and reverse proxy.

First, we'll get the archive, extract it, and set the correct permissions.

# mkdir -p /var/www/rainloop
$ sudo chown pinusc:pinusc /var/www/rainloop
$ wget https://www.rainloop.net/repository/webmail/rainloop-community-latest.zip
# unzip rainloop-community-latest.zip -d /var/www/rainloop
# chown nginx:nginx /var/www/rainloop
$ cd /var/www/rainloop
# find . -type d -exec chmod 775 {} \;
# find . -type f -exec chmod 664 {} \;

Next, we'll need to setup nginx and php.

# dnf install nginx php-fpm php-json php-dom
# systemctl enable --now nginx
# systemctl enable --now php-fpm
/etc/php-fpm.d/www.conf
listen = 127.0.0.1:9000
user = nginx
group = nginx

/etc/nginx/conf.d/rainloop.conf
server {
        listen 80 default_server;
        listen [::]:80 default_server;

        server_name mail.gstelluto.com;

        root /var/www/rainloop;

        index index.html index.htm index.php;

        location / {
                # First attempt to serve request as file, then
                # as directory, then fall back to displaying a 404.
                try_files $uri $uri/ =404;
        }
        

        location ^~ /data {
                deny all;
        }

        location ~ \.php$ {
                include /etc/nginx/fastcgi_params;
                fastcgi_pass  127.0.0.1:9000;
                fastcgi_index index.php;
                fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
        }
}

Now we need to setup the SSL certificates. We'll let Certbot handle the configuration; but we need to install the nginx plugin first

# dnf install certbot-nginx
# certbot --nginx -d mail.gstelluto.com

When prompted, make sure to select to reinstall the existing certificate, and not request a new one. This will leave the certificate unaltered, and automatically alter the configuration file for nginx's rainloop.

Next, we need to actually configure Rainloop; The configuration is done through the admin UI available at https://mail.gstelluto.com/?admin. Log-in with admin and 12345 as credentials, and play around with the settings.

Make sure to change the admin password (Security section).

In "Domains" section, add a new domain. The settings should be straightforward. Make sure to enable "use short login name". For IMAP, server is mail.gstelluto.com, security SSL/TLS, port 993. For SMTP, server is the same, security is STARTTLS, port is 587. Easy peasy.

Now you should be able to log in your email account!

Author: Giuseppe (giuseppe@gstelluto.com)

Date:

Emacs 26.1 (Org mode 9.1.14)