Secure Your letsencrypt Setup With acme-tiny

Using acme-tiny for an even more secure letsencrypt setup

 ·  4 minutes read

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 ...
}

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.

letsencrypt security server