Diagram

Every few months I used to spend an afternoon issuing yet another certificate for yet another subdomain, pasting a token into some web form, and praying the private key made it across. Then a friend pointed out that one wildcard certificate plus a DNS challenge kills that whole chore for good. This post is the exact recipe I now use — copy-paste and tweak the domain name.


Why wildcard, and why DNS-01

A wildcard certificate is valid for *.example.org and every subdomain beneath it — api., blog., app., you name it — issued once, not per-host. The catch: Let’s Encrypt won’t issue a wildcard over plain HTTP, because there’s no single http://*.example.org URL to fetch a token from. That’s what forces the DNS-01 challenge, where ownership is proven through the DNS zone itself.

Challenge How it proves control Wildcard? Needs a reachable server?
HTTP-01 serves a token at /.well-known/acme-challenge/ No Yes
DNS-01 publishes a _acme-challenge TXT record Yes No
TLS-ALPN-01 answers on the TLS port with a special cert No Yes

Because DNS-01 only needs to write a TXT record, it also works for domains behind NAT, on a laptop that’s offline, or across many subdomains at once — which is exactly what automation wants.


What the DNS-01 challenge looks like under the hood

Diagram

The whole point is that this runs without you touching anything: certbot writes the record, deletes it, and never leaves a permanently-valid proof sitting in your zone. It’s the same “let it expire” philosophy from the shorter-lifetime post, applied to the challenge itself.


Prerequisites

Thing Why you need it Notes
A domain using Cloudflare DNS the plugin edits your zone via API any zone in your account works
A fine-grained Cloudflare API token authenticate the plugin use the Edit zone DNS template, scoped to just that zone
certbot + the Cloudflare plugin issue and renew install command below
Port 443 reachable for your web server actually serve the cert DNS validation itself needs no port

Create the API token in the Cloudflare dashboard at Profile → API Tokens → Create Token, pick the Edit zone DNS template, and scope it to the zone you’ll use. Least-privilege: the plugin only needs to read and write DNS records, nothing else.


The walkthrough

Step 1 — install certbot and the Cloudflare plugin

Debian / Ubuntu:

sudo apt update
sudo apt install -y python3-certbot-dns-cloudflare

Arch / CachyOS / EndeavourOS:

sudo pacman -S --noconfirm certbot-dns-cloudflare

The plugin package is what gives certbot the ability to create and remove TXT records through Cloudflare’s API, which is what a DNS-01 wildcard needs.

Step 2 — write the credentials file

Create a config file certbot will read, and drop your API token into it:

sudo mkdir -p /etc/letsencrypt
sudo nano /etc/letsencrypt/cloudflare.ini

Put exactly this inside, replacing the token:

dns_cloudflare_api_token = REPLACE_WITH_YOUR_TOKEN

Make it private — certbot refuses to run if the file is world-readable:

sudo chmod 600 /etc/letsencrypt/cloudflare.ini

Step 3 — request the wildcard certificate

sudo certbot certonly \
  --dns-cloudflare \
  --dns-cloudflare-credentials /etc/letsencrypt/cloudflare.ini \
  -d example.org \
  -d '*.example.org'

Two notes on the domain flags: you request both the bare apex -d example.org and the wildcard -d '*.example.org', because a wildcard does not cover the apex domain itself. And you’ll notice it uses a DNS-01 challenge — certbot will print something like Waiting for verification while the TXT record propagates, then report a success path like Successfully received certificate.

Step 4 — verify the certificate is real

sudo openssl x509 -in /etc/letsencrypt/live/example.org/fullchain.pem \
  -noout -text | grep -A1 'Subject Alternative Name'

You should see both DNS:example.org and DNS:*.example.org. A wildcard covering the whole domain — one certificate, one expiry date, every subdomain.

Step 5 — point your web server at it

Whatever your server (nginx, Apache, Caddy), use the certbot live paths so renewals pick up automatically:

ssl_certificate     /etc/letsencrypt/live/example.org/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/example.org/privkey.pem;

Reload the server and confirm a real handshake carries the wildcard name:

sudo systemctl reload nginx

echo | openssl s_client -connect api.example.org:443 \
  -servername api.example.org 2>/dev/null \
  | openssl x509 -noout -text | grep 'DNS:'

Step 6 — let the machine renew it forever

Test that renewal actually works without the cert expiring:

sudo certbot renew --dry-run

Then enable the on-time renewal it ships with:

sudo systemctl enable --now certbot-renew.timer   # Arch / CachyOS
sudo systemctl enable --now certbot.timer         # Debian / Ubuntu

sudo systemctl list-timers | grep certbot

Once the timer is active, certbot wakes up on its own schedule, writes its DNS token for a few seconds, swaps in a fresh wildcard, and deletes the token — no human in the loop, matching the exact flow in the diagram at the top.


Common pitfalls

Pitfall Symptom Fix
Forgot the bare apex example.org fails, *.example.org is fine add -d example.org alongside the wildcard
Tried HTTP-01 for a wildcard error: wildcard not supported over HTTP use --dns-cloudflare / DNS-01
Credentials file too open plugin refuses: permissions too broad chmod 600 /etc/letsencrypt/cloudflare.ini
Used a global API key random auth errors use a fine-grained token with dns_cloudflare_api_token
DNS propagation too slow Waiting for verification then timeout give it ~60s; certbot retries automatically
Wrong plugin flag name unrecognized arguments use --dns-cloudflare + --dns-cloudflare-credentials

The one-line takeaway

A wildcard certificate validated through DNS-01 and renewed by a timer is the single highest-leverage TLS thing you can automate — one command, one certificate, every subdomain, forever.


Companion to: Why SSL certificates keep shrinking