The agent-plane server cert was issued by hand (server-pki.sh / docker exec step ca certificate), which needs the step CLI on the host, re-specifies SANs each time, and silently expires at the CA's 90-day cap — the exact failure that just broke agent TLS (cert minted with a stale SAN, missing manager.fleet.lab). Automate it: - agent-cert-renewer sidecar (smallstep step-cli image, compose profile cert-auto): issues the cert once from the co-located fleet step-ca, then 'step ca renew --daemon' rotates it before the cap. Renewal preserves the subject + SANs, so SAN drift cannot recur. Chowns to the manager's nonroot uid so the distroless manager reads the key. Opt-in + only valid where the CA is co-located; server-pki.sh stays as the manual/remote-CA path. - manager hot-reloads via GetCertificate (certreload.go): a rotated cert on disk is picked up on the next handshake with no restart and no dropped agent streams; a half-written pair keeps the last good cert serving. Unit-tested. - defaults.env documents the cert-auto knobs; DEPLOY.md §2a recommends the renewer and flags the mandatory manager.fleet.lab SAN.
213 lines
11 KiB
YAML
213 lines
11 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).
|
|
#
|
|
# 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 <you> # 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-<sha7>); 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
|