Auto-issue + auto-renew the admin-UI TLS cert with a long-running compose side-car instead of a host cron. The `acme` service (goacme/lego image, running acme/renew-loop.sh) obtains the cert over DNS-01 via the AWS Route 53 API, writes fullchain/privkey into ./pki/admin/ — the SAME dir the manager reads (./pki:/pki:ro) — and chowns them to the nonroot runtime uid so the read-only container can load the key. The manager's certReloader hot-swaps on the mtime bump, no restart. Loop renews within 30 days of expiry; ACME_STAGING=1 gives a dry-run vs LE staging. - docker-compose.yml: `acme` service + `acme-state` volume (lego account/cert store). ACME_DOMAIN reuses FLEET_MGR_RP_ID so cert host == passkey origin. - .env.example: ACME_EMAIL + AWS creds (or instance role) + ACME_STAGING. - DEPLOY.md §2c: rewritten to the side-car flow — scoped Route 53 IAM policy, bring-acme-up-first ordering, flip off insecure, and an origin-match note. (The docker-compose.yml commit also carries the local harbor.bitasylum.org influxdb image mirror edit.)
52 lines
2.2 KiB
Bash
Executable File
52 lines
2.2 KiB
Bash
Executable File
#!/bin/sh
|
|
# renew-loop.sh — admin-UI cert issue/renew loop for the `acme` side-car.
|
|
#
|
|
# 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)"
|
|
}
|
|
|
|
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 is intentionally two words when set
|
|
if lego --accept-tos --email "$EMAIL" --dns route53 --domains "$DOMAIN" \
|
|
--path /le $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
|