1. The problem I’m having:
According to the documentation, enabling ECH in Caddy seems straightforward: set env.CLOUDFLARE_API_TOKEN and add two lines in the global block:
{
dns cloudflare {env.CLOUDFLARE_API_TOKEN}
ech ech.example.net
}
As I understand it, ECH is just an encryption/decryption mechanism, and the domain name after ech can be anything – even just a single character.
However, after I configured this, the nightmare began. Error logs like the following appeared:
Caddy kept trying to obtain certificates even when I set auto_https disable_certs to disable automatic certificate management. I then switched to a domain I actually control. After a while, Caddy generated .crt, .key, config.bin, and key.bin files, and I saw "2.ech=“AEn+DQ…” records appear in Cloudflare DNS. However, when I test with curl --ech and inspect packets with Wireshark, ECH does not appear to be actually working.
What could be going wrong?
2. Error messages and/or full log output:
Jul 18 17:27:13 : {"level":"info","ts":1784366833.375879,"logger":"http","msg":"waiting on internal rate limiter","identifiers":["www.courant.com"],"ca":"https://acme-v02.api.letsencrypt.org/directory","account":""}Jul 18 17:27:13 : {"level":"info","ts":1784366833.3765242,"logger":"http","msg":"done waiting on internal rate limiter","identifiers":["www.courant.com"],"ca":"https://acme-v02.api.letsencrypt.org/directory","account":""}Jul 18 17:27:13 : {"level":"info","ts":1784366833.3765945,"logger":"http","msg":"using ACME account","account_id":"https://acme-v02.api.letsencrypt.org/acme/acct/3545384985","account_contact":}Jul 18 17:27:14 : {"level":"info","ts":1784366834.6525803,"logger":"http.acme_client","msg":"trying to solve challenge","identifier":"www.courant.com","challenge_type":"dns-01","ca":"https://acme-v02.api.letsencrypt.org/directory"}Jul 18 17:27:15 : {"level":"error","ts":1784366835.3726997,"logger":"http.acme_client","msg":"cleaning up solver","identifier":"www.courant.com","challenge_type":"dns-01","error":"no memory of presenting a DNS record for "_acme-challenge.www.courant.com" (usually OK if presenting also failed)"}Jul 18 17:27:15 : {"level":"error","ts":1784366835.5886862,"logger":"tls.obtain","msg":"could not get certificate from issuer","identifier":"www.courant.com","issuer":"acme-v02.api.letsencrypt.org-directory","error":"[www.courant.com] solving challenges: presenting for challenge: adding temporary record for zone "courant.com.": expected 1 zone, got 0 for courant.com. (order=https://acme-v02.api.letsencrypt.org/acme/order/3545384985/533675824255) (ca=https://acme-v02.api.letsencrypt.org/directory)"}Jul 18 17:27:15 : {"level":"error","ts":1784366835.588895,"logger":"tls.obtain","msg":"will retry","error":"[www.courant.com] Obtain: [www.courant.com] solving challenges: presenting for challenge: adding temporary record for zone "courant.com.": expected 1 zone, got 0 for courant.com. (order=https://acme-v02.api.letsencrypt.org/acme/order/3545384985/533675824255) (ca=https://acme-v02.api.letsencrypt.org/directory)","attempt":1,"retrying_in":60,"elapsed":2.213651226,"max_duration":2592000}
3. Caddy version:
v2.11.4 h1:XKxkMTgNSizEvKG6QHue6cAsFOteU2qA61w2tKkCWi0=
4. How I installed and ran Caddy:
xcaddy build --with GitHub - caddy-dns/cloudflare: Caddy module: dns.providers.cloudflare · GitHub
a. System environment:
ubuntu 26.04 x86
caddy list-modules->dns.providers.cloudflare
b. Command:
curl -v --ech hard https://mydomain
* Host mydomain:443 was resolved.
* IPv6: 2400:xxxxx
* IPv4: 23.xx.xxx.xx
* HTTPS-RR: -
* Trying [2400:xxxxx]:443...
* Trying 23.xx.xxx.xx:443...
* ECH: requested but no ECHConfig available
* closing connection #0
curl: (35) SSL connect error
c. Service/unit/compose file:
PASTE OVER THIS HERE IN THIS CODE BLOCK.
Please ensure it looks nice.
d. My complete Caddy config:
{
auto_https disable_certs
# https://acme-staging-v02.api.letsencrypt.org/directory
# dns cloudflare {env.CLOUDFLARE_API_TOKEN}
acme_dns cloudflare {env.CLOUDFLARE_API_TOKEN}
ech www.courant.com
}
www.nodisk.xyz, nodisk.xyz {
tls /etc/nginx/ssl/***.crt /etc/nginx/ssl/###.key {
resolvers 1.1.1.1
}
header {
X-Frame-Options "SAMEORIGIN"
X-Content-Type-Options "nosniff"
Referrer-Policy "strict-origin-when-cross-origin"
-Server
}
root * /var/www/html
file_server
route /portal/v2/fetch* {
@not_allowed {
not method GET POST
}
respond @not_allowed 403
reverse_proxy 127.0.0.1:9056 {
header_up Host {host}
header_up X-Real-IP {remote_host}
flush_interval -1
transport http {
read_timeout 10m
write_timeout 10m
}
header_down -X-Powered-By
header_down -Server
}
}
log {
output file /var/log/caddy/system.log {
roll_size 20mb
roll_keep 5
roll_keep_for 6h
}
format console
level debug
}
}
:80 {
respond 404
}
5. Links to relevant resources:
Type here