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:
@@ -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
|
||||
|
||||
|
||||
Reference in New Issue
Block a user