How to Get a Let's Encrypt Wildcard Certificate with the DNS Challenge (HTTPS for Internal Services, No Open Ports)

One real certificate for every internal name: the DNS challenge needs no open port, and *.home.example.com covers them all.

The normal Certbot flow (tutorial 27) proves you own a name by answering on port 80 from the internet. That's no use for nas.home.example.com, which resolves only inside your house. The DNS challenge proves ownership differently: Certbot adds a TXT record to your domain's DNS through the provider's API, Let's Encrypt reads it, and issues the certificate — including wildcards. No port forward, no public reachability, real certificates trusted by every device without installing anything. It's the cleanest answer to internal HTTPS if you own a domain.

Quick reference

TaskWhat
Needsa domain whose DNS has an API: Cloudflare (free), DuckDNS, deSEC, Route53, many others
Plugincertbot-dns-cloudflare (or -duckdns, -desec, …)
TokenCloudflare → API Tokens → Edit zone DNS for that zone
Issuecertbot certonly --dns-cloudflare -d "*.home.example.com" -d home.example.com
Files/etc/letsencrypt/live/home.example.com/
Renewautomatic via Certbot's timer — nothing to open

1. Install the plugin

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

For DuckDNS: pipx install certbot-dns-duckdns (and run that certbot). For other providers, apt search certbot-dns or the certbot-dns-* pip packages — Lego and acme.sh support even more if yours isn't there.

2. The API token

Cloudflare → My Profile → API Tokens → Create Token → Edit zone DNS template, zone resources limited to example.com. Copy it.

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

dns_cloudflare_api_token = YOUR_TOKEN

sudo chmod 600 /etc/letsencrypt/secrets/cloudflare.ini

3. Issue the wildcard

sudo certbot certonly --dns-cloudflare \
--dns-cloudflare-credentials /etc/letsencrypt/secrets/cloudflare.ini \
--dns-cloudflare-propagation-seconds 30 \
-d "*.home.example.com" -d home.example.com

Certbot creates _acme-challenge.home.example.com TXT records, waits, Let's Encrypt checks them, the records are removed, and the certificate lands in /etc/letsencrypt/live/home.example.com/ (fullchain.pem and privkey.pem). Renewal is already scheduled by the Certbot timer and uses the same plugin — no port 80 ever involved.

4. Internal DNS

Make the names resolve inside the house: Pi-hole Local DNS → DNS Records (34) with *.home.example.com isn't possible directly, so add each name (nas.home.example.com → 192.168.1.50), or use Pi-hole's dnsmasq wildcard: a file in /etc/dnsmasq.d/ (mounted into the container) containing address=/home.example.com/192.168.1.50. Publicly, those names need not exist at all.

5. Use the certificate

Nginx — one ssl_certificate pair for every internal server block:

server {
listen 443 ssl;
server_name nas.home.example.com;
ssl_certificate /etc/letsencrypt/live/home.example.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/home.example.com/privkey.pem;
location / { proxy_pass http://127.0.0.1:9443; ... }
}

Add a --deploy-hook "systemctl reload nginx" to the Certbot command (or /etc/letsencrypt/renewal-hooks/deploy/reload.sh) so renewals take effect.

Caddy does the DNS challenge itself with a provider module (xcaddy build --with github.com/caddy-dns/cloudflare, then tls { dns cloudflare {env.CF_TOKEN} } in a global block) — no Certbot needed.

Nginx Proxy Manager (58): SSL Certificates → Add → Use a DNS Challenge with the same token; pick the wildcard on each host.

Other machines (the Pi running something with its own HTTPS): copy the two files over with a deploy hook and scp, or run Certbot there with the same token.

6. Keep the token safe

It can edit your whole zone's DNS. chmod 600, never in a compose file or git, and scope it to one zone. If it leaks, revoke it in Cloudflare and make another.

Troubleshooting

"DNS problem: NXDOMAIN looking up TXT". Propagation; raise --dns-cloudflare-propagation-seconds to 60. Or the token lacks edit rights on the zone.

Certificate issued but the browser still warns. It's serving another certificate — nginx -t, the right ssl_certificate path, and the name in the URL must match the wildcard's level (*.home.example.com covers nas.home.example.com, not nas.example.com or a.b.home.example.com).

Renewal fails after months. Token revoked or expired; sudo certbot renew --dry-run shows the error.

No domain. Then it's mkcert or step-ca (tutorial 146), or a free subdomain with API access (DuckDNS, deSEC's dedyn.io) — the plugin exists for both.

Wrapping up

A DNS plugin, a scoped API token in a 600 file, one certbot certonly for *.home.example.com. A real certificate for every internal service with no open port and no root to install, renewing by itself — and Nginx, Caddy, NPM and the Pi all share it.

Call us