Files
diskless-workstation/serving/fleet-manager/acme/renew-loop.sh
T
Audiobuzz 4c1db3f765 fleet-manager: front the admin UI with Traefik (443 + Route 53 ACME), retire acme side-car
Management-plane ingress: a host-networked Traefik terminates the LE cert on :443
(Route 53 DNS-01, public-resolver propagation check) and forwards plaintext to
the manager running FLEET_MGR_ADMIN_BEHIND_PROXY=true on loopback :9500. Only the
admin plane rides Traefik — the agent gRPC/mTLS plane (:9600) stays direct (Traefik
can't terminate fleet-CA mTLS; hosts reach it via OPNsense L3 routing). This is the
k8s ingress-controller + cert-manager role in miniature.

- traefik/dynamic/fleet-manager.yml: file-provider router (Host rule -> manager),
  passHostHeader so the WebAuthn RP origin matches.
- compose: traefik service (static config as CLI flags so ${ACME_EMAIL} interpolates)
  + traefik-acme volume; manager switched to behind-proxy defaults, RP_ORIGIN now
  the port-less https origin.
- Retire the acme side-car (Traefik owns ACME now): remove the service + acme-state
  volume; renew-loop.sh kept + relabelled as the self-TLS-mode cert helper.
- :443 only (admin UI is https-only) so it can't collide with a boot-LAN nginx :80.
2026-07-05 00:57:04 +00:00

74 lines
3.3 KiB
Bash
Executable File

#!/bin/sh
# renew-loop.sh — admin-UI cert issue/renew loop (self-TLS alternative).
#
# NOTE: the default deployment fronts the admin UI with Traefik (docker-compose.yml),
# which owns ACME itself — this script is NOT used then. It remains as the cert
# helper for the SELF-TLS admin mode (FLEET_MGR_ADMIN_BEHIND_PROXY=false, no
# Traefik): run it under a `goacme/lego` container to write ./pki/admin/.
#
# Runs inside the goacme/lego image (alpine-based). Obtains the fleet-manager
# admin-UI cert from Let's Encrypt via DNS-01 over AWS Route 53, and writes it
# into /out — bind-mounted to the SAME ./pki/admin dir the manager reads, so the
# manager's certReloader hot-swaps it on the next mtime bump (NO restart).
#
# The UI is on the mgmt network (not internet-reachable), so only DNS-01 can
# validate; Route 53 automates the _acme-challenge TXT record via the AWS API.
# lego renews only within --days of expiry, so this can loop harmlessly.
#
# Env (from compose/.env): ACME_DOMAIN, ACME_EMAIL, AWS_* creds (or an instance
# role), optional ACME_STAGING=1 for a dry-run against LE staging.
set -eu
DOMAIN=${ACME_DOMAIN:?set ACME_DOMAIN}
EMAIL=${ACME_EMAIL:?set ACME_EMAIL}
UID_OUT=${RUNTIME_UID:-65532} # distroless "nonroot" — must be able to read the key
INTERVAL_OK=${INTERVAL_OK:-43200} # 12h between checks after success
INTERVAL_ERR=${INTERVAL_ERR:-3600} # 1h retry after a failure
STAGING=""
[ -n "${ACME_STAGING:-}" ] && {
STAGING="--server https://acme-staging-v02.api.letsencrypt.org/directory"
echo "acme: using LE STAGING — certs are UNTRUSTED (dry run)"
}
# The goacme/lego image ships the binary at /lego (not on PATH); fall back to it
# when we've overridden the entrypoint with the shell.
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
set -- renew --days 30
else
set -- run
fi
echo "acme: lego $* for $DOMAIN (Route 53 DNS-01)"
# shellcheck disable=SC2086 # $STAGING/$RESOLVERS intentionally word-split
if "$LEGO" --accept-tos --email "$EMAIL" --dns route53 --domains "$DOMAIN" \
--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 \
|| echo "acme: WARN could not chown to $UID_OUT (manager may not read the key)"
echo "acme: staged /out/{fullchain,privkey}.pem; next check in ${INTERVAL_OK}s"
sleep "$INTERVAL_OK"
else
echo "acme: lego FAILED (check AWS creds / zone perms); retry in ${INTERVAL_ERR}s"
sleep "$INTERVAL_ERR"
fi
done