Combination of ech and a site block for the ech name cannot use tls resolver config
Issue Details
{
acme_dns cloudflare foo
ech example.com
}
example.com {
tls {
resolvers 1.1.1.1
}
}
Result:
Error: hostname appears in more than one automation policy, making certificate management ambiguous: example.com
I didn't even have to redact the file, any hostname triggers this, if set in both ech and a site block.
xCaddy Build script
#!/bin/bash
IFS=$'\n\t'
set -euo pipefail
# Debug logs
#set -x
# Done by dependency
#/usr/libexec/xcaddy/install_golang.sh
export PATH=/opt/golang/latest/go/bin:$PATH
go install github.com/caddyserver/xcaddy/cmd/xcaddy@latest
export PATH=$PATH:${GOPATH}/bin
xcaddy build --output xcaddy master \
--with github.com/caddy-dns/cloudflare \
--with github.com/aksdb/caddy-cgi/v2
cd /opt/xcaddy
tmpdir=$(mktemp -d --tmpdir=/opt/xcaddy .xcaddy-XXXX)
chmod go+rx ${tmpdir}
tmpdir_nodot="${tmpdir//./}"
# Copy built files to tmpdir
cp -r \
/tmp/xcaddy \
${tmpdir}/
restorecon -RvF ${tmpdir}
mv ${tmpdir} ${tmpdir_nodot}
sync /opt/xcaddy
ln --verbose --relative --force --no-dereference --symbolic $(basename ${tmpdir_nodot}) live
# Rename old directories for cleanup by systemd-tmpfiles
find . -maxdepth 1 -type d -name 'xcaddy-*' ! -name $(basename $(realpath /opt/xcaddy/live)) -exec bash -c 'mv -v $(basename {}) .$(basename {})' \;
Assistance Disclosure
AI not used
If AI was used, describe the extent to which it was used.
No response
Just discovered that by adding any OTHER site block, this error disappears???
The other block doesn't even need content:
{
acme_dns cloudflare foo
ech example.com
}
example.com {
tls {
resolvers 1.1.1.1
}
}
example.org
Boom?
I discovered this because I knew i had this active another server already and i had copied the config over, and removed the extra stuff, confusingly triggering the above issue.
Here's what I found in caddyconfig/httpcaddyfile/tlsapp.go:
- ECH creates automation policy in
buildTLSApp() - Default issuers are added to ECH policy in
buildTLSApp() - Site block with tls { resolvers ... } creates policy with explicit ACME issuers that have custom DNS resolvers
- Consolidation fails in
consolidateAutomationPolicies()- policies only merge ifreflect.DeepEqualon issuers returnstrue
Same issue occurs with tls internal - site block gets InternalIssuer while ECH gets default ACME issuers.
@Siomachkin does that explain why it works if I add any other site block? What changes with a second site block?
@Gunni I added detailed logging to consolidateAutomationPolicies() and tested both scenarios with a mock DNS provider:
Case 1: Does not work
{
acme_dns mock foo
ech example.com
}
example.com {
tls {
resolvers 1.1.1.1
}
}
=== DEBUG: consolidateAutomationPolicies START ===
Total policies: 3
Policy 0: subjects=[], issuers=1, managers=0
Issuer 0: {"challenges":{"dns":{"provider":{"argument":"foo","name":"mock"}}},"module":"acme"}
Policy 1: subjects=[example.com], issuers=1, managers=0
Issuer 0: {"challenges":{"dns":{"provider":{"argument":"foo","name":"mock"},"resolvers":["1.1.1.1"]}},"module":"acme"}
Policy 2: subjects=[example.com], issuers=1, managers=0
Issuer 0: {"challenges":{"dns":{"provider":{"argument":"foo","name":"mock"}}},"module":"acme"}
Comparing policies 0 (subjects=[example.com]) and 1 (subjects=[example.com]): issuersEqual=false
Comparing policies 0 (subjects=[example.com]) and 2 (subjects=[]): issuersEqual=true
Policies 0 and 2 can be consolidated!
Policy 0 shadows check: shadowIdx=1, j=2, can delete=false
Cannot delete policy 0 - shadowed by policy at 1
Comparing policies 1 (subjects=[example.com]) and 2 (subjects=[]): issuersEqual=false
=== DEBUG: consolidateAutomationPolicies END ===
Final policies count: 3
Final Policy 0: subjects=[example.com], issuers=1 (with resolvers)
Final Policy 1: subjects=[example.com], issuers=1 (ECH, without resolvers)
Final Policy 2: subjects=[], issuers=1 (catch-all)
Case 2: Does work (your workaround with example.org)
{
acme_dns mock foo
ech example.com
}
example.com {
tls {
resolvers 1.1.1.1
}
}
example.org
=== DEBUG: consolidateAutomationPolicies START ===
Total policies: 4
Policy 0: subjects=[], issuers=1, managers=0
Issuer 0: {"challenges":{"dns":{"provider":{"argument":"foo","name":"mock"}}},"module":"acme"}
Policy 1: subjects=[example.com], issuers=1, managers=0
Issuer 0: {"challenges":{"dns":{"provider":{"argument":"foo","name":"mock"},"resolvers":["1.1.1.1"]}},"module":"acme"}
Policy 2: subjects=[example.org], issuers=1, managers=0
Issuer 0: {"challenges":{"dns":{"provider":{"argument":"foo","name":"mock"}}},"module":"acme"}
Policy 3: subjects=[example.com], issuers=1, managers=0
Issuer 0: {"challenges":{"dns":{"provider":{"argument":"foo","name":"mock"}}},"module":"acme"}
Comparing policies 0 (subjects=[example.com]) and 1 (subjects=[example.org]): issuersEqual=false
Comparing policies 0 (subjects=[example.com]) and 2 (subjects=[example.com]): issuersEqual=false
Comparing policies 0 (subjects=[example.com]) and 3 (subjects=[]): issuersEqual=false
Comparing policies 1 (subjects=[example.org]) and 2 (subjects=[example.com]): issuersEqual=true
Policies 1 and 2 can be consolidated!
Comparing policies 1 (subjects=[example.org example.com]) and 2 (subjects=[]): issuersEqual=true
Policies 1 and 2 can be consolidated!
Policy 1 shadows check: shadowIdx=2, j=2, can delete=true
Deleting policy 1 in favor of catch-all at 2
=== DEBUG: consolidateAutomationPolicies END ===
Final policies count: 2
Final Policy 0: subjects=[example.com], issuers=1 (with resolvers)
Final Policy 1: subjects=[], issuers=1 (catch-all)
The consolidation algorithm does this:
- Policy example.org and Policy ECH example.com have identical issuers - they consolidate into [example.org, example.com]
- This consolidated policy then consolidates with the catch-all Policy (also identical issuers)
- Since the catch-all has empty subjects, the more specific policy gets deleted
- Result: example.com appears only once. The extra site blocks triggered the consolidation chain that absorbed the ECH policy.