# Caddy Cloudflare

> Configure Caddy with Cloudflare DNS-01 challenge for automatic wildcard TLS certificates. Use when the user explicitly asks to set up HTTPS with Cloudflare domains, wildcard certificates, or Caddy TLS automation. Covers installation, secure token storage, Caddyfile writing, and Cloudflare DNS configuration.

- Skill: `animadverto-mundum/caddy-cloudflare` (Agent Skill)
- Install (CLI): `npx skillmds@latest add animadverto-mundum/caddy-cloudflare`
- Raw SKILL.md: https://api.skillmd.com/api/skills/animadverto-mundum/caddy-cloudflare/raw
- Safety review: pending (external: skill-scanner WARNING, skillspector WARNING)
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Coding & Dev Tools
- Author: Animadverto-mundum (https://skillmd.com/u/animadverto-mundum)
- Updated: 2026-09-22
- Page: https://skillmd.com/skills/animadverto-mundum/caddy-cloudflare

---


# Caddy + Cloudflare TLS Configuration

Guide the user through setting up Caddy to automatically manage wildcard TLS certificates via Cloudflare DNS challenge. Do not configure reverse proxying or backend handlers unless explicitly asked.

## 1. Install Caddy

Offer two options and let the user choose.

### Option A (recommended): Pre-built binary with cloudflare-dns plugin

Download a single binary already compiled with the Cloudflare DNS provider module:

```bash
curl -sL "https://caddyserver.com/api/download?os=linux&arch=amd64&p=github.com%2Fcaddy-dns%2Fcloudflare" -o /tmp/caddy
sudo mv /tmp/caddy /usr/bin/caddy
sudo chmod +x /usr/bin/caddy
caddy version
caddy list-modules | grep cloudflare
```

Verify the output shows `dns.providers.cloudflare`.

Optionally, also include the Cloudflare IP source module so the user can write `trusted_proxies cloudflare` instead of a long IP list:

```bash
curl -sL "https://caddyserver.com/api/download?os=linux&arch=amd64&p=github.com%2Fcaddy-dns%2Fcloudflare&p=github.com%2FWeidiDeng%2Fcaddy-cloudflare-ip" -o /tmp/caddy
```

### Option B: `apt install caddy` from Ubuntu/Debian repos

```bash
sudo apt install -y caddy
caddy version
```

This version does not include the cloudflare-dns plugin, so you must use Caddy's static IP range source for `trusted_proxies`. The DNS challenge must rely on an environment variable (see Token section).

## 2. Secure Token Storage

Ask the user for their Cloudflare API Token. It needs `Zone:DNS:Edit` permission at minimum.

Give the user this command to run themselves - the token will not appear in terminal output or history:

```bash
read -s TOKEN && echo "$TOKEN" | sudo tee /etc/caddy/cloudflare.token > /dev/null && \
    sudo chmod 600 /etc/caddy/cloudflare.token && \
    sudo chown caddy:caddy /etc/caddy/cloudflare.token && \
    unset TOKEN
```

Then check the token is valid:

```bash
TOKEN="$(sudo cat /etc/caddy/cloudflare.token)"
curl -s "https://api.cloudflare.com/client/v4/user/tokens/verify" -H "Authorization: Bearer $TOKEN"
```

For Option A (pre-built binary with plugin), the Caddyfile can reference the token file directly with:
```
tls {
    dns cloudflare {env.CLOUDFLARE_API_TOKEN}
}
```

But since Caddy's systemd service needs the env var, create an env file from the token:

```bash
sudo sh -c 'echo "CLOUDFLARE_API_TOKEN=$(cat /etc/caddy/cloudflare.token)" > /etc/caddy/cloudflare.env'
sudo chown caddy:caddy /etc/caddy/cloudflare.env
sudo chmod 600 /etc/caddy/cloudflare.env
```

Then load it in systemd:

```bash
sudo mkdir -p /etc/systemd/system/caddy.service.d
sudo tee /etc/systemd/system/caddy.service.d/override.conf > /dev/null <<'UNIT'
[Service]
EnvironmentFile=/etc/caddy/cloudflare.env
UNIT
sudo systemctl daemon-reload
```

For Option B (apt install without plugin), the environment variable approach is the only way since there's no cloudflare-dns module to parse a file argument.

## 3. Write the Caddyfile

Write `/etc/caddy/Caddyfile`. Always use `sudo tee` since the file is root-owned.

### Global Options Block

The first block must be a global options block `{ ... }`.

**trusted_proxies** goes inside a `servers` sub-block, not at the top level:

```caddy
{
    servers {
        trusted_proxies static 173.245.48.0/20 103.21.244.0/22 103.22.200.0/22 103.31.4.0/22 141.101.64.0/18 108.162.192.0/18 190.93.240.0/20 188.114.96.0/20 197.234.240.0/22 198.41.128.0/17 162.158.0.0/15 104.16.0.0/13 104.24.0.0/14 172.64.0.0/13 131.0.72.0/22
        trusted_proxies_strict
    }
}
```

If Option A was chosen and the `WeidiDeng/caddy-cloudflare-ip` module was included, use the shorthand:

```caddy
{
    servers {
        trusted_proxies cloudflare
        trusted_proxies_strict
    }
}
```

`trusted_proxies_strict` enables right-to-left parsing of `X-Forwarded-For`, which is correct for Cloudflare's proxy behavior.

### Site Blocks

Always include the wildcard with every subdomain. Putting a subdomain in a separate site block from its wildcard causes Caddy TLS routing conflicts.

**Domain name pattern:**

When the user says something like "ss.example.com" where the zone apex is `example.com`, the Caddy site block names must be:

```
ss.example.com, *.example.com, example.com { ... }
```

When the user says something like "kk.sub.example.com" where the parent is `sub.example.com`, use:

```
kk.sub.example.com, *.sub.example.com, sub.example.com { ... }
```

The wildcard is always `*.<parent_domain>`, not `*.*.<apex>`.

**TLS configuration:**

```caddy
tls {
    dns cloudflare {env.CLOUDFLARE_API_TOKEN}
}
```

This uses the DNS-01 challenge via Cloudflare. The `{env.VAR}` placeholder reads the environment variable set up in step 2.

**No reverse_proxy by default.** Only add `reverse_proxy <target>` if the user explicitly asks for it. If asked, put the target on `localhost:<port>`.

### Routing specific subdomains to specific backends

When the user wants a particular subdomain to go to a specific backend, use named matchers and `handle` blocks **within the same site block** — never split into separate blocks:

```caddy
kk.sub.example.com, *.sub.example.com, sub.example.com {
    tls {
        dns cloudflare {env.CLOUDFLARE_API_TOKEN}
    }

    @kk host kk.sub.example.com
    handle @kk {
        reverse_proxy localhost:23456
    }
}
```

Subdomains without a matching `handle` block will have no backend (only TLS).

## 4. Cloudflare DNS Configuration

Tell the user to ensure these DNS records exist in Cloudflare:

- `*.<parent_domain>` — A record pointing to the server IP
- `<parent_domain>` — A record pointing to the server IP

The proxy status (orange/grey cloud) depends on the domain depth:

- **One-level subdomains** (e.g., `ss.example.com`) — can use **Proxied** (orange cloud). Cloudflare's free Universal SSL covers `*.example.com`.
- **Multi-level subdomains** (e.g., `kk.sub.example.com`) — must use **DNS Only** (grey cloud). Cloudflare's free Universal SSL only matches `*.example.com`, not `*.sub.example.com`. Using Proxied causes ERR_SSL_VERSION_OR_CIPHER_MISMATCH because Cloudflare's edge has no certificate covering that depth.

Explain this limitation clearly. The user can work around it by upgrading to Cloudflare Pro or Advanced Certificate Manager.

## 5. Reload and Verify

```bash
sudo systemctl reload caddy
```

If reload hangs (common when a new certificate is being issued), wait. Wildcard DNS challenges can take a few seconds.

Verify certificates:

```bash
sudo journalctl -u caddy --since "1 min ago" --no-hostname -o short | grep -E "obtained|error"
```

Verify the site is serving:

```bash
curl -k --max-time 5 --resolve '<domain>:443:127.0.0.1' https://<domain>/
```

## Common Pitfalls

- **trusted_proxies at top level**: Must be inside `servers {}` block, not a bare global option. Using it directly under `{ }` gives "unrecognized global option".

- **Separate site blocks for overlapping hostnames**: If `kk.example.com` matches both `kk.example.com { ... }` AND `*.example.com { ... }`, Caddy TLS routing breaks. Always merge overlapping names into one block.

- **Token in a separate file**: When using `dns cloudflare /path/to/token`, the token file must have no trailing newline and no quotes. The env-var approach (`{env.VAR}`) is more reliable.

- **Cloudflare plan limits**: Free plans do not support `trusted_proxies cloudflare` (needs extra plugin) or uploading custom edge certificates (needs Pro plan).

- **reload vs restart**: `systemctl reload caddy` applies config changes gracefully without dropping connections. Only use `restart` when the binary has been replaced.

