#!/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