Hi everyone,
I am testing the new native ECH (Encrypted Client Hello) support in Caddy v2.11 Beta using Cloudflare as the DNS provider.
1. The problem I’m having:
Caddy successfully generates ECH keys (generated new ECH config), but it never attempts to publish the HTTPS record to Cloudflare DNS. The logs show certificate maintenance and ACME challenges working perfectly, but the “publishing ECH config” step is missing entirely.
2. Error messages and/or full log output:
https://pastebin.com/raw/ZXPrW1hX
3. Caddy version:
Caddy v2.11.0-beta.2 (running via Docker) with dns.providers.cloudflare module.
4. How I installed and ran Caddy:
In docker: docker compose build --pull caddy && docker compose up -d && docker system prune -f
a. System environment:
Arch Linux, x86-64, systemd, docker
DNS Provider: Cloudflare.
c. Service/unit/compose file:
compose.yaml:
services:
caddy:
build: .
image: caddy-with-cloudflare
container_name: caddy
restart: unless-stopped
env_file:
- .env
ports:
- "80:80"
- "443:443"
- "443:443/udp" # Required for HTTP/3
volumes:
- ./Caddyfile:/etc/caddy/Caddyfile
- caddy_data:/data
- caddy_config:/config
- /srv/docker/caddy/log:/var/log/caddy
volumes:
caddy_data:
caddy_config:
Dockerfile:
FROM caddy:builder-alpine AS builder
RUN xcaddy build \
--with github.com/caddy-dns/cloudflare
FROM caddy:2.11-alpine
COPY --from=builder /usr/bin/caddy /usr/bin/caddy
d. My complete Caddy config:
{
email caddy.cert@acme.camis.ovh
ech ...
cert_issuer acme {
dir https://acme-v02.api.letsencrypt.org/directory
dns cloudflare {env.CLOUDFLARE_API_TOKEN}
}
log {
output file /var/log/caddy/system.log {
roll_size 20mb
roll_keep 5
roll_keep_for 6h
}
format console
level debug
}
}
# Snippet: Settings
(settings) {
header {
-Server
X-Robots-Tag "noindex, nofollow"
X-Content-Type-Options "nosniff"
X-Frame-Options "SAMEORIGIN"
Referrer-Policy "strict-origin-when-cross-origin"
Strict-Transport-Security "max-age=31536000; includeSubDomains; preload"
}
tls {
protocols tls1.3
curves x25519
issuer acme {
dir https://acme-v02.api.letsencrypt.org/directory
profile shortlived
dns cloudflare {env.CLOUDFLARE_API_TOKEN}
}
}
}
# Snippet: Restrict access to Local Network only
(local_only) {
@not_local not remote_ip 10.0.0.0/20 127.0.0.1
respond @not_local "Forbidden" 403
}
# --- Domain Blocks ---
... {
import settings
import local_only
reverse_proxy https://10.0.10.10:8006 {
transport http {
tls_insecure_skip_verify
}
}
}
... {
import settings
import local_only
reverse_proxy https://10.0.10.12:9443 {
transport http {
tls_insecure_skip_verify
}
}
}
... {
import settings
import local_only
reverse_proxy http://10.0.10.11:80
}
... {
import settings
import local_only
reverse_proxy http://10.0.10.14:8080
}
... {
import settings
import local_only
reverse_proxy http://10.0.10.17:8123
}
... {
import settings
reverse_proxy http://10.0.10.15:80
}
... {
import settings
handle_path /osmand/* {
reverse_proxy http://10.0.10.22:5055
}
handle {
reverse_proxy http://10.0.10.22:80
}
}
... {
import settings
reverse_proxy http://10.0.10.11:8000
}
... {
import settings
respond "Welcome!" 200
}
... {
import settings
respond "Welcome!" 200
}
... {
import settings
# Relay (WebSocket)
reverse_proxy /relay* netbird-relay:80
# Signal WebSocket
reverse_proxy /ws-proxy/signal* netbird-signal:80
# Signal gRPC (h2c for plaintext HTTP/2)
reverse_proxy /signalexchange.SignalExchange/* h2c://netbird-signal:10000
# Management API
reverse_proxy /api/* netbird-management:80
# Management WebSocket
reverse_proxy /ws-proxy/management* netbird-management:80
# Management gRPC
reverse_proxy /management.ManagementService/* h2c://netbird-management:80
# Embedded IdP OAuth2
reverse_proxy /oauth2/* netbird-management:80
# Dashboard (catch-all)
reverse_proxy /* netbird-dashboard:80
}
As you can see, the config is generated, and ACME DNS-01 challenge works fine (certs are obtained), so the API token is valid. However, there is no log entry regarding the publication of the HTTPS record for ECH.
Logs (Debug Level):**
https://pastebin.com/raw/ZXPrW1hX
What I have verified/attempted:
Cloudflare “Grey Cloud”: The A record for ech1.camis.ovh exists and is set to DNS Only (Grey Cloud), not Proxied.
API Token Permissions: The token works for acme_dns (Caddy successfully creates and removes TXT records for DNS-01 challenges). The token has Zone:DNS:Edit permissions.
Clean State: I have wiped the caddy_data volume to force a fresh generation of keys and certificates.
Verification: Running dig @1.1.1.1 HTTPS ech1.camis.ovh returns no ANSWER section.
My Question:
Does the global acme_dns directive apply to the ech module for publishing DNS records, or does the ech directive require its own explicit DNS provider configuration block?.
It seems like Caddy knows how to use Cloudflare for ACME but doesn’t use it to publish the ECH keys.
Question about ECH and Wildcard Certificates:
I have one architectural question regarding Caddy’s ECH implementation:
Does ECH work correctly when using Wildcard certificates for the hosted sites?.
Currently, I define each subdomain explicitly in my Caddyfile (e.g., app1.example.com, app2.example.com), so Caddy manages separate certificates for them.
If I were to switch to a Wildcard certificate setup (e.g., *.example.com) while keeping a dedicated ECH public name (e.g., ech.example.com):
Will Caddy be able to correctly unwrap the ECH ClientHello and serve the wildcard certificate for the inner SNI?
Or does the current ECH implementation require explicit domain blocks (and individual certificates) to map the keys correctly?
Thanks for any help!