I’ve been looking into migrating to Let’s Encrypt for a while now, but due to my server setup for some reason the webroot method just wasn’t working for me (and is ugly in general). For a very long time I’ve always validated my domains ownership via a DNS TXT record and I find that much more elegant (personal preference).

I didn’t realize Let’s Encrypt had a --preferred-challenges dns option, which as far as I know only works with the manual plugin. I tried it. It was ugly. The reason being it was, obviously, a manual process. Therefore every time you run the certbot command you need to manually edit your TXT record with the value that will be generated.

I usually tend to stay away from third-party solutions, but from the sparse documentation I’ve seen dehydrated seemed like the way to go. It seems pretty well vetted so I’ll go with that. Here’s how my setup ended up looking like.

git clone https://github.com/certbot/certbot.git /opt/certbot
git clone https://github.com/lukas2511/dehydrated.git /opt/dehydrated
# I lied, I first forked the above repo and cloned my fork because no real reasons
cd /opt/dehydrated
git clone https://github.com/NatsumiHoshino/letsencrypt-cloudflare-hook.git hooks/cloudflare
pip install -r hooks/cloudflare/requirements.txt

Next, I simply need to create a bash script for each domain I want to validate. It would look something like:

#!/usr/bin/env bash
export CF_EMAIL='<cloudflare email>'
export CF_KEY='<cloudflare key>'

/opt/dehydrated/dehydrated \
        --cron \
        --domain mydomain.example.com \
        --challenge dns-01 \
        --hook '/opt/dehydrated/hooks/cloudflare/hook.py'

Then this script can be run on as a service (my preferred method) or as a cron job.

However this is where it gets interesting. I have some devices on which it is not easy to install the Let’s Encrypt client, such as my router or printer (because why not?). However, with this approach, I can validate ANY domain I own. For my router, I append the following to the above script:

if [ /mnt/my-cifs-share/cert.pem -ot /opt/dehydrated/certs/router.example.com/fullchain.pem ]; then
        logger "[certbot] A new cert has been created, uploading"
        cp -f /opt/dehydrated/certs/router.example.com/fullchain.pem /mnt/my-cifs-share/cert.pem
        cp -f /opt/dehydrated/certs/router.example.com/privkey.pem /mnt/my-cifs-share/key.pem
        logger "[certbot] Cert still valid"

What this does is copy the new certificate and private key to a mounted share if the file is newer, in other words if a certificate renewal has occurred. On the router side, I handle the remaining tasks:

if [ /cifs1/cert.pem -nt /tmp/etc/cert.pem ]; then
        logger "[certbot] New cert found, updating"
        cp -f /cifs1/cert.pem /tmp/etc/cert.pem
        cp -f /cifs1/key.pem /tmp/etc/key.pem
        tar -C / -czf /tmp/cert.tgz /tmp/etc/cert.pem /tmp/etc/key.pem
        nvram setfb64 https_crt_file /tmp/cert.tgz
        nvram commit
        logger "[certbot] Restarting httpd"
        service httpd restart
        rm /tmp/cert.tgz
        logger "[certbot] Cert valid, exiting"

I have this script scheduled to run twice daily and it checks for a new certificate in the mounted share. If the certificate has been renewed it will copy the new files over, save the new certificate to nvram, and restart httpd. As usual though I’ll have to wait until the next renewal to see if things go as planned…