Using Let's Encrypt with Supervisor

Learn how to use Supervisor to automate Let's Encrypt certificates in a Docker-friendly way

Let’s Encrypt with Supervisor

Let’s Encrypt works great with Supervisor, as it provides easy orchestration and some basic scheduling that the certificate management requires. Configuring it is also not rocket science; just identify the environment your app is running in, and choose a suitable workflow. This can even be done with minimal changes to your app, and in a Docker-friendly way.

For automating Let’s Encrypt, you generally need 3 program:

  1. The app itself (Apache, Node.js, or whatever you use)
  2. An initial cert issuance script. This will run when you start the server
  3. A renewal script that handles getting new certificates

A basic supervisord.conf which we’ll customize looks like this:

[supervisord]
nodaemon=true

[program:app]
command=...start

[program:letsencrypt]
command=/bin/bash -c "certbot certonly --keep-until-expiring ...other parameters"
autorestart=false

[program:letsencrypt-renew]
command=/bin/bash -c "sleep 1d && certbot renew ...other parameters"
autostart=false
autorestart=true

Depending on the authorization flow you use, the exact parameters and when each program start/stop differ.

The app is to be managed by supervisor. This is usually the case, but for example Apache spawns child processes and that makes it out of the scope of supervisor. Make sure that supervisorctl restart app and supervisorctl stop app works.

The letsencrypt program handles the initial certificate. It does not autorestart, as it is run only once. The --keep-until-expiring flag makes sure that if there is an existing certificate, it will use that instead of getting a new one.

And finally, letsencrypt-renew runs every day and does the renewals. It autorestarts, but does not autostarts, so it needs a kickoff.

Flow #1: Get the certificate before launching the app, shut down during renewal

This is the easiest case. The app runs neither during getting a certificate, nor when a renewal is happening.

Part #1: run letsencrypt then start app

The only program that autostarts is the letsencrypt, and it starts both the app and the letsencrypt-renew.

[program: app]
...
autostart=false

[program:letsencrypt]
command=/bin/bash -c "certbot certonly --keep-until-expiring --standalone --agree-tos ... --staging -d $CERT_URL && supervisorctl start app letsencrypt-renew"
...

Do not use --post-hook or --deploy-hook, as they won’t run if a valid certificate already exists. Instead, use the ... && ... construct, that runs every time after the program is finished.

Part #2: Stop app -> renew -> start app

The other part is to configure how to renew the certificate. In this case, during renewal, the app is not running. To achieve this, use the --pre-hook and --post-hook.

[program:letsencrypt-renew]
command=/bin/bash -c "sleep 1d && certbot renew --pre-hook 'supervisorctl stop app' --post-hook 'supervisorctl start app'"
...

Since it was not defined, renewal also uses the standalone auth.

Flow #2: Start the app, restart on new certificate

If you can configure your app so that Let’s Encrypt can use the webroot auth, then you can start it and only need to restart on new certificates.

Make sure you handle the case when you have no valid certificates, for example, use a self-signed one.

In this case, app and letsencrypt autostarts.

[program: app]
...
autostart=true

[program:letsencrypt]
command=/bin/bash -c "certbot certonly --keep-until-expiring ... --staging -d $CERT_URL --deploy-hook 'supervisorctl restart app' && supervisorctl start letsencrypt-renew"
autostart=true

[program:letsencrypt-renew]
command=/bin/bash -c "sleep 1d && certbot renew"
autostart=false
...

Use --deploy-hook to restart the app, as most server software won’t pick up a new certificate automatically. And since the renewal flow works the same, it does not need any parameters.

There is one problem with this setup. A race condition is present between the app and letsencryt, as the latter needs the former to run. If your app needs more time to start serving static files, you need to start letsencrypt after that with supervisorctl start letsencrypt instead of autostarting it.

Another solution is to use a sleep before letsencrypt, like sleep 5m && .... But that hardcodes an upper limit, which is fragile.

A better solution is to use a script that waits for the server to start, like the wait-on that checks a HTTP service:

command=/bin/bash -c "wait-on https://app/health-check && certbot certonly ... "

This will ping the parameter URL and only run letsencrypt when it is alive.

10 July 2018

Interesting article?

Get hand-crafted emails on new content!