In my previous post about letsencrypt I explained how to run the official client as an unprivileged user. While this is a major security improvement over the default setup it is insufficient.
As was mentioned in the previous post I have since switched to acme-tiny (and created an AUR package for it). In this post I will explain how to set up letsencrypt in an even more secure manner using that.
Parts of this tutorial are based on the acme-tiny README. Many thanks to the original author for both the software and the readme.
This post supersedes the previous one, you only need to follow one of them.
Why bother?
Security.
You should strive to run every process with the lowest privileges possible. This both reduces the chances of a bug causing data loss, and more importantly, a security issue leading to a compromise.
Using acme-tiny enables you to limit access to your private keys in addition to running the process as a unprivileged user.
How is this better than unprivileged letsencrypt (previous post)?
Running the official client as an unprivileged user protects your system from compromise, but it falls short in protecting your private key. Because the official client also generates your private keys, it, by definition, has access to them. While your server may not get compromised, your private keys might, which is also very bad.
Preface
In this tutorial we will setup acme-tiny to run as an unprivileged user and properly configure permissions so it does not have access to our private key.
Definitions and assumptions:
- The domain: example.com
- The web server's web root: /srv/http/letsencrypt
- The acme-tiny directory: /etc/acme-tiny
- The private keys directory: /etc/ssl/keys
- Commands preceded by # should be run as root.
- Commands preceded by $ should be run as the letsencrypt user.
Preparing the environment
First we need to create an unprivileged user for letsencrypt. I chose letsencrypt. The following command will create a new system user without a login shell or a home directory.
# useradd --shell /bin/false --system letsencrypt
Now we will create the private keys directory, and make it only accessible to root. Feel free to change the location and user to whatever suits your setup.
# mkdir -p /etc/ssl/keys
# chmod 700 /etc/ssl/keys
Put the acme-tiny script in your path as acme-tiny
(or use the AUR package if on Arch) and make sure it's executable.
Put the acme-tiny-wrapper script in your path (or use the AUR package if on Arch) and make sure it's executable.
Finally, we will create the needed directory for the domain validation, and set the correct permissions.
# mkdir -p /srv/http/letsencrypt/.well-known/acme-challenge
# chown -R letsencrypt: /srv/http/letsencrypt
You need to setup your web server to serve this for any of your domains, for example for Nginx you can accomplish this by adding the following location
directive at the top of your server block.
server {
server_name example.com;
location /.well-known/acme-challenge/ {
root /srv/http/letsencrypt/;
try_files $uri =404;
}
# Snip ...
}
Optional: verify the web server can serve files created by our user:
$ echo works > /srv/http/letsencrypt/.well-known/acme-challenge/test.txt
$ curl example.com/.well-known/acme-challenge/test.txt
If the last command printed "works", everything works. Otherwise, something is wrong with your configuration. This is unfortunately out of scope for this tutorial, though feel free to contact me, maybe I can help.
Preparing acme-tiny
First we need to create a directory for our certificates to live in and set the proper permissions. I chose /etc/acme-tiny.
# mkdir -p /etc/acme-tiny/csr
# chown -R letsencrypt: /etc/acme-tiny
Create an account key
In order to use letsencrypt, we need a private key for the account.
The rest of the commands will be called from withing the /etc/acme-tiny directory.
$ cd /etc/acme-tiny
Option 1: Create a new account key
$ openssl genrsa 4096 > account.key
Option 2: Use existing letsencrypt account key
$ wget -O - "https://gist.githubusercontent.com/JonLundy/f25c99ee0770e19dc595/raw/6035c1c8938fae85810de6aad1ecf6e2db663e26/conv.py" > conv.py
$ cp /etc/letsencrypt/accounts/acme-v01.api.letsencrypt.org/directory/<id>/private_key.json private_key.json
$ openssl asn1parse -noout -out private_key.der -genconf <(python conv.py private_key.json)
$ openssl rsa -in private_key.der -inform der > account.key
$ rm conv.py private_key.json private_key.der
Create a certificate signing request and key (per domain)
Create your private key as root. No need to further restrict access to it, because we already restricted access to the directory.
# openssl genrsa 4096 > /etc/ssl/keys/example.com.key
There are two options for the certificate signing request, a certificate for a single domain, for example example.com, or a certificate for multiple domains, for example example.com and www.example.com in the same certificate.
You can put whatever domains you want on the certificate, but be aware that anyone accessing any of the domains will be able to see the whole list.
Option 1: Single domain certificate
# openssl req -new -sha256 -key /etc/ssl/keys/example.com.key -subj "/CN=example.com" > /etc/acme-tiny/csr/example.com.csr
Option 2: Multiple domains certificate
# openssl req -new -sha256 -key /etc/ssl/keys/example.com.key -subj "/" -reqexts SAN -config <(cat /etc/ssl/openssl.cnf <(printf "[SAN]\nsubjectAltName=DNS:example.com,DNS:www.example.com")) > /etc/acme-tiny/csr/example.com.csr
Running acme-tiny
Create as many certificate signing requests as you want, and put them in the csr directory, the script will automatically generate certificates for all of them.
$ acme-tiny-wrapper /srv/http/letsencrypt/.well-known/acme-challenge/
Now you will have a directory created in /etc/acme-tiny/live for every csr you had in the csr directory.
Note: You can set an alternative path to the acme-tiny binary by setting the ACME_TINY_BIN environment variable.
Verify everything worked
If you got here, you should already have your certificate issued. You can verify this by running:
$ ls /etc/acme-tiny/live/example.com
This should output cert.pem fullchain.pem
Certificate renewal
This section is here just for completeness. You should run the script in a cron
job so the certificate is renewed before it expires (at the time of writing, 3 months). I have a cron job running once a month.
The cron
job should run:
$ acme-tiny-wrapper /srv/http/letsencrypt/.well-known/acme-challenge/
Important: do not forget to make the server reload the certificates after they are renewed. Nginx for example, does not do this automatically.
Finishing notes
This tutorial does not cover setting up your web server to use the new certificates. This is very simple and covered at length elsewhere.
However, here is an example for nginx:
server {
server_name example.com;
# Snip ...
ssl_certificate /etc/acme-tiny/live/example.com/fullchain.pem;
ssl_certificate_key /etc/ssl/keys/example.com.key;
# Snip ...
}
Please let me know if you encountered any issues or have any suggestions.
Please let me know if you spotted any mistakes or have any suggestions, and follow me on Twitter or RSS for updates.