I’ll always read ALPN as Alpine

I run Nextcloud on my Raspberry PI and I have a certificate from Let’s Encrypt on it. I set this up a couple years ago with certbot and it’s been fine. I don’t expose port 80 in my environment so I’ve relied on the TLS-SNI-01 challenge using the certbot client which ships on Raspbian Jessie.

TLS-SNI-01 is now end-of-life because of security vulnerabilities so I needed to find an alternative. There are a couple different challenge methods available. Let’s Encrypt’s cerbot supports two:

  • HTTP-01: a random file is placed in the webroot of your server
  • DNS-01: a random TXT record is added to your DNS

Neither of these was a great option for me. Routing port 80 traffic really wasn’t something I wanted to do. The DNS option is feasible, but my current registrar doesn’t offer a good API I’d either have to change registrars or introduce a manual step to the renewal process.

Fortunately, there’s another option, but it involves a little work setting up: TLS-ALPN-01. Certbot doesn’t support it, but other clients do. After some trial-and-error, I settled on dehydration after reading a great Medium post by Sam Decrock.

Getting the Pi ready

One thing I’d noted during this whole process, though in the end it wasn’t relevant, is that the shipped version of certbot in Raspbian Jessie was really old, and newer versions required Python 3.5 or higher, but Jessie was stuck at 3.4+. Dehydration’s sample ALPN responder also depends on the ALPN support added to the ssl module in Python 3.5.

Stretch has been available for a year and half; I’d built my Pi maybe six months prior. The core instructions for a dist-upgrade went smoothly though it took a couple hours. The only oddity was that the wired network interface wasn’t available at boot. I added auto eth0 to /etc/network/interfaces to resolve the issue.

Setting up dehydration

I followed Decrock’s post closely for configuring dehydration. I dumped everything into /etc/dehydrated. The sample domains.txt has varous sample configurations; I replaced all of it with a single line containing my hostname. The sample responder worked out of the box once I had Python 3.5 available. The one gotcha is that you have stop Apache/Nginx/whichever before running the responder so that it can listen on port 443.

Migrating Apache

Apache was already configured to use the certbot-issued certificates. These were in /etc/letsencrypt/live/yourdomain/; dehydration’s were in /etc/dehydration/certs/yourdomain. Changing the paths in the default host configuration worked fine. I noted that dehydration didn’t appear to have anything comparable to the configuration block in /etc/letsencrypt/options-ssl-apache.conf (ciphers and such), so I copied that directly into the virtual host configuration. Apache started cleanly on the first try.

Automation and cleanup

Let’s Encrypt certificates have a shelf life of three months, and this is a recurring task I don’t want. The renewal process looks like this:

  1. Stop apache
  2. Start the responder on port 443
  3. Execute the renewal request
  4. Stop the responder
  5. Start apache

This isn’t as simple as the TLS-SNI-01 challenge and does involve a little bit of downtime. If we’re not in the renewal window this takes ~3-5 seconds; if it renews the certificate it’s more like 20 seconds. I wrapped up the whole process in a shell script, using nohup to background the responder task:

#!/bin/bash
/bin/systemctl stop apache2
/usr/bin/nohup /usr/bin/python3 /etc/dehydrated/alpn-responder.py > /dev/null 2>&1 &
alpnPID=$!
/etc/dehydrated/dehydrated -c -f /etc/dehydrated/config
kill $alpnPID
/bin/systemctl start apache2

The magic part here is getting the PID from the responder process so that we can safely kill it (and not anything else!) once the renewal task is complete. I scheduled this for the wee hours of the morning. Finally, I uninstalled the now-updated certbot package because I don’t need it anymore and it won’t work going forward anyway.