# ============================================================================= # 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 # 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 -------------------------------------- # CI builds + pushes this image (.gitea/workflows/fleet-manager-image.yaml); # `docker compose pull` fetches it. `build: .` stays so local dev can still # `docker compose build fleet-manager`. Pin FLEET_MANAGER_VERSION in .env to a # CI tag (e.g. main-); defaults to :latest. fleet-manager: image: "git.bitasylum.org/audiobuzz/fleet-manager:${FLEET_MANAGER_VERSION:-latest}" build: . 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