Back to blog
Blog

Linux VPS ACME DNS challenge automation: wildcard TLS with Caddy + Cloudflare in 2026

Linux VPS ACME DNS challenge automation for wildcard TLS using Caddy + Cloudflare, with safe rollout, verification, and rollback.

By Anurag Singh
Updated on Apr 19, 2026
Category: Blog
Share article
Linux VPS ACME DNS challenge automation: wildcard TLS with Caddy + Cloudflare in 2026

Wildcard TLS is convenient: one certificate covers *.example.com, so you can add new subdomains without revisiting your reverse proxy config every time. The catch is that wildcard certificates require the DNS-01 challenge, not the usual HTTP-01. This guide shows Linux VPS ACME DNS challenge automation with Caddy and Cloudflare, so your VPS renews wildcard certificates on schedule—no late-night copy/paste of TXT records.

The focus here is day-two ops: keep credentials scoped and private, confirm renewals actually work, and have a rollback path if DNS or auth goes sideways.

Scenario: one VPS, many apps, one wildcard certificate

You’re running a few internal and public services on a single VPS: docs, a small API, a status page, maybe a staging app. You want:

  • Wildcard TLS for *.dev.acme-labs.net
  • Automatic renewals with zero manual DNS edits
  • Minimal secrets exposure (no credentials in world-readable files)
  • A repeatable setup you can move between servers

We’ll use Caddy because it handles certificates cleanly and supports DNS-01 via provider modules. If you already run Nginx, see this Nginx reverse proxy guide for routing patterns—the same ideas apply even if the config syntax differs.

Prerequisites (what you need before you touch your VPS)

  • A VPS with root access (Ubuntu 24.04/25.04 LTS-style layouts work fine; Debian 12+ also works)
  • A domain in Cloudflare (or at least a DNS zone hosted there)
  • Cloudflare API token with DNS edit permissions for that zone
  • Port 80 and 443 open to the internet (for serving traffic; DNS-01 itself doesn’t need inbound HTTP)
  • Basic familiarity with systemd and editing config files

If you’re building the VPS from scratch, a HostMyCode VPS fits this setup well: predictable networking, clean OS images, and enough headroom to run several small services behind one proxy.

Why DNS-01 matters for wildcard certificates

ACME challenges prove you control a domain. For a normal cert like api.dev.acme-labs.net, HTTP-01 works by serving a token over HTTP. For a wildcard like *.dev.acme-labs.net, the CA requires DNS-01: you publish a TXT record under _acme-challenge.dev.acme-labs.net.

The hard part isn’t creating the TXT record once. It’s doing it reliably during every renewal, with the right zone, every time. Automation fixes that—but it also introduces a new credential (your API token). Treat it like production access, because it is.

Step 1: Create a tight Cloudflare API token (DNS-only, one zone)

In Cloudflare, create an API token with the smallest scope that still works:

  • Permissions: Zone → DNS → Edit
  • Zone resources: Include → Specific zone → acme-labs.net

Name it so it’s obvious in audit logs, for example caddy-acme-dns01-dev-wildcard. Copy the token once; Cloudflare won’t show it again.

Expected outcome: a token string that starts with something like xxxxx (format varies), saved in your password manager until you store it on the server.

Step 2: Install Caddy (and verify the version)

On Ubuntu/Debian, install from the official Caddy repository so you get timely updates.

sudo apt update
sudo apt install -y debian-keyring debian-archive-keyring apt-transport-https curl
curl -1sLf 'https://dl.cloudsmith.io/public/caddy/stable/gpg.key' | sudo gpg --dearmor -o /usr/share/keyrings/caddy-stable-archive-keyring.gpg
curl -1sLf 'https://dl.cloudsmith.io/public/caddy/stable/debian.deb.txt' | sudo tee /etc/apt/sources.list.d/caddy-stable.list
sudo apt update
sudo apt install -y caddy
caddy version

Expected output: a recent Caddy v2 release (for 2026 you should see a v2.x build). The exact patch version matters less than staying current.

Quick check:

systemctl status caddy --no-pager

If it’s running, you’ll see active (running). If it’s not, leave it alone for now—we’ll finish DNS-01 setup first.

Step 3: Build Caddy with the Cloudflare DNS module (xcaddy)

Most distro packages ship a “standard” Caddy build. DNS-01 providers (including Cloudflare) require a module. The straightforward approach is xcaddy, which compiles Caddy with the module baked in.

sudo apt install -y golang-go

# Install xcaddy into /usr/local/bin
GOBIN=/usr/local/bin go install github.com/caddyserver/xcaddy/cmd/xcaddy@latest

# Build a Caddy binary with the Cloudflare DNS module
cd /tmp
xcaddy build --with github.com/caddy-dns/cloudflare@latest

# Replace the packaged caddy binary (keep a backup)
sudo mv /usr/bin/caddy /usr/bin/caddy.dist
sudo mv /tmp/caddy /usr/bin/caddy
sudo chown root:root /usr/bin/caddy
sudo chmod 755 /usr/bin/caddy

caddy list-modules | grep -E 'dns.providers.cloudflare' || true

Expected output: a line containing dns.providers.cloudflare. If you don’t see it, the module isn’t present and DNS-01 will fail later with a provider error.

Pitfall: On small instances, compiling can be slow or even fail under memory pressure. If your VPS is tiny, build the binary on a workstation and upload it. For real traffic, a modest tier also helps with TLS handshakes and log retention.

Step 4: Store the Cloudflare token as a systemd environment file

Don’t paste API tokens into /etc/caddy/Caddyfile. That file often ends up in backups, config repos, and “can you send me your config?” support threads.

Create a root-only env file:

sudo install -d -m 0750 /etc/caddy/secrets
sudo nano /etc/caddy/secrets/cloudflare.env

Put this content inside (use your real token):

CLOUDFLARE_API_TOKEN=cf_pat_your_real_token_here

Lock permissions:

sudo chown root:caddy /etc/caddy/secrets/cloudflare.env
sudo chmod 0640 /etc/caddy/secrets/cloudflare.env
sudo ls -l /etc/caddy/secrets/cloudflare.env

Expected output: permissions like -rw-r----- and owner root, group caddy.

Step 5: Wire the env file into the Caddy systemd service

Use a systemd drop-in so package updates don’t overwrite your changes.

sudo systemctl edit caddy

Add:

[Service]
EnvironmentFile=/etc/caddy/secrets/cloudflare.env

Reload systemd:

sudo systemctl daemon-reload

Verification:

systemctl show caddy -p EnvironmentFile

You should see your env file path listed.

Step 6: Write a Caddyfile for wildcard TLS (DNS-01) and safe routing

Edit /etc/caddy/Caddyfile:

sudo nano /etc/caddy/Caddyfile

Example config for *.dev.acme-labs.net with two services:

{
	# Keep logs readable; ship them elsewhere if you need long retention.
	email ops@acme-labs.net
}

# Wildcard site block: catch any subdomain
*.dev.acme-labs.net {
	encode zstd gzip

	# DNS-01 via Cloudflare
	tls {
		dns cloudflare {env.CLOUDFLARE_API_TOKEN}
	}

	# Basic hardening headers for most web apps
	header {
		Strict-Transport-Security "max-age=31536000; includeSubDomains; preload"
		X-Content-Type-Options "nosniff"
		X-Frame-Options "DENY"
		Referrer-Policy "no-referrer"
	}

	@api host api.dev.acme-labs.net
	reverse_proxy @api 127.0.0.1:9087

	@docs host docs.dev.acme-labs.net
	reverse_proxy @docs 127.0.0.1:9073

	# Default: return 404 for unknown subdomains
	handle {
		respond "unknown service" 404
	}

	log {
		output file /var/log/caddy/dev-acme-labs-access.log {
			roll_size 50MiB
			roll_keep 5
		}
		format json
	}
}

Notes on the example:

  • The upstreams are local backends on ports 9087 and 9073. Use ports that don’t collide with your stack.
  • The handle fallback prevents accidental exposure if you create a new DNS record but forget to add routing.
  • Access logs go to /var/log/caddy/. If you want centralized logs, pair this with log shipping with Loki (the same approach works for Caddy logs).

Step 7: Validate config and start Caddy cleanly

Validate the config before you restart anything. Syntax errors should fail fast.

sudo caddy validate --config /etc/caddy/Caddyfile

Expected output: Valid configuration.

Now restart Caddy:

sudo systemctl restart caddy
sudo systemctl status caddy --no-pager

If DNS permissions are correct, Caddy will request the wildcard cert automatically. First issuance can take 30–120 seconds depending on DNS propagation behavior.

Verification via logs:

sudo journalctl -u caddy -n 120 --no-pager

Look for messages about certificate acquisition for *.dev.acme-labs.net. If it fails, the log usually points straight at auth scope or zone lookup issues.

Step 8: Make sure DNS records point to your VPS

DNS-01 proves control, but clients still need to reach your server. Add A/AAAA records in Cloudflare:

  • api.dev.acme-labs.net → your VPS IPv4/IPv6
  • docs.dev.acme-labs.net → your VPS IPv4/IPv6

For troubleshooting, be deliberate about Cloudflare proxy mode:

  • If you use Cloudflare proxy (orange cloud), your origin still needs valid TLS if you’re using Full/Strict mode.
  • If you want to isolate issues, temporarily set to DNS-only (gray cloud) while you validate origin TLS end-to-end.

Step 9: Verify end-to-end TLS and routing

From your laptop (not from inside the VPS), run:

curl -I https://api.dev.acme-labs.net
curl -I https://docs.dev.acme-labs.net

Expected output: an HTTP status from your upstreams (often 200 or 302), plus headers like strict-transport-security.

To confirm you’re actually serving the wildcard certificate, inspect the certificate subject/SANs:

echo | openssl s_client -servername api.dev.acme-labs.net -connect api.dev.acme-labs.net:443 2>/dev/null | openssl x509 -noout -subject -issuer -ext subjectAltName

Expected output: a SAN list that includes *.dev.acme-labs.net (and often also the base name depending on how you requested it).

Step 10: Confirm automatic renewal (and don’t wait 60 days to find out)

Caddy renews automatically, but you still want confidence that the renewal path works. Check it while you’re already at the console.

Trigger a few checks (this won’t always re-issue immediately if the cert is fresh, but it exercises the plumbing):

sudo caddy trust || true
sudo caddy list-certificates --format json | head

Then watch logs for renewal-related activity:

sudo journalctl -u caddy --since "10 minutes ago" --no-pager | tail -n 200

If you run an observability agent, add an alert for Caddy certificate errors now, not later. If you don’t have monitoring in place yet, Linux VPS monitoring agent setup with OpenTelemetry Collector is a solid baseline for catching renewal failures before they become outages.

Common pitfalls (and how to spot them quickly)

  • Wrong Cloudflare token scope: If the token can’t edit DNS for the zone, logs show authorization errors. Fix the token permissions and restart Caddy.
  • Zone mismatch: You created a token for example.com but you’re requesting acme-labs.net. Caddy typically fails to find the zone or can’t create the TXT record.
  • Clock skew on the VPS: TLS and ACME are time-sensitive. Check timedatectl and ensure NTP is active.
  • Port 80/443 blocked: DNS-01 doesn’t need inbound HTTP for validation, but your users do. Confirm firewall rules. If you’re migrating firewall rulesets, iptables to nftables migration uses a careful verify/rollback flow you can borrow.
  • Storing secrets in the Caddyfile: It works until the file shows up in a backup or a ticket. Use an env file with tight permissions instead.

Rollback plan: get back to a known-good TLS setup

Rollback is only “easy” if you decide ahead of time what “known-good” means. The goal is to restore service fast, then debug without pressure.

  1. Revert to a non-wildcard config: Replace the wildcard block with explicit hosts and use HTTP-01 (default Caddy TLS) if that worked previously.
  2. Restore the original binary if you suspect the custom build:
    sudo mv /usr/bin/caddy /usr/bin/caddy.custom.bad
    sudo mv /usr/bin/caddy.dist /usr/bin/caddy
    sudo systemctl restart caddy
  3. Disable the env file quickly if it’s causing startup errors:
    sudo systemctl revert caddy
    sudo systemctl daemon-reload
    sudo systemctl restart caddy
  4. Validate the old behavior: Use curl -I against your primary hostname and check logs with journalctl.

If you want a bigger safety net before changes like this, snapshots help. Pair this workflow with Linux VPS snapshot backups so you can revert the whole machine state if you break networking or TLS.

Operational tips that keep this boring (the good kind of boring)

  • Keep token blast radius small: one zone, DNS edit only. Don’t use global API keys.
  • Log retention: Caddy logs can grow quickly on noisy endpoints. Keep rolling limits (as shown) and watch disk usage. If you need a triage playbook, this disk space troubleshooting post is a practical reference.
  • Use separate upstream ports per service: It’s easier to reason about and faster to debug than hiding everything behind one local gateway.
  • Don’t mix test and production in one wildcard: Use *.staging and *.prod as separate zones/subzones and separate tokens.

Next steps (small upgrades that pay off)

  • Add health checks and self-healing: systemd watchdog plus a simple /healthz endpoint prevents silent failures. See systemd watchdog on a VPS for a clean pattern.
  • Centralize logs and alerts: a renewal failure should page you long before browsers start warning users.
  • Move secrets management up a notch: if you distribute configs across servers, use encrypted secrets files. SOPS + age works well for Git-backed config.

If you’re running multiple services and want wildcard TLS without the ongoing manual work, start with a HostMyCode VPS and keep routing at the edge in Caddy. If your team prefers fewer on-call chores, managed VPS hosting can handle patching and baseline hardening while you keep control of your application stack.

FAQ: Linux VPS ACME DNS challenge automation

Do I still need port 80 open if I’m using DNS-01?

For validation, no. For real users, yes—at least port 443. Many setups also keep port 80 open so Caddy can redirect HTTP to HTTPS cleanly.

Can I use the same Cloudflare token for multiple servers?

You can, but it increases blast radius. A better pattern is one token per environment (or per server) with the same minimal DNS permissions.

Where does Caddy store certificates and account keys?

By default, Caddy uses its data directory (commonly under /var/lib/caddy on system packages). Treat that directory as sensitive; it contains private keys.

What if I want wildcard TLS but I’m not on Cloudflare?

Caddy supports many DNS providers via modules. The mechanics stay similar: build with the provider module, supply credentials via an env file, and use tls { dns ... }.

How do I prove renewals work without waiting months?

Watch Caddy logs and ensure it can create and clean up the _acme-challenge TXT record. Also verify that your monitoring alerts on certificate acquisition/renewal errors so you’ll know long before expiry.

Summary

Wildcard certificates don’t need to be fragile. With Linux VPS ACME DNS challenge automation through Caddy and a scoped Cloudflare token, renewals become predictable and adding subdomains becomes routine. Keep the token tightly scoped, confirm the Cloudflare module is installed, and run through the rollback steps once so you’re not improvising during an outage.

For a stable place to run this setup, provision a HostMyCode VPS, then treat Caddy like infrastructure: keep it updated, monitor it, and keep secrets out of your repo.