How to deploy a personal email server

Published 03/09/2013, last edit 30/09/2020.

Although emails are a very common thing on internet, deploying an email server is not trivial. Therefore, you may find a lot of tutorials on the web, more or less trustworthy, more or less complete and more or less out-of-date. Each of those tutorial uses it's favorite software and has its own way of doing things, so here is mine.

Prerequisites

This guide supposes you:

  • have admin rights on a machine running an UNIX-like operation system;
  • own a domain name;
  • know how to modify the DNS entries associated to your domain name;
  • know what are X.509 certificates (improperly called SSL certificates);
  • know how to use your operating system (in particular, know how to manage daemons, packages, ports, …);
  • understand that depending on your distribution some files may have a different location;
  • are able to perform some research all by yourself;
  • do not copy/paste stuff without understanding it.

First things first: the DNS

In order to access the email server, it is important to add an MX field to our domain name. In this case, we have it referring to a sub-domain which has both a A and a AAAA entries.

example.org.      IN  MX 10 mx1.example.org.
mx1.example.org.  IN  A     203.0.113.42
mx1.example.org.  IN  AAAA  2001:db8::42

Before going any further, you must check whether or not your domain name has a valid FCrDNS. In short, reverse DNS lookup to the IP addresses must return the associated domain name.

$ dig +short -x "203.0.113.42"
mx1.example.org.
$ dig +short -x "2001:db8::42"
mx1.example.org.

If it's not the case, fix your DNS configuration.

SPF and DMARC

In order to fight against identity theft, there is several solutions. Two of them are SPF and DMARC. To configure them, we have to add TXT entries defining restrictions to apply to emails coming from our domain name. Example:

example.org.             IN  TXT  "v=spf1 a mx ip6:2001:db8::/32 -all"
_dmarc.example.org.      IN  TXT  "v=DMARC1;adkim=s;aspf=s;p=reject;sp=reject;ruf=mailto:postmaster@example.org;fo=1"

The MTA

The MTA (Mail Transfer Agent) is the software which allows us to send and receive emails. There is many of those (qmail, exim, postfix, sendmail, …), here we will focus on an MTA that is stable, full-featured and extremely easy to configure: OpenSMTPD. This tutorial requires OpenSMTPD version 6.4 or later.

Once installed, the configuration is done in /etc/mail/smtpd.conf and man 5 smtpd.conf comes in handy. To have a server able to receive emails for the example.org domain name, all we need to have is this:

table aliases "file:/etc/smtpd/aliases"

listen on 127.0.0.1
listen on ::1

listen on 203.0.113.42
listen on 2001:db8::42

action local_deliver maildir alias <aliases>

match from local for local action local_deliver
match from any for domain "example.org" action local_deliver

We will now make this configuration more robust and functional by using TLS and authorizing users to send emails.

pki example.org cert "/etc/acmed/certs/example.org_ecdsa-p384.crt.pem"
pki example.org key  "/etc/acmed/certs/example.org_ecdsa-p384.pk.pem"

table aliases "file:/etc/smtpd/aliases"

# SMTP port 25 from localhost
listen on 127.0.0.1    tls pki example.org hostname mx1.example.org
listen on ::1          tls pki example.org hostname mx1.example.org

# SMTP port 25 from external sources
listen on 203.0.113.42 tls pki example.org hostname mx1.example.org
listen on 2001:db8::42 tls pki example.org hostname mx1.example.org

# SMTPS port 465 from authenticated users
listen on 203.0.113.42 smtps pki example.org auth hostname mx1.example.org mask-src
listen on 2001:db8::42 smtps pki example.org auth hostname mx1.example.org mask-src

action local_deliver maildir alias <aliases>
action relay_out relay helo example.org

match from local for local action local_deliver
match from any for domain "example.org" action local_deliver
match from any auth for any action relay_out

The private key must not be accessible (read, write, execute), excepted for the owner. It is therefore recommended to use chmod 600 or chmod 400 on it.

Ports

We shall accept incoming TCP connections on ports 25 (SMTP) and 465 (SMTPS).

Virtual users

Our MTA currently uses the system's users. Although it can be a good thing in some situation, it may not be something you want to stick with. My personal preference is to use a virtual user so I can have a different password on my email account and my user account on my dedicated server.

Let's create the /etc/smtpd/vusers file which will contain, on each line, our users using the <user name>:<hashed password> format :

john:$y$jCT$JnBTCfMf6qDY0YAyr.Wyz1$gFFF/qWkcclRfahZOcPa7Q2vTwYmX1omW34AL/kR9U1
bob:$y$jCT$2JqWUNiukyr5XRvieZQS4.$RF7fRV9fCEqRw7k65A0iWFz1vp/MCpORif3p.aqvQj6
emilia:$y$jCT$bRvBpoQV2LtjoNC.ooKbe1$aIy2AMzL0Ip1ejE1AjV/A1kG.uyx51innV9ok/R5mO0

The passwords are hashed using the crypt(3) function, which can be invoked using the smtpctl encrypt command. After each modification of this file, the smtpctl update table vusers must be executed.

In order to use this new user database in OpenSMTPD, we have to add a new vusers table and use it after auth in the listen directives:

table vusers  "file:/etc/smtpd/vusers"

# SMTPS port 465 from authenticated users
listen on 203.0.113.42 smtps pki example.org auth <vusers> hostname mx1.example.org mask-src
listen on 2001:db8::42 smtps pki example.org auth <vusers> hostname mx1.example.org mask-src

Aliases

In order to add aliases, let's edit /etc/smtpd/aliases:

#
# Please run `smtpctl update table <table_name>` after any change to this file.
#

# Person who should get root's mail. Don't receive mail as root!
root:           john

# Basic system aliases
MAILER-DAEMON:  postmaster
postmaster:     root

# General redirections for pseudo accounts
bin:            root
daemon:         root
nobody:         root
decode:         root

# Well-known aliases
manager:        root
dumper:         root
operator:       root
abuse:          root
admin:          root
webmaster:      root
hostmaster:     root
spam:           root

# Personal aliases
john.doe:       john
doe.john:       john
bob.something:  bob

As for the virtual users file, we have to run smtpctl update table aliases after each modification of this file.

Fetching our emails

In order for the MUA (Mail User Agent, like ThunderBird or mutt) to be able to fetch our emails, we have to set up a POP or IMAP server. Let's use Dovecot.

Generic configuration

The /etc/dovecot/dovecot.conf file contains Dovecot's generic configuration. We will change the following parameters :

  • protocols = imap sieve lmtp will enable the IMAP server and allow the use of Sieve as well as email distribution through LMTP;
  • listen = 203.0.113.42 2001:db8::42 will have the IMAP server to listen of the public IP addresses.

Email distribution

In order to store our user's emails, we have to create the new vmail system user which will keep virtual user's home directories within /var/vmail:

useradd --system --user-group --shell /usr/bin/nologin --home-dir /var/vmail --create-home vmail

We now have to tell Dovecot how to use our virtual users database. To this end, we edit the /etc/dovecot/conf.d/10-auth.conf file. By default, this file includes an other file describing the user authentication method. We comment out this include and add our own one at the end:

#!include auth-system.conf.ext

!include auth-passwdfile-static.conf.ext

Then we create the /etc/dovecot/conf.d/auth-passwdfile-static.conf.ext file:

passdb {
  driver = passwd-file
  args = scheme=CRYPT username_format=%n /etc/smtpd/vusers
}

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

Let's check that, in /etc/dovecot/conf.d/10-mail.conf, the path to the user's Maildir is correctly set :

mail_location = maildir:~/Maildir

As mentioned earlier, LMTP will be used to allow Dovecot to distribute the emails sent by OpenSMTPD. Its configuration is located in /etc/dovecot/conf.d/20-lmtp.conf. Let's check that the Sieve plugin is activated.

protocol lmtp {
  mail_plugins = $mail_plugins sieve
}

Sieve

Sieve having been enabled, we can configure it using the /etc/dovecot/conf.d/20-managesieve.conf file:

plugin {
  sieve = ~/.dovecot.sieve
  sieve_dir = ~/sieve
  sieve_extensions = +notify +imap4flags +imapflags +editheader
}

TLS

In order to use TLS to protect communications between the client and the server, we edit the /etc/dovecot/conf.d/10-ssl.conf file as follows:

ssl = required

ssl_cert = </etc/acmed/certs/example.org_ecdsa-p384.crt.pem
ssl_key = </etc/acmed/certs/example.org_ecdsa-p384.pk.pem

ssl_min_protocol = TLSv1.2

ssl_cipher_list = HIGH:!aNULL:!eNULL:!LOW:!MEDIUM:!EXP:!RC4:!3DES:!MD5:!SHA1:!PSK:!kRSA:!SRP:-AES128:-DH:+ECDH
ssl_prefer_server_ciphers = yes

Unless you must support unusual stuff, every other line must be empty or commented out.

Be careful with the certificate, you really should not use the default self-generated one. Please note the minimal TLS version is 1.2 since previous version are not recommended. In fact, TLS 1.2 is also not recommended, ideally we'll use only TLS 1.3. Unfortunately, Dovecot currently does not allows to set TLS 1.3 as the minimal version.

You should also notice the TLS configuration which forbids traditional Diffie-Hellman key exchanges but allows the variant based on elliptical curves. Therefore, there is no need to worry about the default weak DH parameters.

Ports

We shall accept incoming TCP connections on port 993 (IMAPS).

The MDA

While the MTA is able to send and receive emails, the MDA (Mail Delivery Agent) is the software which delivers the emails to the recipients. Most SMTP servers can act as both an MTA and an MDA, which is the case for OpenSMTPD. That's why we have been able to configure it to deliver emails into the maildir folder.

An other MDA

Using the MDA integrated into our SMTP server works, however it is quite limited in term of functionalities. Instead, we'll use the one that comes with dovecot and interface it using LMTP. This MDA has the advantage of using sieve).

OpenSMTPD

In OpenSMTPD's configuration, all we need to do is to edit our local_deliver action and give it the LMTP's socket path:

action local_deliver lmtp "/run/dovecot/lmtp" alias <aliases>

The socket's address may vary depending on your operating system.

Filtering emails

Now we can use sieve to filter emails. Each user may set his own sieve rules in the ~/.dovecot.sieve file (example: /var/vmail/john/.dovecot.sieve). Here is an example:

require ["fileinto", "imapflags"];

if header :contains "X-Spam" "yes" {
  setflag "\\Seen";
  fileinto "Junk";
}
elsif address :is ["From", "To", "Cc", "Reply-to"] "nantes@lists.afpy.org" {
  fileinto "INBOX.Python.Nantes";
}
elsif address :is ["From", "To", "Cc", "Reply-to"] "python@example.org" {
  fileinto "INBOX.Python";
}

This example put emails marked as spam in the spam folder. Then, it sorts emails into custom folders.

DKIM

DomainKeys Identified Mail (DKIM) is an other way to fight against identity theft. The principle is to sign (at the server level) all sent emails and publish the public key in a DNS entry. Thank to this, when receiving an email from whatever@example.org, a DNS query is made to a sub-domain of example.org in order to fetch the public key allowing to verify the email's signature. If everything is ok, it proves the email has been sent by a server which has the associated private key.

OpenDKIM certainly is the most popular DKIM implementation. Unfortunately, it uses a milter to be integrated in the MTA, which is not supported by OpenSMTPD. We'll have a to a lesser known solution: dkimproxy.

Signing outgoing emails

As its name shows, dkimproxy acts as an SMTP proxy. One sends it an email via SMTP on a specific port and it returns the same email, signed, on a different port. To configure it, we'll suppose the dkimproxy configuration is located in the /etc/dkimproxy directory instead of the default one.

mkdir -p "/etc/dkimproxy/private"
chown dkimproxy:root "/etc/dkimproxy/private"
chmod 700 "/etc/dkimproxy/private"
cd "/etc/dkimproxy/private"
openssl genrsa -out "private.key" 2048
openssl rsa -in "private.key" -pubout -out "public.key"
chown dkimproxy:root "private.key" "public.key"
chmod 600 "private.key"

And for /etc/dkimproxy/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    example.org

# specify what signatures to add
signature dkim(c=relaxed)
signature domainkeys(c=nofws)

# specify location of the private key
keyfile   /etc/dkimproxy/private/private.key

# specify the selector (i.e. the name of the key record put in DNS)
selector  pubkey

Then add a TXT entry for pubkey._domainkey.example.org with the value v=DKIM1; k=rsa; t=s; p=contenu_de_la_clef_publique (the available options are defined in RFC 6376).

Beware, with RSA keys of 2048 bits or higher the public key's length will exceed the 255 bytes limit of a TXT entry. As shown in RFC 7208, the solution is to split it into several strings that will be concatenated.

pubkey._domainkey.example.org.  IN  TXT  "v=DKIM1; k=rsa; t=s; p=MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDEjPmxQHTJeBoHSJPuPMl0ry0q31TqoC2OuqUiVHSk3hM6x6oDat4pIischVYi/3ODsjBC6wg1BPgtfdVfzboAF/bQK+Kh0rBaQlv2vloP96mu6dmUGMJSo5BrtoAMdx6DbMe7s7TP58uAUknQNEL6s9w7iNF9gD/+yBiEB+yDYwIDAQAB"

At last, let's configure OpenSMTPD to have him use dkimproxy.

listen on lo port 10028 tag DkimOut

action to_dkimproxy relay host smtp://127.0.0.1:10027
action relay_out relay helo example.org

match from any tagged DkimOut for any action relay_out
match for any action to_dkimproxy
match from any auth for any action to_dkimproxy

Anti-spam

The best way to fight spam is to use rspamd since it is perfectly integrated in OpenSMTPD using filter-rspamd. Once the filter directive is declared, you can simply add it at the end of the listen directives where emails coming from untrusted sources will be received.

filter "rspamd" proc-exec "/usr/lib/smtpd/opensmtpd/filter-rspamd"

listen on 203.0.113.42 tls pki example.org hostname mx1.example.org filter "rspamd"
listen on 2001:db8::42 tls pki example.org hostname mx1.example.org filter "rspamd"

Note that the filter-rspamd executable path may vary depending on the distribution you are running.

The MUA

The configuration of your email client should be done as follows:

Incoming server

  • Name/address: your domain name
  • Protocol: IMAP
  • Security: SSL/TLS
  • Port: 993
  • Authentication method: normal password
  • Username: your username (no alias or full email)
  • Password: your account's password

Outgoing server

  • Name/address: your domain name
  • Security: SSL/TLS
  • Port: 465
  • Authentication method: normal password
  • Username: your username (no alias or full email)
  • Password: your account's password

Summary

Receiving an email

Réception d'un email

  1. OpenSMTPD accepts the email comming from Bob;
  2. OpenSMTPD gives it to rspamd in order to detect whether or not it's a spam;
  3. OpenSMTPD sends it to Dovecot via LMTP;
  4. Dovecot puts the email in John's ~/Maildir folder (not illustrated: it uses sieve);
  5. John look his emails;
  6. Dovecot fetches the new email from Bob;
  7. Dovecot transfers the email to John's email client.

Sending an email

Émission d'un email

  1. John sends his email to OpenSMTPD;
  2. OpenSMTPD gives the email to DkimProxy;
  3. DkimProxy gives it back to OpenSMTPD after signing it;
  4. OpenSMTPD sends the now signed email to the recipients.

OpenSMTPD configuration

#
# Encryption
#

pki example.org cert "/etc/acmed/certs/example.org_ecdsa-p384.crt.pem"
pki example.org key  "/etc/acmed/certs/example.org_ecdsa-p384.pk.pem"


#
# Tables
# If you edit a file, you have to run `smtpctl update table <table_name>`
#

table vusers  "file:/etc/smtpd/vusers"
table aliases "file:/etc/smtpd/aliases"


#
# Filters
#

filter "rspamd" proc-exec "/usr/lib/smtpd/opensmtpd/filter-rspamd"


#
# Listening
#

# SMTP port 25 from localhost
listen on 127.0.0.1 tls pki example.org hostname mx1.example.org
listen on ::1       tls pki example.org hostname mx1.example.org

# SMTP port 25 from external sources
listen on 203.0.113.42 tls pki example.org hostname mx1.example.org filter "rspamd"
listen on 2001:db8::42 tls pki example.org hostname mx1.example.org filter "rspamd"

# SMTPS port 465 from authenticated users
listen on 203.0.113.42 smtps pki example.org auth <vusers> hostname mx1.example.org mask-src
listen on 2001:db8::42 smtps pki example.org auth <vusers> hostname mx1.example.org mask-src

# signed by dkimproxy
listen on lo port 10028 tag DkimOut


#
# Actions
#

action local_deliver lmtp "/run/dovecot/lmtp" alias <aliases>
action to_dkimproxy  relay host smtp://127.0.0.1:10027
action relay_out     relay helo example.org


#
# Matches
#

# Deliver emails
match from local for local action local_deliver
match from any for domain "example.org" action local_deliver

# Relay outgoing emails when signed
match from any tag DkimOut for any action relay_out

# Send outgoing unsigned emails to dkimproxy
match for any action to_dkimproxy
match from any auth for any action to_dkimproxy

Tests

Some tools can help you to check whether or not your configuration is working as expected:

Other emails with an auto-reply to test DKIM and SPF

In order to be sure the emails you send have been signed and SPF is correctly configured, you can send an email to check-auth@verifier.port25.com (IPv6 + IPv4).

IPv6 support

To check if your server handles IPv6, you can send an email to test@doesnotwork.eu and wait for the auto-reply.