Renewing Let's Encrypt Certificates with NGINX Unit
Tags: webdev, ssl, bash, cron
Published: 2024-05-02 00:13:50

Recently, I moved the [DjangoTricks website](https://www.djangotricks.com/) and started [PyBazaar](https://www.pybazaar.com) on servers with Nginx Unit. One thing that was left undone was SSL certificate renewals. Let's Encrypt has special `certbot` parameters for renewing certificates for websites on Apache or Nginx servers, but they don't work out of the box with the Nginx Unit. In this blog post, I will tell you how to do that.

## The certificate bundle

Nginx Unit doesn't use the `fullchain.pem` and `privkey.pem` generated by `certbot` directly from the location where they were generated. Instead, one has to create a bundle (like `bundle1.pem`) by concatenating them and then uploading it to the Nginx Unit configuration endpoint.

## The bash script

For that, I created a bash script:

```bash
#!/usr/bin/env bash
SECONDS=0
CRON_LOG_FILE=/var/webapps/pybazaar/logs/renew_certificate.log

echo "=== Renewing Letsencrypt Certificate ===" > ${CRON_LOG_FILE}
date >> ${CRON_LOG_FILE}

echo "Renewing certificate..." >> ${CRON_LOG_FILE}
certbot --renew-by-default certonly -n --webroot -w /var/www/letsencrypt/ -m hello@pybazaar.com --agree-tos --no-verify-ssl -d pybazaar.com -d www.pybazaar.com

echo "Creating bundle..." >> ${CRON_LOG_FILE}
cat /etc/letsencrypt/live/pybazaar.com/fullchain.pem /etc/letsencrypt/live/pybazaar.com/privkey.pem > /var/webapps/pybazaar/unit-config/bundle1.pem

echo "Temporarily switching the Unit configuration to a dummy one..." >> ${CRON_LOG_FILE}
curl -X PUT --data-binary @/var/webapps/pybazaar/unit-config/unit-config-pre.json --unix-socket /var/run/control.unit.sock http://localhost/config

echo "Deleting old certificate from Nginx Unit..." >> ${CRON_LOG_FILE}
curl -X DELETE --unix-socket /var/run/control.unit.sock http://localhost/certificates/certbot1

echo "Installing new certificate to Nginx Unit..." >> ${CRON_LOG_FILE}
curl -X PUT --data-binary @/var/webapps/pybazaar/unit-config/bundle1.pem --unix-socket /var/run/control.unit.sock http://localhost/certificates/certbot1

echo "Switching the Unit configuration to the correct one..." >> ${CRON_LOG_FILE}
curl -X PUT --data-binary @/var/webapps/pybazaar/unit-config/unit-config.json --unix-socket /var/run/control.unit.sock http://localhost/config

echo "Finished." >> ${CRON_LOG_FILE}
duration=$SECONDS
echo "$(($duration / 60)) minutes and $(($duration % 60)) seconds elapsed." >> ${CRON_LOG_FILE}
```

Once you have adapted the script, you can run it manually as a root user to test it:

```bash
$ chmod +x renew_certificate.sh
$ ./renew_certificate.sh
```

Note that the `certbot` command will try to validate your website's URL by attempting to reach a temporary file that it will create on `http://example.com/.well-known/acme-challenge/`, so make sure that this location is accessible and serving the static files.

For more details about the Nginx Unit, check my [previous blog post](https://djangotricks.blogspot.com/2024/02/django-project-on-nginx-unit.html).

## The cron job

If everything works as expected, you can add it to the root user's cron jobs to be executed weekly.

Export the current root cron jobs to a `crontab.txt`:

```bash
$ crontab -l > crontab.txt
```

Then edit it and add the weekly script to update the SSL certificate:

```
PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/snap/bin
SHELL=/bin/bash
MAILTO=""
@weekly /var/webapps/pybazaar/unit-config/renew_certificate.sh
```

Then run the following as the root user to apply it:

```
$ crontab crontab.txt
```

The good thing about not editing the cron job with `crontab -e` is that you can choose the editor and even put the `crontab.txt` under Git version control.

Happy web development with WSGI or ASGI!

---

Cover picture by [Gotta Be Worth It](https://www.pexels.com/photo/assorted-color-metal-keys-1160845/)