From 693cf946955405396d1da24c50bacb0873e9e508 Mon Sep 17 00:00:00 2001 From: Audiobuzz Date: Sat, 4 Jul 2026 10:24:42 +0000 Subject: [PATCH] fleet-manager: point acme DNS-01 propagation check at a public resolver MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Split-horizon DNS made lego's propagation check query the container's internal resolver, which doesn't serve the public _acme-challenge TXT record, so the challenge timed out. Add ACME_RESOLVERS (comma-separated host:port, default 8.8.8.8:53) → one --dns.resolvers flag each. Documented in .env + DEPLOY.md. --- serving/fleet-manager/.env.example | 4 ++++ serving/fleet-manager/DEPLOY.md | 6 ++++++ serving/fleet-manager/acme/renew-loop.sh | 15 +++++++++++++-- serving/fleet-manager/docker-compose.yml | 3 +++ 4 files changed, 26 insertions(+), 2 deletions(-) diff --git a/serving/fleet-manager/.env.example b/serving/fleet-manager/.env.example index c8b1c55..4aed1ed 100644 --- a/serving/fleet-manager/.env.example +++ b/serving/fleet-manager/.env.example @@ -31,6 +31,10 @@ AWS_REGION=us-east-1 # Optional but recommended: pin the Route 53 zone. Skips ListHostedZonesByName # (so you can drop it from the IAM policy) and disambiguates public/private zones. AWS_HOSTED_ZONE_ID= +# Public resolver(s) for the DNS-01 propagation check (comma-separated host:port). +# Needed because internal/split-horizon DNS won't resolve the public challenge +# record. Defaults to Google DNS; override if 8.8.8.8 is blocked (e.g. 1.1.1.1:53). +ACME_RESOLVERS=8.8.8.8:53 # ACME_STAGING=1 # uncomment for a dry-run vs LE staging (UNTRUSTED certs, no rate limit) # --- agent plane (gRPC + mTLS, fleet CA) --------------------------------------- diff --git a/serving/fleet-manager/DEPLOY.md b/serving/fleet-manager/DEPLOY.md index a670a88..7098205 100644 --- a/serving/fleet-manager/DEPLOY.md +++ b/serving/fleet-manager/DEPLOY.md @@ -151,6 +151,12 @@ A publicly-trusted cert also keeps the WebAuthn secure-context clean (no browser warnings). The side-car keeps renewing on its 12-hour loop; the manager reloads each new cert automatically. +> **Split-horizon DNS:** the side-car defaults `ACME_RESOLVERS=8.8.8.8:53` so +> lego checks TXT propagation against **public** DNS. Without it, lego uses the +> container's internal resolver, which doesn't serve the public +> `_acme-challenge` record, and the challenge times out. Override if 8.8.8.8 is +> blocked (e.g. `1.1.1.1:53`, comma-separated for several). + > **Origin match:** the cert CN/SAN (`FLEET_MGR_RP_ID`) must equal the origin the > browser uses *and* `FLEET_MGR_RP_ORIGIN` — passkeys are origin-bound. The admin > listener binds loopback (`127.0.0.1:9500`) by default; expose it as diff --git a/serving/fleet-manager/acme/renew-loop.sh b/serving/fleet-manager/acme/renew-loop.sh index ae2df53..5d82712 100755 --- a/serving/fleet-manager/acme/renew-loop.sh +++ b/serving/fleet-manager/acme/renew-loop.sh @@ -32,6 +32,17 @@ LEGO=$(command -v lego 2>/dev/null || true) [ -z "$LEGO" ] && [ -x /lego ] && LEGO=/lego [ -z "$LEGO" ] && { echo "acme: FATAL lego binary not found (looked on PATH and /lego)"; exit 1; } +# Force the DNS-01 propagation check against a PUBLIC resolver — the container's +# internal/split-horizon DNS doesn't serve the public _acme-challenge record, so +# lego's default (system resolver) never sees the TXT and times out. Accepts a +# comma-separated list of host:port; each becomes a --dns.resolvers flag. +RESOLVERS="" +if [ -n "${ACME_RESOLVERS:-}" ]; then + OLDIFS=$IFS; IFS=',' + for r in $ACME_RESOLVERS; do RESOLVERS="$RESOLVERS --dns.resolvers $r"; done + IFS=$OLDIFS +fi + while true; do # First pass issues; later passes renew when near expiry. if [ -f "/le/certificates/$DOMAIN.crt" ]; then @@ -41,9 +52,9 @@ while true; do fi echo "acme: lego $* for $DOMAIN (Route 53 DNS-01)" - # shellcheck disable=SC2086 # $STAGING is intentionally two words when set + # shellcheck disable=SC2086 # $STAGING/$RESOLVERS intentionally word-split if "$LEGO" --accept-tos --email "$EMAIL" --dns route53 --domains "$DOMAIN" \ - --path /le $STAGING "$@"; then + --path /le $RESOLVERS $STAGING "$@"; then install -m 644 "/le/certificates/$DOMAIN.crt" /out/fullchain.pem install -m 600 "/le/certificates/$DOMAIN.key" /out/privkey.pem chown "$UID_OUT:$UID_OUT" /out/fullchain.pem /out/privkey.pem 2>/dev/null \ diff --git a/serving/fleet-manager/docker-compose.yml b/serving/fleet-manager/docker-compose.yml index a01f3e9..a33b71a 100644 --- a/serving/fleet-manager/docker-compose.yml +++ b/serving/fleet-manager/docker-compose.yml @@ -125,6 +125,9 @@ services: ACME_DOMAIN: "${FLEET_MGR_RP_ID:?set FLEET_MGR_RP_ID in .env}" # = the UI hostname ACME_EMAIL: "${ACME_EMAIL:?set ACME_EMAIL in .env}" ACME_STAGING: "${ACME_STAGING:-}" # set 1 for a dry-run + # Public resolver(s) for the propagation check — the internal/split-horizon + # DNS doesn't serve the public _acme-challenge record. Comma-separated host:port. + ACME_RESOLVERS: "${ACME_RESOLVERS:-8.8.8.8:53}" # AWS creds for the Route 53 DNS-01 challenge (or omit + use an instance role). AWS_ACCESS_KEY_ID: "${AWS_ACCESS_KEY_ID:-}" AWS_SECRET_ACCESS_KEY: "${AWS_SECRET_ACCESS_KEY:-}"