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).