fleet-manager: checkpoint 7 — deployment (compose, PKI, deploy runbook, design-doc updates)

Complete the deployment story for the server-side control plane:

- docker-compose.yml: private InfluxDB (published to loopback only) + a
  host-networked fleet-manager (reaches the loopback enrol-broker on
  127.0.0.1:9443 and binds a routable gRPC agent listener, like the broker).
- .env.example: RP origin (diskless-server.bitasylum.org), Influx token/DB
  password, cert/PKI paths; the Influx token wires influxdb + manager with
  one value.
- server-pki.sh: issue the agent-plane server cert from the fleet CA (step)
  and stage the fleet root as the client-verify CA.
- DEPLOY.md: end-to-end runbook — three-cert PKI (fleet-CA agent cert, broker
  admin-client cert, Let's Encrypt DNS-01 UI cert), register-admin passkey
  bootstrap, compose up, hardware-free smoke test, backups.
- .dockerignore: keep host node_modules/dist/pki/secrets out of the build
  context so the embedded SPA is the one built in-image.
- .gitignore: never commit .env / issued PKI / data.

Design docs marked as-built: fleet-management-design.md as-built note
(resolved forks — gRPC bidi transport, InfluxDB metrics, SPA graphs, passkey
admin auth, broker fold-in "alongside"); tpm-enrolment-design.md (broker
fold-in + agent transport fork resolved); TODO.md #14 → partly built, with
the on-host agent + per-host boot-version assignment (#6) called out as the
remaining work.
This commit is contained in:
Audiobuzz
2026-07-04 08:44:31 +00:00
parent f62c668ce3
commit 2ef0c1747d
10 changed files with 523 additions and 10 deletions
+32 -1
View File
@@ -1371,7 +1371,36 @@ remains documented here if a minimal/auditable cmdline is ever wanted.
**BENCH-VERIFY:** `chrome://policy` shows `ExtensionInstallBlocklist=["*"]` active;
extensions can't install.
## [ ] 14. (DESIGN) Centralised fleet management plane (health/version/usage/maintenance)
## [~] 14. (PARTLY BUILT) Centralised fleet management plane (health/version/usage/maintenance)
**SERVER-SIDE CONTROL PLANE BUILT 2026-07 — `serving/fleet-manager/`** (Go service
+ embedded Svelte SPA). See the **as-built note** at the top of
[docs/fleet-management-design.md](fleet-management-design.md). What shipped and how
the design forks resolved:
- **Two planes:** admin (HTTPS + **WebAuthn/passkey**, YubiKey; server-side session
+ CSRF; `register-admin` CLI bootstrap; UI cert via **Let's Encrypt DNS-01**,
hot-reloaded) and agent (**bespoke gRPC bidi/mTLS**, fleet CA, reused TPM cert —
health/metrics up, desired-state down; no inbound host port).
- **Metrics → InfluxDB** (manager is the only reader/writer; host holds no Influx
credential; identity-tagged writes; Flux reads behind a field whitelist + hex-
serial guard). Graphs render **in the SPA** (bespoke SVG), not Grafana. This
replaces the earlier Wazuh-Phase-1 / OTLP-Phase-2 sketch for *this* plane;
Wazuh stays the audit/SIEM path.
- **Authorise pillar:** the enrol-broker (#11) folded in "alongside" — manager
drives its admin plane over mTLS and **births the inventory record** on approve.
- **Deployment:** `Dockerfile` + `docker-compose.yml` (private InfluxDB +
host-networked manager) + `server-pki.sh` + `DEPLOY.md`.
- **STILL TO BUILD:** (a) the **on-host agent** — the gRPC report/reconcile client
that pairs with `fleet-hostapi` (feeds this plane; nothing on the host talks to
the manager yet); (b) **per-host boot-version assignment** — `target_version` is
stored + pushed but does not change what a host *boots*; that needs dnsmasq
cohort steering + multi-version UKI serving (**#6**). Kiosk URL / reboot / drain
reconcile via the agent once (a) lands. Version pin is labelled "recorded intent"
in the Lifecycle UI.
Depends on the step-ca fleet CA (#8) for the mTLS identity.
<details><summary>Original design capture (2026-06-28) — retained for reference</summary>
**DESIGN CAPTURED 2026-06-28 — [docs/fleet-management-design.md](fleet-management-design.md).**
Server-authoritative, pull-based, TPM-identified management plane reusing existing
@@ -1388,6 +1417,8 @@ manifest + SPDK flow. Build-time decisions (telemetry encoding, metrics/desired-
state store, per-host vs cohort, osquery/FleetDM eval) listed in the design note.
Depends on the step-ca fleet CA (#8) for the mTLS identity.
</details>
## [ ] 15. Scope the root overlay → per-subtree tmpfs/overlay (un-shadowable `/usr` + STIG mount options)
**DESIGN CAPTURED 2026-07-02.** Today the whole root is one `systemd.volatile=overlay`
+51 -2
View File
@@ -1,7 +1,56 @@
# Fleet management plane — design note
**Status:** DESIGN (not built). Captured 2026-06-28. Build phased — see "Phasing"
and `docs/TODO.md` #14.
**Status:** PARTLY BUILT. Captured 2026-06-28; **server-side control plane built
2026-07** (`serving/fleet-manager/`). The sections below are the original design;
the **as-built note** immediately following records what shipped and how the
open forks resolved. Build still phased — see "Phasing" and `docs/TODO.md` #14.
## As-built note (2026-07) — `serving/fleet-manager/`
The **server-side control plane + admin SPA is built** and supersedes several
"open questions" below. What shipped:
- **One service, two planes** (`serving/fleet-manager/`, Go): an **admin plane**
(HTTPS + SPA + JSON API) and an **agent plane** (gRPC/mTLS). Runs as its own
container on the mgmt host (`docker-compose.yml`: private InfluxDB +
host-networked manager). Deployment runbook: `serving/fleet-manager/DEPLOY.md`.
- **Resolved forks (were "open questions / decisions"):**
- *Steady-state transport (agent ↔ manager)* → **bespoke gRPC bidi stream**
(`proto/fleet_agent.proto`: `FleetAgent.Session`), host-dialed mTLS, health/
metrics up + desired-state down. Chosen over OTLP+Wazuh+control-stream: one
stream, no polling, no inbound host port. Enrolment stays plain HTTP/JSON.
- *Metrics store* → **InfluxDB 2.x** (was VictoriaMetrics/Prometheus). The
manager is the **only** reader/writer (host holds no Influx credential);
writes are line-protocol tagged with the mTLS-authenticated identity, reads
are a Flux query behind a **field whitelist + hex-serial guard**. Graphs are
rendered **in the SPA** (bespoke SVG), **not Grafana**.
- *Dashboards* → the **LYSANDRA restricted-violet SPA** (Svelte 5 + Tailwind 4,
embedded via `go:embed`): Monitor / Authorise / Lifecycle. Not Grafana/Wazuh
dashboards for this plane (Wazuh stays the audit/SIEM path).
- *Desired-state store* → a single **JSON file** (atomic writes) behind a
`Store` interface — not git/DB yet; a DB is a drop-in. Server-authoritative.
- *Admin/browser identity* → **WebAuthn/passkey (YubiKey)**, not browser mTLS:
usernameless discoverable login, UV required, server-side session + CSRF.
Bootstrap is a closed CLI (`register-admin`, one-time HMAC enrol link). The
UI cert is **Let's Encrypt DNS-01** (mgmt-net host, hot-reloaded). The
*manager→broker* hop stays mTLS (service-to-service, broker admin CA).
- *Authorise pillar* → the **enrol-broker folded in** "alongside": the broker
stays the state machine + step-ca signer; the manager drives its admin plane
over mTLS (Accept-gated JSON approve) and **births the inventory record** on
approve, recording the human passkey admin as approver.
- **Agent identity confirmed:** the agent reuses the **same TPM identity cert**
as 802.1X EAP-TLS / NVMe hostnqn — "one identity, three stages". Peer cert
serial is authoritative; an unenrolled serial is refused.
- **NOT yet built (recorded intent):** per-host **boot-version assignment**.
`desired.target_version` is stored + pushed, but changing what a host *boots*
needs dnsmasq cohort steering + multi-version UKI serving (`docs/TODO.md` #6).
Kiosk URL / reboot / drain reconcile via the agent today; the version pin is
labelled "recorded intent" in the Lifecycle UI. The **on-host agent** that
feeds this plane (extend `fleet-hostapi` with the gRPC report/reconcile client)
is likewise still to build.
Everything below is the original design context; treat the as-built note as
authoritative where they differ.
Centralised monitoring + maintenance of the diskless kiosk fleet: **health,
version, usage, and maintenance actions**. This note is the authoritative design;
+19 -5
View File
@@ -146,6 +146,18 @@ pinned for mTLS — one trust anchor across 802.1X, NVMe, and management.
everything else on. Build the broker standalone first (fastest path to the state
machine), but design it to fold into the mgmt service with no rewrite — same Go
style, same step-ca anchor, same data model.
- **AS-BUILT (2026-07): folded in "alongside", not merged.** The
`serving/fleet-manager/` control plane drives the standalone broker's admin
plane over mTLS (broker admin CA) rather than absorbing its code: the broker
stays the enrolment **state machine + step-ca signer**, and gained an
`Accept: application/json` approve response so the manager gets the decided
enrolment back and **births the inventory record**, recording the human
(passkey-authenticated) admin as approver. `verify-serving.sh` / browser
callers are unchanged (still get the 204/redirect). The public
`/enrol/request|status` edge is untouched. This keeps the one unauth,
stdlib-only, reproducible pre-auth parser isolated while giving the manager
the authenticated authorise pillar — same trust anchor, same data model, no
rewrite. See `serving/fleet-manager/README.md` (Authorise pillar).
- **host-api is not in the enrolment path** (it is loopback/post-boot; the host has
no cert during enrolment). The tie is downstream: the fleet-manager **agent** (the
push sibling of `fleet-hostapi`) authenticates with the *enrolled* cert over mTLS,
@@ -200,11 +212,13 @@ fleet-manager agent) — one language, one build/posture story. Transport is spl
- **Steady-state agent ↔ manager (telemetry + reconciliation) → gRPC over mTLS,
host-dialed.** Streaming fits here, and it stays compatible with "no inbound port on
hosts" because the **host dials out** and holds one long-lived mTLS stream (health
up / desired-state down). OPEN FORK (a fleet-manager build decision, not enrolment):
bespoke gRPC vs reuse **OTLP-gRPC for metrics + Wazuh for events** + a thin gRPC
control stream — the latter gets VictoriaMetrics/Grafana/Wazuh for free. `Connect`
(connectrpc.com) is an option if one protobuf IDL serving gRPC + JSON + browser is
wanted across the authenticated surface.
up / desired-state down). ~~OPEN FORK~~ **RESOLVED (2026-07):** a **bespoke gRPC
bidi stream** (`serving/fleet-manager/proto/fleet_agent.proto` —
`FleetAgent.Session`, health/metrics up + desired-state down), over the same
reused TPM cert (fleet CA). Metrics land in **InfluxDB** via the manager (host
holds no Influx credential), not OTLP→VictoriaMetrics; events stay on the Wazuh
path. One stream, no polling, no inbound host port. (The on-host agent — the
gRPC report/reconcile client that pairs with `fleet-hostapi` — is still to build.)
- **host-api stays HTTP/JSON — locked**, because its consumer is the Chromium kiosk
(a browser, which cannot speak native gRPC).
+11
View File
@@ -0,0 +1,11 @@
# The image builds the SPA in its own stage and the Go stage embeds THAT dist —
# keep host build artifacts, deps, secrets, and PKI out of the build context.
ui/node_modules
ui/dist
node_modules
/fleet-manager
*.test
.env
pki/
data/
.git
+46
View File
@@ -0,0 +1,46 @@
# Copy to .env and edit. See DEPLOY.md for the full bring-up.
# .env is gitignored — it holds the Influx token and DB password.
# --- WebAuthn RP (admin passkeys are ORIGIN-BOUND — must be the UI's real URL) --
# For this deployment the UI is https://diskless-server.bitasylum.org .
FLEET_MGR_RP_ID=diskless-server.bitasylum.org
FLEET_MGR_RP_ORIGIN=https://diskless-server.bitasylum.org
FLEET_MGR_RP_NAME=Fleet Command
# --- admin plane HTTPS ---------------------------------------------------------
# The UI cert is a publicly-trusted Let's Encrypt cert via DNS-01 (the UI is on
# the mgmt net, not internet-reachable, so HTTP-01/TLS-ALPN-01 can't validate;
# DNS-01 proves control of bitasylum.org via a TXT record). An external ACME
# client (lego/certbot + your DNS provider) writes fullchain/privkey into
# ./pki/admin/ ; the manager hot-reloads on renewal. See DEPLOY.md.
FLEET_MGR_ADMIN_ADDR=127.0.0.1:9500
FLEET_MGR_ADMIN_CERT=/pki/admin/fullchain.pem
FLEET_MGR_ADMIN_KEY=/pki/admin/privkey.pem
# Dev-only escape hatch: plaintext, NO passkey auth. NEVER set true in prod.
FLEET_MGR_ADMIN_INSECURE=false
# --- agent plane (gRPC + mTLS, fleet CA) ---------------------------------------
# Routable mgmt address enrolled hosts dial (":9600" = all interfaces). The
# server cert is issued by the fleet CA (server-pki.sh); the client CA is the
# fleet root so host TPM certs verify. Paths are fixed in docker-compose.yml.
FLEET_MGR_AGENT_ADDR=:9600
# --- authorise pillar (enrol-broker admin plane over mTLS) ---------------------
# The broker runs network_mode:host on 127.0.0.1:9443. The manager's client cert
# is minted by ../enrol-broker/admin-pki.sh (admin CA), into ./pki/broker/ .
FLEET_MGR_BROKER_URL=https://127.0.0.1:9443
# --- InfluxDB (private; the manager is the ONLY reader/writer) -----------------
# The token is handed to BOTH influxdb (init admin token) and the manager, so a
# single value wires the two with no copy step. Treat as a secret.
FLEET_MGR_INFLUX_TOKEN=changeme-generate-a-long-random-token
FLEET_MGR_INFLUX_ORG=fleet
FLEET_MGR_INFLUX_BUCKET=fleet_health
INFLUX_ADMIN_USER=fleet-admin
INFLUX_ADMIN_PASSWORD=changeme-influx-admin-password
INFLUX_RETENTION=90d
# --- misc ----------------------------------------------------------------------
FLEET_MGR_SESSION_TTL_MIN=720
# Must match the enrol-broker's HOSTNQN_BASE.
HOSTNQN_BASE=nqn.2026-06.org.example.fleet:host-
+4
View File
@@ -1,2 +1,6 @@
/fleet-manager
*.test
# deployment secrets + issued PKI (never commit)
.env
/pki/
/data/
+155
View File
@@ -0,0 +1,155 @@
# fleet-manager — deployment runbook
Bring-up of the fleet-manager (control plane + admin SPA) and its private
InfluxDB on the **management host**. This is a lab/on-prem deployment; adapt the
PKI/ACME steps to your environment. See [`README.md`](README.md) for the design
and [`docs/fleet-management-design.md`](../../docs/fleet-management-design.md) for
the architecture.
> **Where it runs.** The management host — the trusted side, alongside the
> enrol-broker and step-ca (`serving/dot1x-bench/`). **Never on the boot LAN.**
> The admin plane (passkey UI) faces admins; the agent plane (gRPC/mTLS) faces
> enrolled hosts; both are segmented from the boot network.
## Prerequisites
- Docker + compose, and the `step` CLI (smallstep) on the host.
- The **fleet step-ca** already bootstrapped (`serving/dot1x-bench/` —
`docker compose up -d step-ca && ./step-ca/bootstrap.sh`), so
`../dot1x-bench/pki/root_ca.crt` and the `enrol` provisioner exist.
- The **enrol-broker** running (`serving/dot1x-bench/` compose) with its admin
plane on `127.0.0.1:9443`.
- A DNS zone you control for the UI hostname (`bitasylum.org`) and an ACME
client that can do **DNS-01** (e.g. `lego` or `certbot` + your DNS provider).
- A **YubiKey** (or other WebAuthn authenticator) for the bootstrap admin.
## 1. Configure
```bash
cd serving/fleet-manager
cp .env.example .env
# Edit .env:
# FLEET_MGR_RP_ID / _ORIGIN -> your UI hostname (diskless-server.bitasylum.org)
# FLEET_MGR_INFLUX_TOKEN -> a long random secret (openssl rand -hex 32)
# INFLUX_ADMIN_PASSWORD -> a strong password
```
The Influx token is handed to **both** InfluxDB (its init admin token) and the
manager, so one value wires them with no copy step. `.env` is gitignored.
## 2. PKI
Three certs, three trust roots — do not conflate them (see README "two planes"):
### 2a. Agent-plane server cert (fleet CA)
Enrolled hosts dial the gRPC agent plane and verify the manager against the
**fleet root** they hold, so this cert is issued by the fleet CA:
```bash
# SANs = every name/IP a host might dial the agent plane on.
AGENT_SANS=diskless-server.bitasylum.org,10.0.0.1 \
./server-pki.sh
# -> ./pki/agent/{server.crt,server.key,fleet-ca.crt}
```
### 2b. Broker admin-client cert (broker admin CA)
The manager is an mTLS **client** of the enrol-broker's admin plane. Mint its
client cert from the broker's **dedicated admin CA** (NOT the fleet CA — reusing
the fleet CA would let any enrolled host authenticate to the admin plane):
```bash
../enrol-broker/admin-pki.sh ../dot1x-bench/pki/broker fleet-manager
cp -r ../dot1x-bench/pki/broker ./pki/broker
# -> ./pki/broker/{admin-ca.crt, clients/fleet-manager.{crt,key}}
```
### 2c. Admin UI cert (Let's Encrypt DNS-01)
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`:
```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
```
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.
## 3. Start InfluxDB
```bash
docker compose up -d influxdb
docker compose ps # wait for healthy
```
First start runs setup with the org/bucket/token/retention from `.env`.
## 4. Bootstrap the first admin (passkey)
Admin auth is closed — there is no password. Mint a **one-time enrolment link**
and register your passkey in the browser:
```bash
docker compose run --rm fleet-manager register-admin "your-name"
# prints: https://diskless-server.bitasylum.org/#/enrol?token=...
```
The `register-admin` subcommand generates the shared enrol-HMAC key on first run
(into the `fleet-data` volume at `/data/enroll.key`) and mints an HMAC-signed,
single-use token (jti ledger prevents replay). Open the link on a machine with
your YubiKey, tap + PIN, and the passkey is registered. Further admins can be
enrolled from the UI once one exists.
## 5. Start the manager
```bash
docker compose up -d fleet-manager
docker compose logs -f fleet-manager
```
Healthy startup logs the four planes: admin HTTPS+passkey, agent gRPC/mTLS,
authorise (broker), and the InfluxDB gateway. Any plane whose config is absent
logs `DISABLED` and the rest still run.
Then browse to `https://diskless-server.bitasylum.org/`, log in with the passkey,
and you should see **Monitor / Authorise / Lifecycle**.
## Smoke test without hardware
To render the SPA against demo data with no passkey/PKI:
```bash
docker compose run --rm -e FLEET_MGR_ADMIN_INSECURE=true -e FLEET_MGR_SEED=true \
-e FLEET_MGR_ADMIN_ADDR=0.0.0.0:9500 -p 9500:9500 fleet-manager
# http://localhost:9500/ (whoami returns "dev-insecure"; two seeded hosts)
```
`FLEET_MGR_ADMIN_INSECURE=true` is **dev/lab only** — plaintext, no passkey.
## Backups
Back up the `fleet-data` volume (`inventory.json`, `admins.json`, `enroll.key`)
and the `influx-data` volume. Losing `admins.json` locks every admin out — you'd
re-bootstrap with `register-admin`. Losing `enroll.key` invalidates any
outstanding (unused) enrol links.
## What is NOT yet wired (recorded intent)
Per-host **boot-version assignment** (`desired.target_version`) is stored and
pushed to the agent, but does not yet change what a host boots: that needs
dnsmasq cohort steering + multi-version UKI serving (`docs/TODO.md` #6). Kiosk
URL / reboot / drain **do** reconcile via the agent today. The Lifecycle page
labels the version pin as recorded intent.
+15 -2
View File
@@ -137,7 +137,15 @@ inventory for offline delivery. Metrics writing is behind a `MetricsSink`
| `FLEET_MGR_SEED` | `false` | inject demo hosts so the SPA has data — dev only |
| `HOSTNQN_BASE` | `nqn.2026-06.org.example.fleet:host-` | hostnqn prefix (match the broker) |
InfluxDB env lands in checkpoint 4.
## Deploy
Full bring-up (PKI, Let's Encrypt DNS-01, `register-admin`, compose) is in
[`DEPLOY.md`](DEPLOY.md). In short: `cp .env.example .env` → `./server-pki.sh` +
`../enrol-broker/admin-pki.sh … fleet-manager` → `docker compose up -d influxdb`
→ `docker compose run --rm fleet-manager register-admin <you>` → `docker compose
up -d fleet-manager`. InfluxDB is private (published to loopback only); the
manager uses host networking to reach the loopback broker + bind a routable
agent listener.
## Dev run
@@ -175,4 +183,9 @@ curl -s localhost:9500/api/hosts | jq '.hosts[] | {name:.identity.starship_name,
Lifecycle + bespoke SVG charts. Embedded via `go:embed` behind the `embedspa`
build tag; served same-origin over the admin plane. Smoke-tested end-to-end
(index + assets + API through the SPA origin).
- Next: Docker/compose + PKI/RP/ACME + docs (ckpt 7).
- **Checkpoint 7 (done):** deployment — three-stage `Dockerfile` (npm→dist→
`go build -tags embedspa`→distroless), `docker-compose.yml` (private InfluxDB +
host-networked manager), `.env.example`, `server-pki.sh` (agent-plane fleet-CA
server cert), and [`DEPLOY.md`](DEPLOY.md) (PKI, LE DNS-01, `register-admin`
bootstrap). Design docs updated (manager marked built; resolved transport/auth/
metrics forks; per-host-versioning gap recorded).
+119
View File
@@ -0,0 +1,119 @@
# =============================================================================
# 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: 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.
- ./pki:/pki:ro
volumes:
influx-data:
influx-config:
fleet-data:
networks:
fleet-mgmt:
driver: bridge
+71
View File
@@ -0,0 +1,71 @@
#!/usr/bin/env bash
# server-pki.sh — issue the fleet-manager AGENT-plane server cert from the fleet
# CA, and stage the fleet root as the agent client-verify CA.
#
# The agent plane is gRPC + mTLS: enrolled hosts present their TPM identity cert
# (fleet CA), and they verify the MANAGER's server cert against the same fleet
# root they hold. So the manager's agent-plane server cert MUST be signed by the
# fleet CA — this script issues it via `step ca certificate` against the running
# fleet step-ca (serving/dot1x-bench/), the same CA that signs enrolment certs.
#
# This does NOT issue:
# - the ADMIN-plane UI cert -> Let's Encrypt DNS-01 (external ACME client;
# drop fullchain/privkey into ./pki/admin/). See DEPLOY.md.
# - the manager's BROKER client cert -> ../enrol-broker/admin-pki.sh (admin CA):
# ../enrol-broker/admin-pki.sh ../dot1x-bench/pki/broker fleet-manager
#
# Usage: server-pki.sh
# env overrides:
# OUT=./pki/agent output dir
# AGENT_SANS=diskless-server.bitasylum.org,10.0.0.1 SANs hosts dial (DNS/IP, comma-sep)
# FLEET_CA_URL=https://ca.fleet.lab:8443
# FLEET_CA_ROOT=../dot1x-bench/pki/root_ca.crt
# STEP_PROVISIONER=enrol
# STEP_PROVISIONER_PASSWORD_FILE=../dot1x-bench/pki/stepca/secrets/password
# CERT_NOT_AFTER=8760h
set -euo pipefail
OUT=${OUT:-./pki/agent}
AGENT_SANS=${AGENT_SANS:-diskless-server.bitasylum.org}
FLEET_CA_URL=${FLEET_CA_URL:-https://ca.fleet.lab:8443}
FLEET_CA_ROOT=${FLEET_CA_ROOT:-../dot1x-bench/pki/root_ca.crt}
STEP_PROVISIONER=${STEP_PROVISIONER:-enrol}
STEP_PROVISIONER_PASSWORD_FILE=${STEP_PROVISIONER_PASSWORD_FILE:-../dot1x-bench/pki/stepca/secrets/password}
CERT_NOT_AFTER=${CERT_NOT_AFTER:-8760h}
command -v step >/dev/null || { echo "error: 'step' CLI not found (smallstep/cli)"; exit 1; }
[ -f "$FLEET_CA_ROOT" ] || { echo "error: fleet root not found at $FLEET_CA_ROOT (bootstrap the fleet CA first)"; exit 1; }
[ -f "$STEP_PROVISIONER_PASSWORD_FILE" ] || { echo "error: provisioner password not found at $STEP_PROVISIONER_PASSWORD_FILE"; exit 1; }
mkdir -p "$OUT"
# The fleet root doubles as the agent-plane CLIENT CA (verifies host TPM certs).
cp "$FLEET_CA_ROOT" "$OUT/fleet-ca.crt"
# First SAN is the CN; every SAN (DNS name / IP) hosts might dial goes in --san.
primary=${AGENT_SANS%%,*}
san_args=()
IFS=',' read -ra sans <<<"$AGENT_SANS"
for s in "${sans[@]}"; do san_args+=(--san "$s"); done
echo "==> issuing agent-plane server cert (CN=$primary, SANs=$AGENT_SANS)"
step ca certificate "$primary" "$OUT/server.crt" "$OUT/server.key" \
--ca-url "$FLEET_CA_URL" \
--root "$FLEET_CA_ROOT" \
--provisioner "$STEP_PROVISIONER" \
--provisioner-password-file "$STEP_PROVISIONER_PASSWORD_FILE" \
--not-after "$CERT_NOT_AFTER" \
--force \
"${san_args[@]}"
chmod 600 "$OUT/server.key"
echo
echo "done. staged in $OUT :"
echo " server.crt / server.key -> FLEET_MGR_AGENT_CERT / _KEY"
echo " fleet-ca.crt -> FLEET_MGR_AGENT_CLIENT_CA (verifies host TPM certs)"
echo
echo "next:"
echo " - admin UI cert : Let's Encrypt DNS-01 -> ./pki/admin/{fullchain,privkey}.pem (DEPLOY.md)"
echo " - broker client : ../enrol-broker/admin-pki.sh ../dot1x-bench/pki/broker fleet-manager"
echo " then: cp -r ../dot1x-bench/pki/broker ./pki/broker"