Wildcard TLS with Certbot and Cloudflare DNS-01 — a copy-paste walkthrough
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
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