Files
diskless-workstation/serving/fleet-manager/docker-compose.yml
T
Audiobuzz 693cf94695 fleet-manager: point acme DNS-01 propagation check at a public resolver
Split-horizon DNS made lego's propagation check query the container's internal
resolver, which doesn't serve the public _acme-challenge TXT record, so the
challenge timed out. Add ACME_RESOLVERS (comma-separated host:port, default
8.8.8.8:53) → one --dns.resolvers flag each. Documented in .env + DEPLOY.md.
2026-07-04 10:24:42 +00:00

154 lines
7.6 KiB
YAML

# =============================================================================
# fleet-manager — server-side fleet control plane + admin SPA, plus its private
# InfluxDB metrics store. Runs on the MANAGEMENT host (never the boot LAN).
#
# cp .env.example .env # set RP origin, Influx token, cert/PKI paths
# ./server-pki.sh # issue the agent-plane server cert (fleet CA)
# ../enrol-broker/admin-pki.sh ../dot1x-bench/pki/broker fleet-manager
# docker compose up -d influxdb
# docker compose run --rm fleet-manager register-admin <you> # passkey link
# docker compose up -d fleet-manager
#
# See DEPLOY.md for the full bring-up (PKI, Let's Encrypt DNS-01, register-admin).
#
# Trust model (two planes, see README.md):
# - admin : HTTPS + WebAuthn passkey, loopback 127.0.0.1:9500 (front with the
# mgmt reverse proxy that terminates the LE cert, or expose directly).
# - agent : gRPC + mTLS (fleet CA), on a routable mgmt interface so enrolled
# hosts can dial in (FLEET_MGR_AGENT_ADDR).
#
# fleet-manager uses HOST networking (like enrol-broker): it must reach the
# broker's admin plane on 127.0.0.1:9443 and bind the agent listener on a
# routable mgmt address. InfluxDB stays on a private bridge and is published
# ONLY to loopback (127.0.0.1:8086), so the host-networked manager reaches it
# while nothing else on any LAN can.
# =============================================================================
services:
# --- metrics store (private; only the manager reads/writes it) --------------
influxdb:
image: harbor.bitasylum.org/dockerhub/library/influxdb:2.7
restart: unless-stopped
# Publish to loopback only — the host-networked manager reaches it at
# 127.0.0.1:8086; it is never on the boot/mgmt LAN.
ports:
- "127.0.0.1:8086:8086"
environment:
DOCKER_INFLUXDB_INIT_MODE: setup
DOCKER_INFLUXDB_INIT_USERNAME: "${INFLUX_ADMIN_USER:-fleet-admin}"
DOCKER_INFLUXDB_INIT_PASSWORD: "${INFLUX_ADMIN_PASSWORD:?set INFLUX_ADMIN_PASSWORD in .env}"
DOCKER_INFLUXDB_INIT_ORG: "${FLEET_MGR_INFLUX_ORG:-fleet}"
DOCKER_INFLUXDB_INIT_BUCKET: "${FLEET_MGR_INFLUX_BUCKET:-fleet_health}"
DOCKER_INFLUXDB_INIT_RETENTION: "${INFLUX_RETENTION:-90d}"
# Deterministic operator token so the manager can be handed the SAME value
# in FLEET_MGR_INFLUX_TOKEN without a post-setup copy step. Treat as a
# secret (it lives in .env, which is gitignored).
DOCKER_INFLUXDB_INIT_ADMIN_TOKEN: "${FLEET_MGR_INFLUX_TOKEN:?set FLEET_MGR_INFLUX_TOKEN in .env}"
volumes:
- influx-data:/var/lib/influxdb2
- influx-config:/etc/influxdb2
networks:
- fleet-mgmt
healthcheck:
test: ["CMD", "influx", "ping"]
interval: 15s
timeout: 5s
retries: 5
# --- the control plane + embedded SPA --------------------------------------
fleet-manager:
build: .
image: fleet-manager:local
restart: unless-stopped
# Host networking: reach the broker on 127.0.0.1:9443 + InfluxDB on
# 127.0.0.1:8086, and bind the agent gRPC listener on a routable mgmt addr.
network_mode: host
depends_on:
influxdb:
condition: service_healthy
environment:
# admin plane (HTTPS + passkey). Bind loopback; the mgmt proxy (holding the
# LE cert) fronts it, or set the cert paths below + expose directly.
FLEET_MGR_ADMIN_ADDR: "${FLEET_MGR_ADMIN_ADDR:-127.0.0.1:9500}"
FLEET_MGR_ADMIN_CERT: "${FLEET_MGR_ADMIN_CERT:-/pki/admin/fullchain.pem}"
FLEET_MGR_ADMIN_KEY: "${FLEET_MGR_ADMIN_KEY:-/pki/admin/privkey.pem}"
FLEET_MGR_ADMIN_INSECURE: "${FLEET_MGR_ADMIN_INSECURE:-false}"
# WebAuthn RP — MUST be the UI's real origin (origin-bound passkeys).
FLEET_MGR_RP_ID: "${FLEET_MGR_RP_ID:?set FLEET_MGR_RP_ID in .env}"
FLEET_MGR_RP_ORIGIN: "${FLEET_MGR_RP_ORIGIN:?set FLEET_MGR_RP_ORIGIN in .env}"
FLEET_MGR_RP_NAME: "${FLEET_MGR_RP_NAME:-Fleet Command}"
FLEET_MGR_ENROLL_KEY_FILE: "/data/enroll.key"
FLEET_MGR_ADMIN_STORE: "/data/admins.json"
FLEET_MGR_SESSION_TTL_MIN: "${FLEET_MGR_SESSION_TTL_MIN:-720}"
# agent plane (gRPC + mTLS, fleet CA). Bind a routable mgmt addr so hosts
# can dial in; verify host TPM certs against the fleet root.
FLEET_MGR_AGENT_ADDR: "${FLEET_MGR_AGENT_ADDR:-:9600}"
FLEET_MGR_AGENT_CERT: "/pki/agent/server.crt"
FLEET_MGR_AGENT_KEY: "/pki/agent/server.key"
FLEET_MGR_AGENT_CLIENT_CA: "/pki/agent/fleet-ca.crt"
# authorise pillar — drive the enrol-broker admin plane over mTLS.
FLEET_MGR_BROKER_URL: "${FLEET_MGR_BROKER_URL:-https://127.0.0.1:9443}"
FLEET_MGR_BROKER_CLIENT_CERT: "/pki/broker/clients/fleet-manager.crt"
FLEET_MGR_BROKER_CLIENT_KEY: "/pki/broker/clients/fleet-manager.key"
FLEET_MGR_BROKER_CA: "/pki/broker/admin-ca.crt"
# metrics — the manager is the ONLY reader/writer; token stays server-side.
FLEET_MGR_INFLUX_URL: "http://127.0.0.1:8086"
FLEET_MGR_INFLUX_TOKEN: "${FLEET_MGR_INFLUX_TOKEN:?set FLEET_MGR_INFLUX_TOKEN in .env}"
FLEET_MGR_INFLUX_ORG: "${FLEET_MGR_INFLUX_ORG:-fleet}"
FLEET_MGR_INFLUX_BUCKET: "${FLEET_MGR_INFLUX_BUCKET:-fleet_health}"
FLEET_MGR_INVENTORY: "/data/inventory.json"
HOSTNQN_BASE: "${HOSTNQN_BASE:-nqn.2026-06.org.example.fleet:host-}"
volumes:
# Durable state (inventory + admin passkeys + enrol key). Back this up.
- fleet-data:/data
# PKI, read-only: admin LE cert, agent-plane fleet-CA server cert, and the
# broker admin-client cert. See DEPLOY.md / server-pki.sh for what lands here.
# The admin LE cert under ./pki/admin/ is produced by the `acme` side-car.
- ./pki:/pki:ro
# --- admin-UI cert renewal side-car (Let's Encrypt DNS-01 via Route 53) -----
# Long-running "compose cron": issues the UI cert on first start, then renews
# within 30 days of expiry. Writes into ./pki/admin/ — the SAME dir the manager
# reads (./pki:/pki:ro) — so the manager hot-reloads it with no restart. Needs
# outbound reach to Let's Encrypt + the AWS Route 53 API (bridge NAT).
# Bring this up BEFORE the manager (or the manager crash-loops until the cert
# exists, then self-heals). See DEPLOY.md §2c.
acme:
image: goacme/lego:v4.17.4 # mirror via harbor if no dockerhub egress
restart: unless-stopped
entrypoint: ["/bin/sh", "/renew-loop.sh"]
environment:
ACME_DOMAIN: "${FLEET_MGR_RP_ID:?set FLEET_MGR_RP_ID in .env}" # = the UI hostname
ACME_EMAIL: "${ACME_EMAIL:?set ACME_EMAIL in .env}"
ACME_STAGING: "${ACME_STAGING:-}" # set 1 for a dry-run
# Public resolver(s) for the propagation check — the internal/split-horizon
# DNS doesn't serve the public _acme-challenge record. Comma-separated host:port.
ACME_RESOLVERS: "${ACME_RESOLVERS:-8.8.8.8:53}"
# AWS creds for the Route 53 DNS-01 challenge (or omit + use an instance role).
AWS_ACCESS_KEY_ID: "${AWS_ACCESS_KEY_ID:-}"
AWS_SECRET_ACCESS_KEY: "${AWS_SECRET_ACCESS_KEY:-}"
AWS_REGION: "${AWS_REGION:-us-east-1}"
# Optional: pin the zone (skips the ListHostedZonesByName lookup — needed if
# a public + private zone share the domain; lets you tighten the IAM policy).
AWS_HOSTED_ZONE_ID: "${AWS_HOSTED_ZONE_ID:-}"
volumes:
- ./acme/renew-loop.sh:/renew-loop.sh:ro
- acme-state:/le # lego account + cert store — PERSIST
- ./pki/admin:/out # shared with the manager's ./pki/admin
networks:
- fleet-mgmt
volumes:
influx-data:
influx-config:
fleet-data:
acme-state:
networks:
fleet-mgmt:
driver: bridge