fleet-manager: add acme side-car for the admin-UI cert (LE DNS-01 via Route 53)

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.)
This commit is contained in:
Audiobuzz
2026-07-04 10:13:51 +00:00
parent bf546a80a6
commit 9479b70b04
4 changed files with 143 additions and 17 deletions
+52 -16
View File
@@ -93,28 +93,64 @@ curl -s --cert ./pki/broker/clients/fleet-manager.crt \
https://127.0.0.1:9443/pending ; echo # expect [] or null (empty queue)
```
### 2c. Admin UI cert (Let's Encrypt DNS-01)
### 2c. Admin UI cert — Let's Encrypt DNS-01 via Route 53 (the `acme` side-car)
The UI hostname resolves on the mgmt net and is **not internet-reachable**, so
HTTP-01/TLS-ALPN-01 can't validate. Use **DNS-01**, which proves control of the
zone via a TXT record (and supports wildcards). Example with `lego`:
zone via a TXT record. The `acme` compose service does this automatically: a
long-running side-car (`acme/renew-loop.sh`, on the `goacme/lego` image) issues
the cert on first start and renews within 30 days of expiry, writing it into
`./pki/admin/` — the same dir the manager reads — so the manager's `certReloader`
hot-swaps it live, no restart.
```bash
# Credentials for your DNS provider are provider-specific (env vars).
lego --dns <provider> --domains diskless-server.bitasylum.org \
--email you@example.org --accept-tos run
# Point the manager at the issued files (it hot-reloads on renewal):
mkdir -p ./pki/admin
cp .lego/certificates/diskless-server.bitasylum.org.crt ./pki/admin/fullchain.pem
cp .lego/certificates/diskless-server.bitasylum.org.key ./pki/admin/privkey.pem
**IAM policy** — give the side-car AWS creds scoped to the `bitasylum.org` hosted
zone (put `AWS_ACCESS_KEY_ID`/`AWS_SECRET_ACCESS_KEY` in `.env`, or attach an
instance role and omit them):
```json
{
"Version": "2012-10-17",
"Statement": [
{ "Effect": "Allow", "Action": ["route53:GetChange"], "Resource": "arn:aws:route53:::change/*" },
{ "Effect": "Allow", "Action": ["route53:ListHostedZonesByName"], "Resource": "*" },
{ "Effect": "Allow",
"Action": ["route53:ListResourceRecordSets", "route53:ChangeResourceRecordSets"],
"Resource": "arn:aws:route53:::hostedzone/<YOUR_ZONE_ID>" }
]
}
```
Automate renewal (cron/systemd-timer) to rewrite those two files; the manager's
`certReloader` notices the mtime bump and swaps the cert live — no restart. A
publicly-trusted cert also keeps the WebAuthn secure-context clean (no browser
warnings). *Alternative:* front the loopback admin listener with your existing
mgmt reverse proxy and terminate the cert there; then set
`FLEET_MGR_ADMIN_INSECURE=false` still but let the proxy hold the cert.
**Run it** (bring the side-car up first so the cert exists before the manager):
```bash
# In .env: set ACME_EMAIL + the AWS creds above. ACME_DOMAIN is taken from
# FLEET_MGR_RP_ID. Optionally ACME_STAGING=1 for a first dry-run (untrusted, no
# rate limit) to prove Route 53 access, then re-run without it for a real cert.
docker compose up -d acme
docker compose logs -f acme # wait for "staged /out/{fullchain,privkey}.pem"
ls -l pki/admin/ # fullchain.pem + privkey.pem, owned by uid 65532
```
Then turn off insecure mode and (re)start the manager — the defaults
`FLEET_MGR_ADMIN_CERT=/pki/admin/fullchain.pem` / `_KEY=/pki/admin/privkey.pem`
already point at what the side-car wrote:
```bash
# ensure FLEET_MGR_ADMIN_INSECURE=false in .env, then:
docker compose up -d --force-recreate fleet-manager
docker compose logs fleet-manager | grep -i listening # "admin (HTTPS + passkey) listening"
```
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.
> **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
> `https://diskless-server.bitasylum.org` either by binding a routable
> `FLEET_MGR_ADMIN_ADDR` or fronting it with a mgmt proxy that passes TLS through
> (don't re-terminate, or the passkey origin/cert won't match).
## 3. Start InfluxDB