Jordan Brown

Principal Software Engineer

← Back

Secure Wildcard Let's Encrypt TLS Certificates Using DNS Subdelegation

I can read this blog post to you thanks to AI.

Introduction

I’ve been using wildcard TLS certificates for my home servers for a while now. My setup involves a virtual machine whose only job is to obtain and serve certificates to other machines on my network. It runs NGINX with basic auth, and each server that needs a certificate has a small script that pulls the private key and cert over HTTPS, then installs it in the right place — whether that’s for a web server, mail server, or something else.

The part that’s been breaking down is renewals. Let's Encrypt certs only last 90 days, and I’ve been doing that part manually. I meant to automate it, but since my DNS provider doesn’t offer scoped API tokens, I didn’t feel comfortable storing full domain access credentials on a machine exposed to the internet. And inevitably, I ignore the renewal reminders and forget — which leads to downtime, always at the worst possible time.

Recently, I came across something called DNS subdelegation, and it felt like just the workaround I needed.

What Is DNS Delegation?

DNS delegation lets you assign responsibility for a subdomain to a different nameserver. So for instance, if you wanted a server to manage records for user1.yourdomain.com, you could set an NS record like:

user1.yourdomain.com NS dns01.externaldnsprovider.com

That way, the external DNS provider can manage all records under user1.yourdomain.com without needing access to the parent domain.

This turns out to be super useful when you want to securely automate DNS-based domain validation (i.e., the DNS-01 challenge that Let's Encrypt uses for wildcard certs).

Why Subdelegate?

Some DNS providers offer APIs, but the ones I’ve used don’t let you scope tokens to just one subdomain — they’re all-or-nothing. That means a single compromised token could let someone hijack your entire domain.

With subdelegation, I can punt control of something like _acme-challenge.yourdomain.com to a totally different DNS provider. That provider can be API-accessible, scriptable, and isolated — and even if its credentials are stolen, the worst-case scenario is someone issuing rogue TLS certs (which I can revoke). Much better than losing control of DNS entirely.

Automating Wildcard TLS with DNS-01 Challenge

I chose Hetzner DNS for this because:

I stumbled upon this guide on Hetzner’s community site, written by dschoeffm, which includes two scripts — one to create the TXT record needed for the DNS challenge, and one to clean it up afterward. I’m lazy, so I only use the one that adds the record.

Here’s the script:

# Script by @dschoeffm on GitHub
# https://raw.githubusercontent.com/dschoeffm/hetzner-dns-certbot/master/certbot-hetzner-auth.sh

#!/bin/bash

token=$(cat /etc/hetzner-dns-token)
search_name=$( echo $CERTBOT_DOMAIN | rev | cut -d'.' -f 1,2 | rev)

zone_id=$(curl \
    -H "Auth-API-Token: ${token}" \
    "https://dns.hetzner.com/api/v1/zones?search_name=${search_name}" | \
    jq ".\"zones\"[] | select(.name == \"${search_name}\") | .id" 2>/dev/null | tr -d '"')

curl -X "POST" "https://dns.hetzner.com/api/v1/records" \
    -H 'Content-Type: application/json' \
    -H "Auth-API-Token: ${token}" \
    -d "{ \"value\": \"${CERTBOT_VALIDATION}\", \"ttl\": 300, \"type\": \"TXT\", \"name\": \"_acme-challenge.${CERTBOT_DOMAIN}.\", \"zone_id\": \"${zone_id}\" }" > /dev/null 2>/dev/null

# just make sure we sleep for a while (this should be a dig poll loop)
sleep 30

And the command I run to actually get the cert looks like this:

sudo certbot certonly \
--manual \
--manual-auth-hook /path/to/certbot-hetzner-auth.sh \
--preferred-challenges=dns \
--email [email protected] \
--server https://acme-v02.api.letsencrypt.org/directory \
--agree-tos \
--manual-public-ip-logging-ok \
-d *.yourdomain.com

The --manual-auth-hook flag tells certbot to run my DNS update script at the right time, passing in environment variables like CERTBOT_DOMAIN and CERTBOT_VALIDATION. After a short wait (30 seconds in my case), certbot checks that the record has propagated, and proceeds.

Once the cert is issued, it ends up in:

/etc/letsencrypt/live/yourdomain.com/

From there, I copy it to the local NGINX instance that serves as my internal cert distribution server. Other VMs on the LAN fetch it daily via HTTPS and install it wherever they need it.

Wrapping Up

There’s probably a cleaner way to do all of this with something like Vault or a full-featured secrets manager, but for my little home lab, this works fine. I don’t mind the bit of scripting glue, and subdelegating _acme-challenge means I get automation without handing over the keys to my entire DNS kingdom.

And most importantly: no more surprise downtime because I forgot to renew something.