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.
Add .gitea/workflows/fleet-manager-image.yaml, modelled on dnsmasq-image.yaml:
buildah bud of serving/fleet-manager (multi-stage SPA+Go→distroless) on a
path filter, with the same branch/tag tagging scheme (semver on tags, main-<sha>
+ latest on main, <branch>-<sha> dev images). Wires it into the shared
conventions:
- gitea-main-ci.yaml paths-ignore: list the new workflow so a fleet-manager
image change doesn't trigger the ~17-min UKI build (serving/** already covers
the dir; the .gitea/ workflow file needed listing).
- registry-gc.yaml IMAGES: add fleet-manager so its dev-branch builds are GC'd.
The dry-run failed with HTTP 403 on the branches API: PACKAGE_TOKEN is a
package-scoped token (registry push/delete) and can't read repo branches. Split
by capability: branch reads now use the automatic Actions token (github.token,
repo read via 'permissions: contents: read'); package list + delete keep
PACKAGE_TOKEN. Also replaced the bare 'curl -f' failures with a get() helper that
reports the HTTP code + a token-scope hint, so an auth problem can't look like
'no results' (which would otherwise skew the reconcile).
Reconciliation workflow (schedule + workflow_dispatch): lists live branches and
every container version across the four images this repo builds (fleet-boot,
fleet-ipxe, fleet-dnsmasq, spdk-nvmf) and deletes dev/branch builds whose branch
no longer exists, via the Gitea Packages API. Not event-driven, so it catches
branches deleted any way and never depends on a 'delete' webhook.
Safety (per request — never touch release tags or latest):
- Only tags ending in the dev commit-sha suffix '-<7 hex>' are ever candidates.
Release/version tags (1.2.3, v1.2.3, spdk v24.09, dnsmasq main-<sha>) and
'latest' never carry that suffix, so they're structurally out of scope; there's
also an explicit 'latest' skip. Verified by simulation across representative
tags incl. spdk's <ver>-<branch>-<sha> form and pre-release semver.
- Keeps any image whose sanitized branch is still live (+ always 'main'); branch
sanitization mirrors the build workflows.
- Fail-safe: aborts before deleting anything if the live-branch list can't be
fetched (0 branches), so an API/auth blip can't trigger mass deletion.
- Conservative matching: a suffix collision only ever causes a KEEP.
DRY RUN by default: workflow_dispatch previews unless dry_run=false; scheduled
runs dry-run until armed via repo variable REGISTRY_GC_ENABLE=true (so a
scheduled dry-run can be eyeballed first). Uses the existing PACKAGE_TOKEN
(write:package). shell: bash (runner defaults to sh).
The out-of-VM Go build step used `set -euo pipefail`, but the self-hosted
Gitea/act runner runs `run:` steps under sh (dash), which rejects
`-o pipefail` ("Illegal option"). Add `shell: bash` to the step. Keeping
`set -e` is load-bearing: without it a failed go build/test in the first
subshell group is masked by the trailing `file` command's success, letting
a broken binary reach the Packer build.
Replaces the shell/openssl-provider host enrolment (a35f2d6) with a static
Go + go-tpm binary, and moves BOTH host Go binaries out of the Packer VM.
fleet-enrol (files/enrol/, Go + go-tpm, no CGO):
- talks to /dev/tpmrm0 DIRECTLY via transport.FromReadWriter(os.File) — no
linuxtpm, so go-tpm is the only dep and there is NO tpm2-tools key-gen /
openssl tpm2 provider / curl in the image (removes the biggest bench risk,
the provider handle: key-ref).
- ensures the ECDSA key @0x81010001, builds the CSR (crypto.Signer over the
TPM key), POSTs CSR+context to the broker, shows the Plymouth fingerprint,
polls /status, writes the leaf cert to NV 0x01800001.
- tpmkey/ is an exported package so the TPM ceremony is tested against the
go-tpm software simulator from a SEPARATE simtest/ module (CGO stays out of
the shipped build). Transaction logic (mock broker) tested in-package.
- deps pinned by go.sum (not vendored): the binary now builds on the CI
runner, which has network — reproducible via pinned Go 1.26.4 + go.sum.
Out-of-VM build (decision): both fleet-enrol AND fleet-hostapi compile in the
CI "Build host Go binaries" step (Go 1.26.4, CGO off, -trimpath) and Packer
FILE-PROVISIONS the static binaries in. Dropped the in-VM `dnf install golang`
+ the 2d removal — the Rocky build image carries no Go toolchain / go-tpm tree.
Wiring:
- tpm-enrol.sh: now a thin GATE (empty-NV check, pre-auth DHCP on en*,
exec /usr/sbin/fleet-enrol, reboot-to-converge). module-setup.sh bundles the
binary (asserted) + reverts the tpm2-openssl/curl installs.
- diskless.pkr.hcl: file provisioners for both binaries; enrol installed to
/usr/sbin before the initramfs build + fapolicyd-trusted; lsinitrd asserts
fleet-enrol; dnf reverted to baseline (no tpm2-openssl/curl).
- gitea-main-ci.yaml: Set up Go + build both binaries + run the simulator
ceremony test, before packer validate.
TESTED here: transaction unit tests + the full TPM ceremony (key-gen -> persist
-> ECDSA sign -> CSR verify -> chunked NV write -> re-enrol) against the go-tpm
simulator. BENCH-VERIFY (real/swtpm, empty owner auth, pre-auth DHCP, Plymouth,
reboot-converge, prod HTTPS) tracked in docs/TODO.md #11.