From f70963e1661138ae6e0d49288c809c4f38a4ad1e Mon Sep 17 00:00:00 2001 From: ginnoir Date: Wed, 5 Aug 2026 09:51:54 -0500 Subject: [PATCH] fix(proxy): pin public resolvers on caddy so ACME DNS-01 works The LAN resolver (OPNsense Unbound) is authoritative for ginnoir.com via the split-horizon override, but its local-zone holds only A records, so `SOA ginnoir.com` returns NODATA. certmagic's zone lookup walks up the label chain hunting for an SOA, finds none at ginnoir.com, climbs to `com.`, and asks Cloudflare for a `com` zone: adding temporary record for zone "com.": expected 1 zone, got 0 for com. Every DNS-01 renewal has failed for ~17 days (attempt 91 on the oldest), and Caddy fell back to the LE staging endpoint. Certs began expiring as they rolled off: 5etools (-4d), files (-1.9d), auth (-15.6h), fam (-15.6h), with ~20 more queued behind them. The expired auth.ginnoir.com cert is what broke Nextcloud SSO: its server-side discovery fetch fails TLS verification ("certificate has expired"), and user_oidc's LoginController catches that and returns 404 "provider unreachable". Browsers were unaffected because they let you click through an expired cert; PHP/curl does not. Pinning 1.1.1.1/1.0.0.1 on the caddy container sends the SOA lookup to public DNS. Verified Docker's embedded resolver still serves container names and the gitea.ginnoir.com alias, which take priority over the external servers. Also corrects the imgstudio comment, which credited its working TLS to avoiding a "false .com" in the hostname. The actual reason was its explicit `resolvers` line -- the same fix, applied to one site. Co-Authored-By: Claude Opus 5 --- Caddyfile | 9 +++++++-- stacks/proxy/docker-compose.yml | 15 +++++++++++++++ 2 files changed, 22 insertions(+), 2 deletions(-) diff --git a/Caddyfile b/Caddyfile index b58997e..61f8f27 100644 --- a/Caddyfile +++ b/Caddyfile @@ -401,8 +401,13 @@ webui.ginnoir.com { # ComfyUI node editor — inference runs on the Mac at 192.168.1.121 (Metal/MPS). # LAN/tailnet only; no Authentik (WebSocket queue/progress breaks under forward_auth). -# Hostname imgstudio (not comfyui): _acme-challenge.comfyui.ginnoir.com embeds a -# false ".com" that breaks Cloudflare DNS-01 zone lookup. +# +# The explicit `resolvers` below is what made DNS-01 work here, not the imgstudio +# hostname (an earlier comment blamed a "false .com" in comfyui.ginnoir.com — +# that was wrong). The LAN resolver returns NODATA for `SOA ginnoir.com`, so +# certmagic's zone lookup climbs to `com.` and Cloudflare rejects it. The caddy +# container now pins public resolvers stack-wide (stacks/proxy/docker-compose.yml), +# making this block redundant; kept as belt-and-braces. imgstudio.ginnoir.com { import internal_only tls { diff --git a/stacks/proxy/docker-compose.yml b/stacks/proxy/docker-compose.yml index 09ebd78..8dfbf52 100644 --- a/stacks/proxy/docker-compose.yml +++ b/stacks/proxy/docker-compose.yml @@ -19,6 +19,21 @@ services: restart: unless-stopped labels: - "com.centurylabs.watchtower.enable=false" + # Public resolvers for ACME DNS-01 zone detection. + # + # The LAN resolver (OPNsense Unbound, 192.168.1.1) is authoritative for + # ginnoir.com via the split-horizon override, but its local-zone only holds + # A records — so `SOA ginnoir.com` returns NODATA. certmagic's zone lookup + # walks up the label chain looking for an SOA, finds none at ginnoir.com, + # climbs to `com.`, and asks Cloudflare for a `com` zone. Result: + # "adding temporary record for zone \"com.\": expected 1 zone, got 0" + # and every DNS-01 renewal fails until certs expire. + # + # Docker's embedded DNS (127.0.0.11) still resolves container names and the + # gitea.ginnoir.com alias below; only external lookups go to Cloudflare. + dns: + - 1.1.1.1 + - 1.0.0.1 networks: edge: aliases: