Since 2015, with Let’s Encrypt there is finally a way of HTTP encryption for everyone for free with the comforting green lock in the browser. And while it might be feasible for tech people to go with self-signed certificates, for ordinary people it is probably alarming to have the browser tell them that this website is about to do something bad.

The default client has recieved some criticism. In particular because it wants to run as root and heavily pulls dependencies. Since I share the opinion, that this client is too heavy, I also don’t want to use it. As alternative there is a bunch of implementations to choose from. I go with acme-tiny, a one-file python script (without dependencies) and my own ruby script around it (also dependency free).

Certificate Strategy

Since end of March 2016, you can register 20 subdomains per domain and week (rate limits). Also, you can renew the certificate for the same set of domains upto 5 times per week. This means, for having 40 domains, you can register 1.example.com, 2.example.com, …, 20.example.com in week 1, and in the next week the remaining 21.example.com, 22.example.com, …, 40.example.com.

Before, due to more strict rate limits, I used one cert for every subdomain for a domain. This meant, my blog had the same cert as my mailserver, and this list of subdomains was also exposed via the certificate, which I find unneccessary. Now I have a seperate cert per logical server unit, which somewhat pleases my aesthetics. This also means, if for some reasons I want to switch one of the certificates to another provider than Let’s Encrypt, I can just exchange that one certificate without touching the others or having a legacy domain hanging in them.

Requesting and Signing

I use acme-tiny and a ruby script around it to manage my certs. Everything is run as unprivileged user with home directory /etc/acme-tiny. In there are these folders:

/etc/acme-tiny
├── certs         # the bundled certs
├── challenges    # here the http-challenge files are placed
├── csrs          # the signing requests
├── private       # well, private keys
├── repo          # the acme-tiny script repository
└── temp          # the non-bundled and Let's Encrypt certs

The basic workflow is as follows:

  1. Create all CSRs according to the configuration. After this, the csrs-folder is populated.
  2. Sign the requests. This is done via a call to acme-tiny during which the responses to the acme challenges are placed in the challenges-folder. Afterwars, the signed certificates lay around in the temp-folder.
  3. Get the intermediate certificate from Let’s Encrypt and bundle every previously signed certificate with it. The result is placed into the certs-folder.

The configuration is a list of lists, for instance:

[
  %w[example.org, www.example.org, a.example.org],
  %w[example.com, www.example.com, b.example.com]
]

Every entry in the outer lists corresponds to a certificate. Each entry of an inner list stands for a domain name included in this certificate. The first entry of the inner lists determines the name of the generated files. In this case: csrs/example.org.csr, csrs/example.com.csr, temp/example.org.crt, temp/example.com.crt, certs/example.org.crt, certs/example.com.crt.

The Script

You find the whole script here. My considerations when making it were in making it as self-contained as possible (no additional configuration file or database) and the state of the certificates discoverable. Also I should be able to use it in a “fire-and-forget”-manor and I don’t want to look for some obscure openssl parameters. Thus, the actions it should be capable of are: listing all certificates and their state, creating new requests and validating certificates.

Listing certificates: this should provide some basic information like the domains for every certificate and how long it is valid, should be also a starting point for debugging. Those “should”s show that I have not added this to the script yet (and I might never do), but it would be nice. For now it only shows the list of domains with some id and these ids are then used as parameters for the next commands.

Creating new certificate signing requests: in case when I want a new certificate or change the (sub-)domains assigned to a certificate. This is basically a wrapper around the openssl command which is invoked to create a csr for the given domains.

Validating: this is the part of the script which is used to initially get a signed certificate and also can be executed on a regular basis for renewal. It uses the acme-tiny script in order to run the acme protocol and get the signed certificate. It also takes care of bundling the domain certificate with Let’s Encrypt’s intermediate certificate.

For both, csrs and certs, the script backs up the previous versions rather than overriding them, in case something goes wrong. Also I only need bundled certificates for all servers I am currently using, thus the non-bundled certs life in the temp directory.

The Rest

In order to let the acme-tiny script do its work, the path http://$DOMAIN/.well-known/acme-challenge/ has to point to the challenges-folder, for every value of $DOMAIN you want to have a certificate for (6 in the above example). For my Nginx I have a snippet added to every ssl-enabled server which consists only of this line:

rewrite     ^/.well-known/acme-challenge/(.*)$ http://example.com$request_uri? redirect;

And in the server-block for example.com set the location accordingly:

location /.well-known/acme-challenge/ {
    alias /etc/acme-tiny/challenges/;
    try_files $uri =404;
}

This precise definition of the path of the URI is necessary, because under .well-known it is common to have other information too, e.g., for CalDAV and CardDAV.

Further, since the certs singed by Let’s Encrypt are only valid for 90 days, a cronjob calls the ruby script every two months.