5 minutes
Managing Lets Encrypt Certificates With Ansible and Dehydrated
Background
I use the not-for-profit certificate authority (CA) Lets Encrypt to provide TLS certificates for a number of websites and services that I host on a number of virtual private servers and on my DMZ server.
I mentioned to a colleague that I was looking for a way to manage Lets Encrypt certificates via Ansible and he suggested I take a look at Dehydrated. In this write-up I share the Ansible configuration that I wrote to setup Dehydrated when (re-)provisioning my hosts.
If you have an interest in automating the provisioning of Lets Encrypt certificates, this write-up should be useful.
Ansible Role
Using Dehydrated to obtain Lets Encrypt certificates on a number of hosts, I created a dehydrated
Ansible role. This is published over on GitLab.
In one situation, I have a pair of servers configured by Ansible, one of which runs a web server to receive dynamic DNS updates. In the vars/main.yml
file is the setupDehydrated
variable which is set to true
.
If you find yourself in a similar situation you can pull this variable into host_vars
or into your Ansible inventory and use it accordingly.
You may want to issue a certificate to the first server in a set of load balanced servers, and then copy or synchronize it to subsequent servers, in which case the setupDehydrated
variable might also be useful.
Tasks
The role is subdivided into two tasks:
- include: packages.yml
when: setupDehydrated | bool
- include: installAndConfigureDehydrated.yml
when: setupDehydrated | bool
packages.yml
The first task installs the prerequisite packages. These are namely a flavour of nginx for Lets Encrypt HTTP-01 ACME challenge and Git for checking out the Dehydrated GitHub repository.
The flavour of nginx to install is determined by the variable nginxPackage
defined in vars/main.yml
. On Debian the value of this would typically be nginx-full
or nginx-light
. In scenarios where you would prefer to run a lighter weight web server, for example when serving up simple web pages, you might prefer to use nginx-light
.
In my Ansible playbooks, I set this variable in group_vars
. In the case where you are adopting this role to setup Lets Encrypt on a single server, then you can simply leave it where it is.
installAndConfigureDehydrated.yml
This is the business end of the role. It is probably best to read through the task first hand. In summary, here is what this task does:
- Creates the
dehydrated
user and group - Creates an
/etc/dehydrated
configuration directory - Copies the
domains.txt
file to the configuration directory. See the domains.txt section. - Copies the dehydrated configuration template into place. See the config section.
- Creates accounts, certificates and ACME challenge directories
- Creates an nginx ‘ssl’ directory
- Configures nginx to serve the ACME challenge directory from the
.well-known/acme-challenge
location from the default site on port 80. See the caveats section. - Sets up [dehydrated-hook.sh](#dehydrated-hook.sh]
- Accepts the CAs terms-of-service
- Generates a certificate
- Installs the a
cron.weekly
script for certificate renewal - Installs a
sudoers
file allowing some commands fromdehydrated-hook.sh
to run as root, for example restarting nginx
domains.txt
files/examplehost/etc/dehydrated/domains.txt
This file contains a list of domains for which the created certificate should be used. The first domain is setup as the CN (Common Name) as well as a SAN (Subject Alternative Name) and the subsquent domains are setup as additional SANs. Here is an example from one of my VPS Servers:
caramel.biscuit.ninja biscuit.ninja dcadden.co.uk cdn.biscuit.ninja
And the resulting certificate:
The domains.txt
file for this VPS would be put into the files/caramel/etc/dehydrated/
folder because the host is called caramel.biscuit.ninja
and the file is sought by the installAndConfigureDehydrated.yml
task using the {{ inventory_hostname_short }}
Ansible variable.
This means that the role can be applied to multiple hosts by simply including a domains.txt
file for each applicable host and making the necessary changes to your playbook.
For example, for two servers, foo.example.com
and bar.example.com
, you would include the following two files within your copy of the role:
files/foo/etc/dehydrated/domains.txt
files/bar/etc/dehydrated/domains.txt
And in your playbook you might have something like:
- name: apply vpsWeb configuration to vpsWeb servers
hosts: vpsWeb
roles:
- dehydrated
- vpsWeb
where both foo.example.com
and bar.example.com
are in the vpsWeb
group in the inventory file:
[vpsWeb]
foo.example.com
bar.example.com
config
The /templates/etc/dehydrated/config.j2
file contains all of the Dehydrated (Lets Encrypt/ACME) configuration options. There is an example configuration file in the Dehydrated GitHub repository. Reading that will help explain some of the configuration options.
When testing your implementation of the Ansible Dehydrated, switch the CA to the acme-staging-v02
API in order to avoid being rate limited in response to multple Ansible pushes.
The config
file requires a contact email address. This is templated in from the acme_contact_email
variable which is included in the vars/main.yml
file.
dehydrated-hook.sh
Dehydrated supports a hook, which is a script that can be run as a result of running the Dehydrated script and, for example, deploying a new certificate. /files/usr/local/bin/dehydrated-hook.sh
will reload nginx when a new certificate is issued (deploy_cert
).
There are empty handlers in that file for a number of other tasks, for example sync_cert()
, which can be used to flush to disk when a new certificate is created but before they are symlinked, which might be useful in some situations.
Caveats
This role:
- Assumes that you are not going to serve content from the nginx default server and will create specific servers for hosting your sites and services with a
server_name
directive. - Makes no provision for the export or re-use of Lets Encrypt account keys.
Conclusion
If you find Dehydrated to be useful in deploying Lets Encrypt TLS certificates to your environment, then I encourage you to buy the developer a ‘coffee’.
Ansible is a great tool for automating the provisioning of servers and environments and most importantly, for getting that configuration into a version control system. I have shared a number of examples over on GitLab which you might find useful.