Merge remote-tracking branch 'origin/main' into tpm-enroll
This commit is contained in:
+166
-7
@@ -645,15 +645,69 @@ must be built around **never restarting the process that holds live connections.
|
||||
- **Planned SPDK restarts only via drain/failover** (multipath fail-over, or a
|
||||
maintenance window with generous `ctrl_loss_tmo` so hosts recover not die). There
|
||||
is **no zero-impact single-node SPDK restart.**
|
||||
- **Implementation gap (current lab):** SPDK runs as a container with
|
||||
`spdk-setup.sh` as entrypoint + the artifact image mounted → effectively
|
||||
**single-version, configured at container start**, so changing versions =
|
||||
container restart = drop all connections. The work here is a **long-lived target
|
||||
+ an RPC-driven control path** ("add release / remove release / drain status")
|
||||
decoupled from container lifecycle, with per-version subsystems.
|
||||
- **[x] Long-lived target + online release control — IMPLEMENTED 2026-06-27.**
|
||||
The old gap (SPDK ran `spdk-setup.sh` as entrypoint, single-version,
|
||||
configured-at-start → changing versions = container restart = drop ALL
|
||||
connections) is closed. Now in `serving/spdk/`:
|
||||
- **`spdk-target.sh`** — persistent entrypoint: starts `nvmf_tgt` ONCE
|
||||
(release-agnostic), creates the TCP transport + discovery listener, then
|
||||
reconciles every staged release. Never restarted to change versions; on
|
||||
(re)start re-adds everything under `./releases/` (a serving-host reboot
|
||||
restores all served versions).
|
||||
- **`spdk-release.sh`** — JSON-RPC control: `add`/`remove`/`list`/
|
||||
`drain-status`/`reconcile`, **one subsystem per version** (distinct NQN, so
|
||||
adding/removing one never disturbs others' connections), **drain-gated
|
||||
removal** (refuses while a version has connected controllers unless
|
||||
`--force`).
|
||||
- **`spdk-ctl.sh`** — host ops: `load`/`unload`/`list`; stages a release's
|
||||
artifacts from its OCI image into `./releases/<ver>/` and drives the live
|
||||
target. Rolling update = `load` new → reboot hosts → `unload` old once
|
||||
drained.
|
||||
- **`spdk-loader.sh`** — DECLARATIVE rolling updates (the "init container that
|
||||
stays up", compose service `spdk-loader`). On `compose up` after an
|
||||
`IMAGE_VERSION` bump it stages + adds the new rootfs to the running target
|
||||
ONLINE, then polls and **auto-reaps** any non-current version once it has
|
||||
drained (0 conns) for a sustained grace window (`REAP_GRACE_SECONDS`, keep >
|
||||
host `ctrl_loss_tmo`). So migration = bump `IMAGE_VERSION` in `.env` +
|
||||
`docker compose up -d` (compose recreates the loader + the http/dnsmasq UKI
|
||||
servers, but NOT the nvmeof target → no dropped connections). Single served
|
||||
UKI ⇒ a 0-conn old version is genuinely orphaned (a returning host fetches
|
||||
the current UKI), so auto-reap is safe; removal is still drain-gated +
|
||||
grace-debounced. The loader + target share the RPC socket on a named volume
|
||||
(`nvmf_tgt -r`); the loader reuses the SPDK image (has python3 + rpc.py).
|
||||
- **No jq dependency:** `spdk-release.sh` parses RPC JSON with **python3** (the
|
||||
SPDK image ships no jq); legacy artifacts (no per-version UUIDs in
|
||||
verity.json) fall back to the fixed UUID pair with a warning.
|
||||
- **Per-version namespace UUIDs** (`scripts/extract-artifacts.sh`): derived
|
||||
deterministically (UUIDv5 of `image_version`), baked into the UKI cmdline +
|
||||
`verity.json` (now also carries `subnqn`); the serving side reads them back
|
||||
from each release's `verity.json`. Required so two versions don't collide on
|
||||
the same `/dev/disk/by-id/nvme-uuid.<uuid>` host symlink.
|
||||
- **Bootstrap unchanged:** `docker compose up` with one version still works —
|
||||
the `/artifacts` image volume is seeded into `./releases/${IMAGE_VERSION}`.
|
||||
- **PARTIALLY BENCH-VERIFIED 2026-06-28:** long-lived target boots a host end
|
||||
to end — UKI served over HTTP → host boots → NVMe/TCP root connects to the
|
||||
persistent `nvmf_tgt` (single version `spdk-permanence-02ffd7d`). Surfaced +
|
||||
fixed several real bugs along the way: bootstrap seed must COPY not symlink
|
||||
(a symlink into the version-pinned `/artifacts` re-pointed a stale release at
|
||||
new data); `spdk-release` parses RPC JSON via **python3** (SPDK image has no
|
||||
jq); legacy `verity.json` (no per-version UUIDs) falls back to the fixed pair;
|
||||
mislabeled-dir guard (dir name vs verity.json `image_version`, case-insensitive);
|
||||
dropped the redundant discovery-listener re-add (daemon logged "Listener
|
||||
already exists"). **CI lowercases `image_version`** (gitea-main-ci vars step) —
|
||||
a mixed-case branch (`SPDK-permanence`) had baked a capital
|
||||
`fleet-SPDK-….efi` the lowercase boot URL couldn't fetch (iPXE "no such
|
||||
file"); booted via a local retag workaround pending a lowercase rebuild.
|
||||
- **STILL BENCH-VERIFY:** (a) `load` a 2nd version while a host is connected to
|
||||
the 1st → 1st's connection survives (no AER/reset on the running host); (b)
|
||||
`unload`/auto-reap drain-gate blocks while CONNS>0 and reaps after grace on a
|
||||
LIVE host; (c) serving-host reboot reconciles all staged versions; (d)
|
||||
per-version UUID build reproducible (two builds, same version → identical
|
||||
`data_ns_uuid`/`hash_ns_uuid` + `root_hash`); (e) the full fix on a clean
|
||||
LOWERCASE CI rebuild (no retag). See `serving/spdk/README.md`.
|
||||
- **Capacity:** each concurrently-served version costs target memory/hugepages +
|
||||
file-backed bdev overhead — plan for N live versions (current + previous +
|
||||
canary) during long rollouts.
|
||||
canary) during long rollouts (raise `HUGEPAGES`).
|
||||
- **Attach gating + discovery:** with several subsystems advertised, the dracut
|
||||
connect + per-subsystem hostNQN allow-list must ensure a vX host attaches **only**
|
||||
vX's namespace (the by-UUID verity ref protects *what mounts*, but gate the
|
||||
@@ -824,6 +878,10 @@ smoke test.
|
||||
`docs/stig-deviations.md` #6 to reflect encrypted offload.
|
||||
(RELP+TLS via `omrelp`/`imrelp` is the alternative if guaranteed delivery is
|
||||
wanted alongside encryption.)
|
||||
- **fleet-hostapi: scope CORS to the kiosk origin.** The host-info agent ships
|
||||
with `FLEET_HOSTAPI_ALLOW_ORIGIN=*` (lab). For prod set it to the exact kiosk
|
||||
origin (default `http://boot.fleet.lab`) via the unit's commented
|
||||
`Environment=` line (or a drop-in). See #12.
|
||||
|
||||
## [ ] 9. (OPTIONAL / nice-to-have) Convert provisioning to Ansible where it pays off
|
||||
|
||||
@@ -992,3 +1050,104 @@ non-destructive TPM capability check (device, key persist, NV roundtrip, openssl
|
||||
TPM2 provider, CSR signed by the TPM key) to de-risk the TPM half before any of
|
||||
this. Expected gap: the image installs `tpm2-tss` but no openssl TPM2 **provider**
|
||||
(`tpm2-openssl`) — add it to the build once the self-test confirms.
|
||||
|
||||
## [~] 12. Workstation host-info API (`fleet-hostapi`) for the kiosk
|
||||
|
||||
**IMPLEMENTED 2026-06-28.** A small **Go, read-only, loopback-only** HTTP agent on
|
||||
the workstation that reports host/OS/hardware facts to the kiosk SPA, per the
|
||||
[HOST-API.md](../HOST-API.md) contract (`GET http://localhost:9099/v1/host`).
|
||||
Source: `files/hostapi/` (stdlib-only, reproducible static build). Built + enabled
|
||||
by provisioner **2b-hostapi**; toolchain removed in 2d; binary fapolicyd-trusted
|
||||
in 2c-STIG-postfix (register #10). Chromium policy grants Private Network Access
|
||||
scoped to the kiosk origin (`files/kiosk/chromium-kiosk-policy.json`).
|
||||
|
||||
- **Per-host safety:** host-specific fields (kernel, hostname, IP, MAC, board,
|
||||
uptime) are read at RUNTIME; only image-generic facts (image version +
|
||||
weston/chromium versions) are baked, to `/etc/fleet-hostinfo.json` (invariant
|
||||
#1 preserved). Deliberately does NOT expose DMI serial (root-only/per-host) or
|
||||
any TPM/cert/identity material.
|
||||
- **Hardening shipped:** loopback bind enforced in code; `DynamicUser`, no caps,
|
||||
`ProtectSystem=strict`, `MemoryDenyWriteExecute`, `SystemCallFilter=@system-service`,
|
||||
`IPAddressDeny=any` (loopback only). Runs unprivileged, reads world-readable
|
||||
files only.
|
||||
|
||||
**Lab posture LEFT AS-IS (decided 2026-06-28) — revisit for prod:**
|
||||
- **CORS `Allow-Origin=*`** → scope to the kiosk origin (`http://boot.fleet.lab`).
|
||||
Consolidated into #8.
|
||||
- **SELinux:** runs in the default service domain (no custom policy module).
|
||||
**BENCH-VERIFY** no AVC denials for `fleet-hostapi` on an enforcing booted host
|
||||
(Wazuh/`fleet_audit` rule 100120 will surface any). If denials appear, decide:
|
||||
write a confined SELinux policy module vs adjust the sandbox.
|
||||
- **No auth** (justified: loopback + read-only + single-user appliance). Option if
|
||||
the threat model tightens: shared token in `kiosk.env` + header check.
|
||||
- **Info sensitivity:** exposes hostname/IP/MAC/board/versions to the kiosk. Drop
|
||||
MAC/IP if that's later deemed too much.
|
||||
- **Reproducibility:** the `golang` toolchain version is a reproducibility-sensitive
|
||||
input (a package bump changes the binary → verity hash); covered by the
|
||||
build-twice gate. Pin the go version only if drift becomes a problem.
|
||||
|
||||
**BENCH-VERIFY (functional):** (a) service starts under enforcing SELinux +
|
||||
fapolicyd (not blocked); (b) the kiosk SPA's System page fetches `/v1/host`
|
||||
successfully through the PNA allowance (preflight + GET); (c) facts render
|
||||
correctly on real hardware (board/CPU/GPU from DMI/DRM, not the virtio build VM).
|
||||
|
||||
## [~] 13. Kiosk GPU compositing + Chromium flag ownership
|
||||
|
||||
**GPU FIX APPLIED 2026-06-28.** The 820 (Intel HD620/i915) ran Chromium at ~150%
|
||||
CPU with periodic crashes: `--disable-gpu` in our `kiosk-session` forced software
|
||||
raster/compositing. Root cause confirmed from the live cmdline. Fixes in
|
||||
`files/kiosk/kiosk-session` + `diskless.pkr.hcl`:
|
||||
- **Runtime GPU probe** (fleet-generic image boots real HW *and* virtio VMs): a
|
||||
real KMS driver (`i915`/`amdgpu`/…) + `/dev/dri/renderD128` → GPU path
|
||||
(`--ignore-gpu-blocklist --enable-gpu-rasterization --enable-zero-copy`, weston
|
||||
GL renderer); else software (`--disable-gpu` + `weston --use-pixman`) so the VMs
|
||||
still render. No per-host state baked.
|
||||
- **kiosk user added to `render`** (renderD128 isn't logind-seat-ACL'd on EL9;
|
||||
`video` only covers card0/KMS for weston).
|
||||
- **Merged the distro wrapper's `--disable-features` into ours** — Chromium takes
|
||||
the LAST `--disable-features` (not the union), so ours was silently dropping the
|
||||
wrapper's Lens/Gemini/Compose disables.
|
||||
|
||||
**BENCH-VERIFY (on the 820):** GPU process alive (`chrome://gpu`), Compositor/
|
||||
ThreadPool threads near-idle + CPU drop, `gt_act_freq_mhz` rises under load, no
|
||||
SELinux AVC for `dri_device_t` from the kiosk domain (Wazuh `fleet_audit` 100120
|
||||
will surface any — if present, add a scoped SELinux allow or confirm the kiosk
|
||||
user's SELinux mapping is unconfined). Confirm VMs still render (software path).
|
||||
|
||||
**DECIDED 2026-06-28 — Option 1 (policy lock), APPLIED.** The distro chromium
|
||||
wrapper (`/usr/bin/chromium-browser`) layers flags as `$CHROMIUM_FLAGS`
|
||||
(`/etc/chromium/chromium.conf` → VA-API enables + AI disables; overridable via
|
||||
`CHROMIUM_USER_FLAGS`) + **hardcoded** `$CHROMIUM_DISTRO_FLAGS`
|
||||
(`--enable-plugins --enable-extensions --enable-user-scripts --enable-printing
|
||||
--enable-sync --auto-ssl-client-auth`) + our `"$@"` (last). The hardcoded layer
|
||||
isn't removable via env/conf — only by bypassing the wrapper. Rather than bypass
|
||||
(Option 2, which would force us to replicate the wrapper's stdio sanitisation +
|
||||
own the VA-API feature list across updates), we **neutralise in managed policy**
|
||||
(policy beats cmdline flags, survives updates): `ExtensionInstallBlocklist=["*"]`
|
||||
added to `files/kiosk/chromium-kiosk-policy.json` (blocks all extensions → also
|
||||
kills the user-scripts surface). `--enable-sync` already locked by `SyncDisabled`;
|
||||
`--auto-ssl-client-auth` is moot (no browser-store client cert; the TPM 802.1X
|
||||
cert isn't one) — `AutoSelectCertificateForUrls` left unset. VA-API enables
|
||||
inherited from `chromium.conf` unchanged. Option 2 (full flag ownership, bypass
|
||||
the wrapper, own the VA-API `--enable-features`:
|
||||
`AllowQt,WaylandLinuxDrmSyncobj,WaylandPerSurfaceScale,WaylandUiScale,AcceleratedVideoDecodeLinuxGL,AcceleratedVideoDecodeLinuxZeroCopyGL,AcceleratedVideoEncoder`)
|
||||
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)
|
||||
|
||||
**DESIGN CAPTURED 2026-06-28 — [docs/fleet-management-design.md](fleet-management-design.md).**
|
||||
Server-authoritative, pull-based, TPM-identified management plane reusing existing
|
||||
pieces: `fleet-hostapi` as the on-host data source, the audit-collector→Wazuh
|
||||
pipeline as telemetry transport, the digest-pinned deployment manifest as version
|
||||
truth, and the SPDK `spdk-loader`/`spdk-release` rolling-update flow (#6) as the
|
||||
redeploy/drain mechanics. Hosts stay stateless (identity from the TPM cert, no
|
||||
baked enrollment); control is pull/reconcile (no inbound surface); convergence to
|
||||
a new version = reboot. **Phased build:** (1) push hostapi snapshots as structured
|
||||
events into Wazuh + fleet dashboard (health/version-drift/offline/usage/alerting);
|
||||
(2) metrics push (OTLP/remote-write) → VictoriaMetrics + Grafana; (3) mTLS
|
||||
desired-state pull (version pin, reboot/drain, kiosk URL) wired to the deployment
|
||||
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.
|
||||
|
||||
@@ -0,0 +1,193 @@
|
||||
# Fleet management plane — design note
|
||||
|
||||
**Status:** DESIGN (not built). Captured 2026-06-28. Build phased — see "Phasing"
|
||||
and `docs/TODO.md` #14.
|
||||
|
||||
Centralised monitoring + maintenance of the diskless kiosk fleet: **health,
|
||||
version, usage, and maintenance actions**. This note is the authoritative design;
|
||||
it deliberately reuses what already exists rather than bolting on a generic MDM,
|
||||
because the base architecture (immutable UKI + NVMe-root, TPM identity, no
|
||||
per-host state, segmented network) makes most off-the-shelf kiosk-management tools
|
||||
a poor fit.
|
||||
|
||||
## Design philosophy (forced by the architecture invariants)
|
||||
|
||||
These are consequences of the CLAUDE.md invariants, not preferences:
|
||||
|
||||
1. **Server-authoritative, hosts are cattle.** The root is immutable erofs +
|
||||
dm-verity on a tmpfs overlay — nothing survives reboot. So **all durable state
|
||||
(history, inventory, desired-state) lives server-side**; the on-host agent is
|
||||
stateless and buffers only in RAM (best-effort).
|
||||
2. **Identity from the TPM, never baked.** No per-host state in any artifact
|
||||
(invariant #1). The agent derives identity at runtime from the **TPM-held
|
||||
cert** (key `0x81010001`, cert NV `0x01800001`) — the same identity used for
|
||||
802.1X EAP-TLS and the NVMe hostnqn. No enrollment token in the image.
|
||||
3. **Pull-based control, no inbound surface.** The boot LAN is locked down and
|
||||
segmented; we do not open an always-on inbound control port on hosts. Hosts
|
||||
**poll** a desired-state endpoint and reconcile. This also fits the immutable +
|
||||
digest-pinned model: *convergence to a new version = reboot*.
|
||||
4. **Reuse the existing buses.** Telemetry rides the already-built
|
||||
`auditd → audit-collector relay → Wazuh` pipeline; version truth is the
|
||||
existing digest-pinned deployment manifest; redeploy/drain mechanics are the
|
||||
SPDK rolling-update flow (TODO #6). The management plane orchestrates these; it
|
||||
does not reinvent them.
|
||||
5. **Minimal, reproducible, sandboxed agent.** Same posture as `fleet-hostapi`
|
||||
(loopback/least-priv, `DynamicUser`, fapolicyd-trusted, reproducible static
|
||||
build).
|
||||
|
||||
## Components
|
||||
|
||||
### 1. Identity — mTLS with the TPM cert
|
||||
|
||||
The agent presents the TPM-held client cert over **mTLS** to the management
|
||||
endpoint; the server authenticates the host by cert CN/SAN, which is the canonical
|
||||
fleet identity (= NVMe hostnqn = 802.1X identity). Reuses the **step-ca fleet CA**
|
||||
(`serving/dot1x-bench/`). No baked secret; the host also pins the server cert.
|
||||
|
||||
- **Secure path:** full mTLS, TPM cert. Strong, no secrets in the image.
|
||||
- **Open/test path (`rd.dot1x=0`):** fall back to dnsmasq fleet-name + MAC as a
|
||||
weak identity. **Lab only** — flagged as insufficient for production.
|
||||
|
||||
Inventory is keyed by **cert serial / hostnqn**, joined to the dnsmasq starship
|
||||
name (`fleet-namer`), MAC, last-seen, running/desired version, and health.
|
||||
|
||||
### 2. On-host agent — extend `fleet-hostapi`
|
||||
|
||||
`fleet-hostapi` (`files/hostapi/`) is already the only fleet-authored component on
|
||||
the hardware and already models os/hardware/net/kiosk facts. Add a **report/push
|
||||
mode** (or a tiny sibling that reads its loopback `/v1/host` and ships it). Keep it
|
||||
stateless; buffer in tmpfs only; identity from the TPM.
|
||||
|
||||
Reported facts:
|
||||
|
||||
- **Health:** CPU/load, GPU act-freq, temps, **tmpfs overlay usage** (the
|
||||
RAM-exhaustion → NVMe-keepalive risk — the single most important kiosk health
|
||||
signal), NVMe-root link state, 802.1X/MACsec `Authorized`, SELinux enforcing,
|
||||
chrony sync, `kiosk.service` active.
|
||||
- **Version:** running image version (`/etc/fleet-image`), rootfs digest, kernel.
|
||||
- **Usage:** kiosk URL, session uptime, **Chromium reload/crash count** (from the
|
||||
`kiosk-session` restart loop — the exact signal behind the GPU-bug diagnosis),
|
||||
input idle/active (liveness only, never content — privacy-aware), display power
|
||||
state.
|
||||
|
||||
Cadence: heartbeat (~30–60 s) + event-driven (crash, auth loss, version change).
|
||||
|
||||
### 3. Transport
|
||||
|
||||
**Phase 1 — ride the Wazuh pipeline.** Emit snapshots as structured JSON events
|
||||
through the existing `audit-collector` relay → `wazuh1`. Add `fleet_health` /
|
||||
`fleet_kiosk` decoders + rules alongside `fleet_audit` (`serving/siem/config/`).
|
||||
Wazuh then provides search, dashboards, and alerting for free, and it's already
|
||||
secured-path-aware.
|
||||
|
||||
**Phase 2 — metrics push** for high-resolution time-series (CPU/GPU/mem trends):
|
||||
**push** via OTLP or Prometheus remote-write to a collector on the mgmt host →
|
||||
VictoriaMetrics/Prometheus. **Push, never scrape** — scraping would require an
|
||||
inbound port on every locked host, against the posture.
|
||||
|
||||
### 4. Control / maintenance — pull-based desired-state reconciliation
|
||||
|
||||
Each host periodically fetches **its** desired state (mTLS, keyed by TPM cert):
|
||||
target version, kiosk URL, `reboot-at`, `drain`. The agent reconciles; because the
|
||||
root is immutable, **convergence = reboot** (a reboot re-PXEs and picks up the new
|
||||
UKI).
|
||||
|
||||
- The **deployment manifest is already the desired-state for versions** (one
|
||||
commit per release, digest-pinned). Extend it to per-host / per-cohort desired
|
||||
state. Rollback = revert the commit (unchanged model).
|
||||
- **Maintenance actions** (reboot, drain, restart kiosk, change URL, request logs)
|
||||
ride the same poll: the server sets the intent; the host obeys on next poll.
|
||||
- This dovetails with the **SPDK rolling update** (TODO #6): "redeploy a cohort" =
|
||||
bump the manifest → `spdk-loader` serves the new version online → the management
|
||||
plane tells those hosts to drain/reboot → it watches `spdk-release list` CONNS
|
||||
fall → the old version auto-reaps. The management plane *orchestrates* this loop.
|
||||
- **No always-on inbound control socket.** Admin SSH (key-only ECDSA) stays purely
|
||||
break-glass for exceptional manual ops.
|
||||
|
||||
### 5. Server side (serving host or a dedicated mgmt node, e.g. alongside `wazuh1`)
|
||||
|
||||
- **Mgmt service** (small Go, mirrors the `fleet-hostapi` style): mTLS endpoint
|
||||
that accepts host reports and serves per-host desired-state; authenticates hosts
|
||||
by TPM cert via the fleet CA.
|
||||
- **Store:** time-series (VictoriaMetrics/Prometheus) for metrics; Wazuh /
|
||||
OpenSearch for events/logs; git or a tiny DB for desired-state + inventory.
|
||||
- **Dashboards:** Grafana (metric trends) + the Wazuh dashboard (events/alerts) +
|
||||
a small fleet-overview board (offline hosts, version drift, crash rates, tmpfs
|
||||
pressure). Could be a sibling of the `serving/kiosk-app` SPA.
|
||||
- **Alerting:** host offline > N min; Chromium crash-loop; **running ≠ desired
|
||||
version**; tmpfs near-full; MACsec/auth down; NVMe-root reconnect storms.
|
||||
|
||||
## Data model (sketch)
|
||||
|
||||
```
|
||||
Host (by cert-serial / hostnqn)
|
||||
├─ identity: cert serial, hostnqn, MAC, starship-name (dnsmasq)
|
||||
├─ version: image_version, rootfs digest, kernel, desired_version
|
||||
├─ health: cpu/load, gpu_freq, temps, tmpfs_used%, nvme_root, auth/macsec,
|
||||
│ selinux, chrony, kiosk_active, last_seen
|
||||
├─ usage: kiosk_url, session_uptime, chromium_crashes, input_active, display
|
||||
└─ desired: target_version, kiosk_url, reboot_at, drain (server-authoritative)
|
||||
```
|
||||
|
||||
## Mapping to existing building blocks
|
||||
|
||||
| Need | Reuse |
|
||||
|---|---|
|
||||
| Host identity | TPM cert / hostnqn / step-ca fleet CA (`serving/dot1x-bench/`) |
|
||||
| On-host data | `fleet-hostapi` `/v1/host` (`files/hostapi/`) |
|
||||
| Telemetry + alerting | audit-collector relay → Wazuh (`serving/siem/`) |
|
||||
| Friendly names / inventory | dnsmasq `fleet-namer` (MAC → name → DNS) |
|
||||
| Version truth + rollback | digest-pinned deployment manifest (git) |
|
||||
| Redeploy / drain mechanics | SPDK `spdk-loader` + `spdk-release` (TODO #6) |
|
||||
| Build / sign / publish | Gitea CI + OCI images |
|
||||
|
||||
## Phasing
|
||||
|
||||
1. **Phase 1 (mostly-built):** push `fleet-hostapi` snapshots as structured events
|
||||
into Wazuh; add decoders/rules + a fleet dashboard → health, version-drift,
|
||||
offline detection, usage, alerting. Reuses the existing pipeline end to end.
|
||||
2. **Phase 2:** metrics push (OTLP / remote-write) → VictoriaMetrics + Grafana for
|
||||
trend graphs.
|
||||
3. **Phase 3:** mTLS desired-state pull (version pin, reboot/drain, kiosk URL),
|
||||
wired to the deployment manifest + SPDK rolling-update flow.
|
||||
|
||||
## Build vs buy
|
||||
|
||||
Off-the-shelf fits poorly because the base is unusual:
|
||||
|
||||
- **FleetDM + osquery** — closest for the query/health side; worth evaluating. But
|
||||
osquery enrollment carries per-host state, so it must be run carefully (identity
|
||||
from TPM, enrollment in tmpfs only) or it breaks invariant #1.
|
||||
- **Mender / balena / typical kiosk MDM** — assume mutable disk + their own OTA +
|
||||
enrollment. We already have a *better* version mechanism (UKI + NVMe-root +
|
||||
digest manifest), so their OTA is redundant and their disk model conflicts.
|
||||
|
||||
**Decision: build the thin mgmt service + reconcile loop; reuse Wazuh/Grafana for
|
||||
the heavy lifting.** The hard parts (identity, immutable versioning, a telemetry
|
||||
bus) already exist.
|
||||
|
||||
## Security posture
|
||||
|
||||
- mTLS + TPM cert = strong identity, zero baked secrets, reuses the 802.1X CA.
|
||||
- Pull-based control = **no new inbound attack surface** on locked hosts.
|
||||
- Telemetry stays on the mgmt/boot LAN, TLS'd (same gap as the Wazuh-relay-TLS
|
||||
TODO) — no internet egress, air-gap intact.
|
||||
- Agent stays minimal/sandboxed/fapolicyd-trusted/reproducible (`fleet-hostapi`
|
||||
template); usage capture is liveness-only, never content (privacy).
|
||||
- Fleet-wide inventory is recon-valuable → protect the management plane (auth +
|
||||
segmentation); never expose its dashboards to the boot LAN.
|
||||
|
||||
## Open questions / decisions for build time
|
||||
|
||||
- **Telemetry encoding:** structured syslog/JSON into Wazuh (Phase 1) vs native
|
||||
OTLP from the start. Leaning Wazuh-first for reuse.
|
||||
- **Metrics store:** VictoriaMetrics vs Prometheus (VM is lighter for push +
|
||||
long retention).
|
||||
- **Desired-state store:** git (GitOps, aud/rollback for free, matches the
|
||||
deployment-manifest model) vs a small DB (richer per-host scheduling).
|
||||
- **Per-host vs per-cohort** desired state granularity (cohorts likely enough;
|
||||
per-host for canaries).
|
||||
- **Open-path identity:** MAC/fleet-name is weak — gate the management plane to the
|
||||
secure path, or accept lab-only for the open path.
|
||||
- **osquery/FleetDM** evaluation for the read side before building a bespoke
|
||||
collector.
|
||||
@@ -143,10 +143,13 @@ hotkey disabled (e.g. a physically exposed terminal), re-select both rule IDs.
|
||||
- **SELinux enforcing** (`selinux --enforcing`; labels preserved into the erofs).
|
||||
- **fapolicyd** kept **enabled** (NOT waived) — it's the real exec-allowlist guard
|
||||
for the writable RAM overlay, which dm-verity does **not** cover. The only
|
||||
adjustment: the one non-RPM executable, `/usr/local/bin/kiosk-session`, is added
|
||||
to fapolicyd's trust DB at build (provisioner 2c-STIG-postfix) so the kiosk runs;
|
||||
everything else stays RPM-trust-only. (fapolicyd blocking that binary was the
|
||||
original kiosk-won't-start cause.)
|
||||
adjustment: the few non-RPM executables we ship are added to fapolicyd's trust DB
|
||||
at build (provisioner 2c-STIG-postfix) so they run; everything else stays
|
||||
RPM-trust-only. Trusted custom binaries: `/usr/local/bin/kiosk-session`,
|
||||
`/usr/local/bin/fleet-sb-enroll`, `/usr/local/bin/fleet-hostapi` (the loopback
|
||||
host-info agent, built from `files/hostapi`). Each is built into the immutable,
|
||||
dm-verity-protected root, so trusting it doesn't weaken the overlay exec guard.
|
||||
(fapolicyd blocking `kiosk-session` was the original kiosk-won't-start cause.)
|
||||
- **auditd** enabled; STIG audit rules applied; logs locally.
|
||||
- **USBGuard** enabled + enforcing (`block` target) with a HID/hub allowlist and
|
||||
`AuditBackend=LinuxAudit` (see #12) — blocks USB mass storage / unknown device
|
||||
|
||||
Reference in New Issue
Block a user