DEV Community

Cover image for Caddy + Cloudflare DNS: Wildcard SSL Without the Pain
Aman
Aman

Posted on

Caddy + Cloudflare DNS: Wildcard SSL Without the Pain

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
Enter fullscreen mode Exit fullscreen mode

If xcaddy isn't found after this, your $GOPATH/bin isn't in $PATH:

export PATH=$PATH:$(go env GOPATH)/bin
Enter fullscreen mode Exit fullscreen mode

4. Build Caddy with the Cloudflare Module

xcaddy build --with github.com/caddy-dns/cloudflare
Enter fullscreen mode Exit fullscreen mode

This drops a caddy binary in your current directory. Verify the module made it in:

./caddy list-modules | grep cloudflare
# dns.providers.cloudflare
Enter fullscreen mode Exit fullscreen mode

If you see that, the build worked.

5. Replace the System Binary

sudo systemctl stop caddy
sudo mv ./caddy $(which caddy)
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

Add and save:

[Service]
EnvironmentFile=/etc/caddy/cloudflare.env
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
}
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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)