Wildcard certs (*.example.com) require DNS-01 challenge. HTTP challenge can't prove you own every subdomain. If your DNS is on Cloudflare, that means building Caddy yourself, since the default binary ships without DNS provider modules.
This guide walks through the full setup: custom build, secure token storage, and a working Caddyfile.
Why a Custom Build?
Caddy's DNS provider support ships as external modules that get compiled in at build time. The official xcaddy tool exists exactly for this. It's a thin wrapper around go build that handles module injection cleanly.
1. Install Base Caddy
Follow the official install docs for your distro. Don't skip this step even though we're replacing the binary later. The package install handles systemd unit setup, user/group creation, and directory structure.
2. Install Go
Avoid your distro's package manager here; Go versions in system repos tend to lag. Get the latest from go.dev/doc/install.
3. Install xcaddy
go install github.com/caddyserver/xcaddy/cmd/xcaddy@latest
If xcaddy isn't found after this, your $GOPATH/bin isn't in $PATH:
export PATH=$PATH:$(go env GOPATH)/bin
4. Build Caddy with the Cloudflare Module
xcaddy build --with github.com/caddy-dns/cloudflare
This drops a caddy binary in your current directory. Verify the module made it in:
./caddy list-modules | grep cloudflare
# dns.providers.cloudflare
If you see that, the build worked.
5. Replace the System Binary
sudo systemctl stop caddy
sudo mv ./caddy $(which caddy)
6. Store the Cloudflare Token Securely
Don't put the token directly in your Caddyfile — it's too easy to accidentally commit or expose. The cleaner approach is a systemd environment file that Caddy reads at startup.
Tell systemd about it:
sudo systemctl edit caddy.service
Add and save:
[Service]
EnvironmentFile=/etc/caddy/cloudflare.env
Create the file and lock it down:
echo "CLOUDFLARE_API_TOKEN=your_token_here" | sudo tee /etc/caddy/cloudflare.env
sudo chmod 400 /etc/caddy/cloudflare.env
sudo systemctl daemon-reload
On the Cloudflare side, create a scoped API token with only Zone > DNS > Edit permissions for your specific zone — not a global API key. If it ever leaks, the blast radius is contained.
7. Update the Caddyfile
{
acme_dns cloudflare {env.CLOUDFLARE_API_TOKEN}
email you@example.com
}
*.example.com {
reverse_proxy localhost:8080
}
The {env.CLOUDFLARE_API_TOKEN} syntax reads the value at runtime, so the token never sits in the config file itself.
8. Start Caddy
sudo systemctl start caddy && journalctl -u caddy -f
Caddy will create a DNS TXT record in your Cloudflare zone, validate with Let's Encrypt, issue the wildcard cert, then clean up the TXT record. The whole thing is automatic — once it's running you won't think about certificates again.
Troubleshooting
Module isn't listed: run caddy list-modules | grep dns. If nothing shows up, the binary replacement didn't take. Double-check with which caddy that you replaced the right one.
Certificate issuance hanging: Let's Encrypt polls for DNS propagation, which can take a couple of minutes even on Cloudflare. Wait before assuming something is broken.
Token permission error: verify the token has Zone > DNS > Edit and is scoped to the correct zone. Cloudflare's token model is granular and easy to misconfigure.
Top comments (0)