Postfix, Dovecot, and Let's Encrypt certificates

Previously I'd been using StartCom for my web and mail server certificates, but due to recent trust issues and also to automate the renewal process I've decided to switch over to Let's Encrypt. Let's Encrypt is the current best source for free basic SSL certificates. While their service is geared toward web servers, they can also be used by any other service that uses SSL with domain name validation, such as mail servers.

I've already taken care of my different websites, that was pretty easy using their certbot client along with the apache plugin to manage the configurations. I did have to redo my configuration slightly to make sure I had one domain per configuration file but that was a relatively minor annoyance.

For mail services I have a separate VPS running Postfix and Dovecot. There is no webserver on this computer and I don't want to install one just for obtaining SSL certificates. This is where certbot's standalone plugin comes in handy. It will act as a webserver just for the renewal process then shutdown once complete.

Install certbot

certbot is the official tool for managing your Let's Encrypt certificates and keeping them renewed. There are other options available, but certbot will do the job and is well documented so I'm just going to go with that.

Convenient instructions for how to install certbot on a number of different systems are provided on their site. Go there and get it installed on your system of choice. My VPS is running Ubuntu 16.04 so that is what this guide will focus on.

apt-get install software-properties-common
add-apt-repository ppa:certbot/certbot
apt-get update
apt-get install certbot

Configure the firewall

Because my VPS only hosts mail, I currently have the the firewall configured to block the standard web ports. For certbot to do it's job it will need to be able to receive connections on port HTTPS (443).

To get started I'll need to open up this port using iptables and ip6tables.

iptables -I INPUT -p tcp --dport 443 -j ACCEPT

# If you support IPv6
ip6tables -I INPUT -p tcp --dport 443 -j ACCEPT

Obtaining the initial certificate

To obtain the initial certificate, use the certonly sub-command along with the standalone plugin for verification. Add however many domains the certificate needs to cover by specifying the -d parameter multiple times. Do a dry-run first just to ensure everything is able to communicate and working.

certbot -n --dry-run --agree-tos --standalone certonly -d mail.example.com

With the dry-run successful, go ahead and obtain the actual certificate. To obtain the actual certificate an email address must also be provided. This is just so you can be contacted in the event anything happens with the certificate that would need your attention, such as it being revoked for example.

certbot -n --agree-tos --email you@example.com --standalone certonly -d mail.example.com

The newly obtained certificate will be stored at /etc/letsencrypt/live/mail.example.com. The symlinks in this directory will always point to the newest certificate so you can configure your software to load them directly and just reload/restart the software after updating the certificates.

Configure Postfix

If your postfix setup does not already use TLS, you'll need to set that up before continuing.

With a certificate successfully obtained and ready to go, it's time to update the postfix configuration. You can edit postfix's main configuration file (/etc/postfix/main.cf) or take advantage of the postconf command to make the changes for you.

The two configuration entries that need to be changed to use the new certificate are smtpd_tls_cert_file and smtpd_tls_key_file. This tells postfix where to find the certificate and key that it will use when talking to client and other mail servers.

Postfix needs both the server's certificate and the intermediate certificates so they can be presented to the clients for verification. Let's encrypt provides these all in a single file called fullchain.pem so use that when configuring the smtpd_tls_cert_file parameter.

postconf -e smtpd_tls_cert_file=/etc/letsencrypt/live/mail.example.com/fullchain.pem
postconf -e smtpd_tls_key_file=/etc/letsencrypt/live/mail.example.com/privkey.pem

Configure Dovecot

Dovecot's SSL configuration is done in an auxiliary file located at /etc/dovecot/conf.d/10-ssl.conf. In here you'll find two parameters that need to be changed: ssl_cert and ssl_key. Like postfix, dovecot will need the full certificate chain to present to clients for validation.

Edit the configuration file to point to the new certificates. Be sure to include the leading < before the file path, this is what tells dovecot to read from a file rather than use the value literally.

ssl_cert = </etc/letsencrypt/live/mail.example.com/fullchain.pem
ssl_key = </etc/letsencrypt/live/mail.example.com/privkey.pem

Automatic renewal

The Ubuntu package for certbot comes pre-configured with systemd timer that will automatically renew existing certificates. What it does not handle however is reloading postfix/dovecot so that they will begin using the new certificates. For that, we need to implement a hook.

Certbot has both pre and post hooks that you can use to execute a script prior to and after the renewal process. It also has a renew hook that is run whenever a certificate is successfully renewed.

Both the renew hook and post hook are good candidates for our reload script. Each has a downside however. The post hook will be run after every renewal attempt, regardless of if anything was actually renewed or not. This will result in the services being reloaded many times for no reason.

The renew hook only runs if a certificate was successfully renewed, but it will be run once for each certificate. This could mean reloading services multiple times if you have multiple certificates. If you only have a single certificate however it'll work great.

In my case I only have a single certificate, so the renew hook is what I'm going to use. To setup the hooks a configuration file for certbot needs to be created at /etc/letsencrypt/cli.ini. The configuration file consists of simple name=value pairs where the name is taken from the list of command line parameters.

To configure a renew hook, add the following to the configuration file:

renew-hook = /root/bin/certbot-renew

Next, create the renew hook script at /root/bin/certbot-renew with the following contents:

#!/bin/sh
systemctl reload postfix
systemctl reload dovecot

Firewall considerations

As I mentioned above, I don't normally have a webserver running and needed to open up the ports on the firewall in order to obtain the certificate. Since there is no server except when certbot is running, I'd like to keep the HTTPS port closed when not in use.

Thanks to the pre and post hooks mentioned above, this can be accomplished. Create a pre-hook script that will punch a hole into the firewall.

#!/bin/sh
/sbin/iptables -I INPUT -p tcp --dport 443 -j ACCEPT
/sbin/ip6tables -I INPUT -p tcp --dport 443 -j ACCEPT

Then create a post-hook script that will close it back up.

#!/bin/sh
/sbin/iptables -D INPUT -p tcp --dport 443 -j ACCEPT
/sbin/ip6tables -D INPUT -p tcp --dport 443 -j ACCEPT

Finally add these scripts to your certbot configuration file:

pre-hook = /root/bin/certbot-pre
post-hook = /root/bin/certbot-post