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:
+32
-1
@@ -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`
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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).
|
||||
|
||||
|
||||
Reference in New Issue
Block a user