fleet-agent: on-host TPM-mTLS telemetry client (v1)

Build the on-host agent that pairs with fleet-hostapi and feeds the
fleet-manager control plane (TODO #14 "STILL TO BUILD (a)"). Nothing on the
workstation talked to the manager before this; Monitor + InfluxDB stayed empty.

Scope: telemetry only. The agent reuses the host TPM identity for mTLS (read-only
tpmkey.LoadSigner at 0x81000100 + cert from NV 0x01800100; the private key never
leaves the TPM), opens the long-lived FleetAgent.Session bidi stream, sends Hello
+ periodic HealthSnapshot (load/cpu/temp/mem/tmpfs/nvme/auth/selinux/chrony/kiosk,
probed read-only from /proc+/sys plus hostapi facts), and LOGS but does not act on
the DesiredState/Command it receives. Reconcile actuators (kiosk URL, reboot,
drain) are deferred until the telemetry half is bench-verified.

- files/agent/: new module (go-tpm + grpc), static CGO_ENABLED=0 like enrol/hostapi.
  Copies the enrol tpmkey (+ read-only LoadSigner) and the generated fleet.agent.v1
  proto so it stays self-contained without importing the initramfs enrol module.
- files/agent/simtest/: proves the TPM-signed cert completes a real mTLS client-auth
  handshake (RequireAndVerifyClientCert) against the go-tpm software simulator.
- files/etc/fleet-agent/agent.env: fleet-generic config (manager DNS name, fleet CA
  reused from radius-ca.pem, TPM handle/nvindex, interval). No per-host state.
- fleet-agent.service: hardened unit (tss group for /dev/tpmrm0, INET egress).
- CI builds fleet-agent out-of-VM + runs vet/test + the simulator handshake test.
- Packer: enable_agent flag (default on) + 2b-agent provisioner + fapolicyd trust.
  Post-switch-root only, so no dracut/initramfs changes.
- docs: TODO #14 + fleet-management-design as-built updated.

Requires the host already enrolled (broker approve births the inventory row) —
else the manager returns PermissionDenied, the correct "not enrolled yet" signal.
This commit is contained in:
Audiobuzz
2026-07-05 03:15:13 +00:00
parent 8ab11a74c5
commit d5c833addf
21 changed files with 2679 additions and 21 deletions
+23 -5
View File
@@ -1396,13 +1396,31 @@ the design forks resolved:
+ agent (`:9600`) listeners to the mgmt net; run `server-pki.sh` when the agent
lands; decide `:9500` vs a 443 proxy; digest-pin the images (invariant #12). See
DEPLOY.md "Hardening status".
- **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
- **[~] (a) the on-host agent — TELEMETRY HALF BUILT 2026-07-05 (`files/agent/`).**
The gRPC client that pairs with `fleet-hostapi` and feeds this plane. It reuses
the host **TPM identity** for mTLS (signing key at `0x81000100` via
`tpmkey.LoadSigner` — a read-only copy of the enrol `tpmkey`; cert from NV
`0x01800100`), opens the long-lived `Session` stream, sends `Hello` + periodic
`HealthSnapshot` (load/cpu/temp/mem/tmpfs/nvme/auth/selinux/chrony/kiosk probed
read-only from `/proc`+`/sys` + hostapi facts), and — **telemetry-only v1** —
**receives `DesiredState`/`Command` but only LOGS them** (zero host mutation).
Static `CGO_ENABLED=0` binary built out-of-VM in CI (like enrol/hostapi), staged
by the `2b-agent` provisioner (`enable_agent`, default on), fapolicyd-trusted,
fleet-generic config in `files/etc/fleet-agent/agent.env`. mTLS handshake proven
end-to-end against the go-tpm software simulator (`files/agent/simtest`). No
dracut/initramfs changes (post-switch-root only). **STILL TO BUILD (deferred):**
the **reconcile actuators** — apply kiosk_url, reboot_at/REBOOT, drain — wired to
be received but not acted on; land after the telemetry half is bench-verified.
**BENCH-VERIFY:** on an enrolled booted host, `systemctl status fleet-agent`
active; manager logs `agent connected: <serial>`; SPA Monitor shows the host
online + health; InfluxDB `fleet_health` accrues points. Requires the host
already enrolled (broker approve birthed its inventory row) — else the manager
returns `PermissionDenied` (the correct "not enrolled yet" signal).
- **STILL TO BUILD:** (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.
reconcile via the agent's deferred actuator half (above). Version pin is labelled
"recorded intent" in the Lifecycle UI.
Depends on the step-ca fleet CA (#8) for the mTLS identity.
+13 -6
View File
@@ -41,13 +41,20 @@ The **server-side control plane + admin SPA is built** and supersedes several
- **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**.
- **On-host agent — telemetry half BUILT (2026-07-05, `files/agent/`).** The
gRPC client that feeds this plane. Reuses the host TPM identity for mTLS (read-only
`tpmkey.LoadSigner` at `0x81000100` + cert from NV `0x01800100`; private key never
leaves the TPM), opens `Session`, sends `Hello` + periodic `HealthSnapshot`
(read-only `/proc`+`/sys` probes + `fleet-hostapi` facts). **Telemetry-only:** it
receives `DesiredState`/`Command` but only **logs** them. Static out-of-VM CI
binary, `2b-agent` provisioner (`enable_agent`), fapolicyd-trusted; mTLS proven
against the go-tpm simulator (`files/agent/simtest`).
- **NOT yet built (recorded intent):** (1) the agent's **reconcile actuators** —
apply kiosk_url, reboot_at/REBOOT, drain — deferred until the telemetry half is
bench-verified; and (2) 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.
needs dnsmasq cohort steering + multi-version UKI serving (`docs/TODO.md` #6). The
version pin is labelled "recorded intent" in the Lifecycle UI.
Everything below is the original design context; treat the as-built note as
authoritative where they differ.