# ============================================================================= # fleet-manager — server-side fleet control plane + admin SPA, plus its private # InfluxDB metrics store. Runs on the MANAGEMENT host (never the boot LAN). # # export COMPOSE_ENV_FILES=defaults.env,.env # layer defaults + your overrides # printf 'FLEET_MGR_INFLUX_TOKEN=...\nINFLUX_ADMIN_PASSWORD=...\n' > .env # secrets/overrides # ./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 traefik # # Config = defaults.env (committed) + .env (gitignored overrides), layered via # COMPOSE_ENV_FILES. Only put overrides + secrets in .env. # # 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 — passkey auth over PLAINTEXT behind the Traefik mgmt ingress: # Traefik terminates the LE cert on :443 and forwards plaintext here on # loopback, so the listener MUST stay private (127.0.0.1) — only Traefik # reaches it. INSECURE=true is the dev/no-auth escape hatch. FLEET_MGR_ADMIN_ADDR: "${FLEET_MGR_ADMIN_ADDR:-127.0.0.1:9500}" FLEET_MGR_ADMIN_INSECURE: "${FLEET_MGR_ADMIN_INSECURE:-false}" # WebAuthn RP — MUST be the UI's real origin (origin-bound passkeys). Behind # Traefik on :443 this is the PORT-LESS https origin. 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: agent-plane fleet-CA server cert + the broker admin-client # cert. (UI TLS is Traefik's — no admin cert here.) See DEPLOY.md/server-pki.sh. - ./pki:/pki:ro # --- agent-plane server-cert auto-renewer (optional; step-ca co-located) ----- # Issues the manager's agent-plane server cert ONCE from the fleet step-ca, then # runs `step ca renew --daemon` to rotate it well before the CA's ~90-day cap — # replacing the manual server-pki.sh + `docker exec … step ca certificate` dance # and the periodic re-mint. Renewal PRESERVES the subject + SANs of the first # issuance, so agent-TLS SAN drift can't recur. The manager hot-reloads the # rotated cert via GetCertificate (certreload.go) — no restart, no dropped hub. # # OPT-IN via `--profile cert-auto`, and ONLY valid where the fleet step-ca # (serving/dot1x-bench) runs on THIS host: it mounts the CA root + intermediate + # 'enrol' provisioner password read-only and reaches the CA over host-loopback. # Where the CA is remote, leave this off and issue via server-pki.sh (or ACME). # # docker compose --profile cert-auto up -d agent-cert-renewer fleet-manager agent-cert-renewer: image: harbor.bitasylum.org/dockerhub/smallstep/step-cli:latest profiles: ["cert-auto"] restart: unless-stopped network_mode: host # reach the co-located CA on ${AGENT_CA_URL} user: "0:0" # writes into /pki/agent, then chowns to the manager uid environment: STEPPATH: /tmp/step HOME: /tmp AGENT_SANS: "${AGENT_CERT_SANS:-manager.fleet.lab,diskless-server.bitasylum.org}" CA_URL: "${AGENT_CA_URL:-https://localhost:8443}" PROVISIONER: "${FLEET_CA_PROVISIONER:-enrol}" RENEW_EXPIRES_IN: "${AGENT_CERT_RENEW_EXPIRES_IN:-720h}" CERT_NOT_AFTER: "${AGENT_CERT_NOT_AFTER:-2160h}" OWNER: "65532:65532" entrypoint: ["/bin/sh", "/renew/agent-cert-renewer.sh"] volumes: - ./pki/agent:/pki/agent # shared with the manager (rw here) - ./agent-cert-renewer.sh:/renew/agent-cert-renewer.sh:ro # fleet CA material (read-only), from the co-located step-ca stack. Override # the host paths in .env if your bench lays them out differently. - ${FLEET_CA_ROOT:-../dot1x-bench/pki/root_ca.crt}:/ca/root_ca.crt:ro - ${FLEET_CA_INTERMEDIATE:-../dot1x-bench/pki/intermediate_ca.crt}:/ca/intermediate_ca.crt:ro - ${FLEET_CA_PROVISIONER_PW:-../dot1x-bench/pki/stepca/secrets/password}:/ca/password:ro # --- management-plane ingress (Traefik) — admin UI on :443 ------------------ # Terminates the Let's Encrypt cert (Route 53 DNS-01) on :443 and forwards # plaintext to the manager (behind-proxy mode, loopback :9500). Host networking # so it binds :443 on the mgmt interface and reaches the host-networked manager # on 127.0.0.1:9500. Needs outbound reach to Let's Encrypt + the AWS Route 53 # API. ONLY the admin plane rides Traefik — the agent gRPC/mTLS plane (:9600) is # direct (Traefik can't terminate fleet-CA client-cert mTLS; hosts reach it via # OPNsense L3 routing). This is the k8s ingress-controller + cert-manager role # in miniature — the router/ACME model maps to Traefik Ingress + cert-manager. # # NOTE: only :443 (the admin UI is https-only; passkeys require it) so it can't # collide with a boot-LAN nginx :80 on a single-host lab. Route 53 DNS-01 needs # no :80. Set the Host() rule in traefik/dynamic/fleet-manager.yml to your UI host. traefik: image: harbor.bitasylum.org/dockerhub/library/traefik:v3.1 # mirror via harbor if no dockerhub egress restart: unless-stopped network_mode: host command: - "--global.checknewversion=false" - "--global.sendanonymoususage=false" - "--entrypoints.websecure.address=:443" - "--providers.file.directory=/etc/traefik/dynamic" - "--providers.file.watch=true" - "--certificatesresolvers.le.acme.email=${ACME_EMAIL:?set ACME_EMAIL in .env}" - "--certificatesresolvers.le.acme.storage=/acme/acme.json" - "--certificatesresolvers.le.acme.dnschallenge.provider=route53" # Public resolvers for the propagation check (split-horizon DNS won't serve # the public _acme-challenge). First dry-run vs LE staging: also add # --certificatesresolvers.le.acme.caserver=https://acme-staging-v02.api.letsencrypt.org/directory - "--certificatesresolvers.le.acme.dnschallenge.resolvers=8.8.8.8:53,1.1.1.1:53" - "--log.level=INFO" environment: # Route 53 DNS-01 creds (or attach an instance role + omit). IAM policy in # DEPLOY.md §2c; AWS_HOSTED_ZONE_ID optional (pins the zone). AWS_ACCESS_KEY_ID: "${AWS_ACCESS_KEY_ID:-}" AWS_SECRET_ACCESS_KEY: "${AWS_SECRET_ACCESS_KEY:-}" AWS_REGION: "${AWS_REGION:-us-east-1}" AWS_HOSTED_ZONE_ID: "${AWS_HOSTED_ZONE_ID:-}" volumes: - ./traefik/dynamic:/etc/traefik/dynamic:ro - traefik-acme:/acme # acme.json (ACME account + issued certs) — PERSIST depends_on: - fleet-manager volumes: influx-data: influx-config: fleet-data: traefik-acme: networks: fleet-mgmt: driver: bridge