tpm-enrol: pivot host enrolment to a Go binary, built out-of-VM

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.
This commit is contained in:
Audiobuzz
2026-07-02 09:55:54 +00:00
parent 8e1d40a30b
commit 6278f2847d
17 changed files with 1061 additions and 254 deletions
+42 -34
View File
@@ -246,6 +246,19 @@ build {
destination = "/tmp/"
}
# fleet-enrol + fleet-hostapi static binaries, built OUT OF THE VM on the CI
# runner (the "Build host Go binaries" step) so the Rocky build image never
# carries the Go toolchain / vendored go-tpm. Staged here; installed to
# /usr/sbin (enrol, before the initramfs build) and /usr/local/bin (hostapi, 2b).
provisioner "file" {
source = "files/enrol/bin/fleet-enrol"
destination = "/tmp/fleet-enrol"
}
provisioner "file" {
source = "files/hostapi/bin/fleet-hostapi"
destination = "/tmp/fleet-hostapi"
}
# Custom 'fleet' Plymouth theme (scripted security-handshake splash). Staged to
# /tmp/plymouth, installed + set default in the package step before dracut runs.
provisioner "file" {
@@ -277,11 +290,11 @@ build {
# zstd: dracut compresses the initramfs with compress="zstd"
# (90-diskless.conf); the minimal ISO lacks the zstd binary, so without it
# dracut warns and silently falls back to gzip (a larger initramfs).
# tpm2-openssl: the openssl PROVIDER that lets `openssl req -provider tpm2
# -key handle:0x…` sign a CSR with a TPM-resident key (tpm2-tss alone can't).
# Required by the inline empty-NV enrolment hook (tpm-enrol.sh); curl is its
# broker HTTP client. Both get bundled into the initramfs by the dracut module.
"dnf -y install wpa_supplicant tpm2-tools tpm2-tss tpm2-openssl nvme-cli cryptsetup dracut-network NetworkManager zstd openssl curl",
# tpm2-tools: tpm2_nvread reads the client cert from NV on the secure path.
# (First-boot ENROLMENT no longer needs tpm2-tools / an openssl tpm2 provider
# / curl in the image — that's the out-of-VM fleet-enrol Go binary, which
# talks to the TPM directly via go-tpm. So this list stays at the baseline.)
"dnf -y install wpa_supplicant tpm2-tools tpm2-tss nvme-cli cryptsetup dracut-network NetworkManager zstd openssl",
# EFI / Secure Boot management tools (efivar, mokutil, efibootmgr — all base
# repos). Used over SSH on a workstation: `mokutil --sb-state/--pk/--kek/--db`
@@ -377,6 +390,12 @@ build {
# No local bootloader regeneration wanted on kernel updates inside CI
"command -v grub2-mkconfig >/dev/null && echo 'GRUB updates not used on diskless hosts' || true",
# Install the out-of-VM-built enrolment binary BEFORE the initramfs build so
# the dracut module (95dot1x-macsec) can inst it into the initramfs. Non-RPM
# exec => fapolicyd-trusted in 2c-STIG-postfix (like kiosk-session/hostapi).
"install -D -m0755 /tmp/fleet-enrol /usr/sbin/fleet-enrol",
"restorecon /usr/sbin/fleet-enrol 2>/dev/null || true",
# ---- Build the fleet-generic initramfs ------------------------------
# %%{ escapes the rpm query tags so Packer's HCL template engine treats
# them as literals (a bare %{...} is parsed as a template directive).
@@ -397,14 +416,12 @@ build {
# a dracut hook).
"grep -q 'nm-unmanaged.sh' /tmp/lsinitrd.txt || { echo 'ERROR: nm-unmanaged.sh hook missing from initramfs' >&2; exit 1; }",
"grep -q 'nvmf-connect.service' /tmp/lsinitrd.txt || { echo 'ERROR: nvmf-connect.service missing from initramfs' >&2; exit 1; }",
# Assert the inline empty-NV enrolment path is fully present: the hook, the
# TPM key-gen tool, curl, and the openssl tpm2 PROVIDER (the piece tpm2-tss
# doesn't provide — without it the enrolment CSR can't be signed by the TPM
# key). A fresh host silently unable to enrol is a bricked host.
# Assert the inline empty-NV enrolment path is present: the gate hook + the
# out-of-VM fleet-enrol binary (Go + go-tpm; it does key-gen/CSR/NV directly,
# so no tpm2-tools/openssl-provider/curl are needed in the initramfs). A fresh
# host silently unable to enrol is a bricked host.
"grep -q 'tpm-enrol.sh' /tmp/lsinitrd.txt || { echo 'ERROR: tpm-enrol.sh hook missing from initramfs' >&2; exit 1; }",
"grep -q 'tpm2_create' /tmp/lsinitrd.txt || { echo 'ERROR: tpm2 key-gen tools missing from initramfs' >&2; exit 1; }",
"grep -Eq '/curl$|/curl ' /tmp/lsinitrd.txt || { echo 'ERROR: curl missing from initramfs (enrolment broker client)' >&2; exit 1; }",
"grep -q 'ossl-modules/tpm2.so' /tmp/lsinitrd.txt || { echo 'ERROR: openssl tpm2 provider (tpm2.so) missing from initramfs — install tpm2-openssl' >&2; exit 1; }",
"grep -q 'fleet-enrol' /tmp/lsinitrd.txt || { echo 'ERROR: fleet-enrol binary missing from initramfs (out-of-VM build not staged?)' >&2; exit 1; }",
# Plymouth must be in the initramfs to splash during the early boot phase.
"grep -q 'plymouthd' /tmp/lsinitrd.txt || { echo 'ERROR: plymouth missing from initramfs (plymouth dracut module not installed)' >&2; exit 1; }",
@@ -483,29 +500,21 @@ build {
}
# --- 2b-hostapi. Workstation host-info API (Go, loopback, read-only) ------
# Compiles files/hostapi into /usr/local/bin/fleet-hostapi and enables it as a
# systemd service the kiosk SPA reads at http://localhost:9099/v1/host (the
# HOST-API.md contract). Build-time, image-GENERIC facts (image version +
# weston/chromium versions) are written to /etc/fleet-hostinfo.json; everything
# host-specific (kernel, hostname, IP, MAC, board, uptime) is read at RUNTIME,
# so NO per-host state lands in the image (invariant #1). The Go toolchain is
# installed here and removed in 2d hygiene; it never ships.
# Installs the OUT-OF-VM-built /usr/local/bin/fleet-hostapi (the "Build host Go
# binaries" CI step; staged to /tmp/fleet-hostapi) and enables it as a systemd
# service the kiosk SPA reads at http://localhost:9099/v1/host (the HOST-API.md
# contract). Build-time, image-GENERIC facts (image version + weston/chromium
# versions) are written to /etc/fleet-hostinfo.json; everything host-specific
# (kernel, hostname, IP, MAC, board, uptime) is read at RUNTIME, so NO per-host
# state lands in the image (invariant #1). No Go toolchain in the VM anymore —
# the reproducible static build happens on the CI runner (like fleet-enrol).
provisioner "shell" {
environment_vars = [
"IMAGE_VERSION=${var.image_version}",
]
inline = [
"set -euxo pipefail",
"dnf -y install golang",
# Reproducible, dependency-free static build (the verity hash must be a pure
# function of the inputs — invariant #8). GOPROXY=off + GOTOOLCHAIN=local
# guarantee no network/toolchain fetch; -trimpath + -buildid= strip paths
# and the build id. Stdlib-only, so this needs no modules. NOTE: a golang
# package update changes the binary (hence the hash) — a reproducibility-
# sensitive input, like the STIG semanage store; build twice to confirm.
"install -d -m0755 /tmp/gocache",
"env -C /tmp/hostapi CGO_ENABLED=0 GOOS=linux GOARCH=amd64 GOPROXY=off GOTOOLCHAIN=local GOFLAGS=-mod=mod GOCACHE=/tmp/gocache GOPATH=/tmp/gopath go build -trimpath -buildvcs=false -ldflags '-s -w -buildid=' -o /usr/local/bin/fleet-hostapi .",
"chmod 0755 /usr/local/bin/fleet-hostapi",
"install -D -m0755 /tmp/fleet-hostapi /usr/local/bin/fleet-hostapi",
"install -D -m0644 /tmp/hostapi/fleet-hostapi.service /etc/systemd/system/fleet-hostapi.service",
# Build-time facts file (image-generic; same for every host of a release).
# rpm '%%{...}' is doubled so Packer's HCL2 doesn't parse %{ as a directive.
@@ -515,7 +524,7 @@ build {
"chmod 0644 /etc/fleet-hostinfo.json",
"restorecon -R /usr/local/bin/fleet-hostapi /etc/systemd/system/fleet-hostapi.service /etc/fleet-hostinfo.json 2>/dev/null || true",
"systemctl enable fleet-hostapi.service",
"rm -rf /tmp/hostapi /tmp/gocache /tmp/gopath",
"rm -rf /tmp/hostapi /tmp/fleet-hostapi",
"echo '==> fleet-hostapi enabled (localhost:9099/v1/host)'",
]
}
@@ -769,7 +778,7 @@ build {
# custom kiosk files: add them to fapolicyd's trust DB and rebuild it. weston
# /chromium are RPM-installed so already trusted. (See docs/stig-deviations.md.)
"if command -v fapolicyd-cli >/dev/null 2>&1; then",
" for f in /usr/local/bin/kiosk-session /usr/local/bin/fleet-sb-enroll /usr/local/bin/fleet-hostapi; do { [ -f \"$f\" ] && fapolicyd-cli --file add \"$f\"; } || true; done",
" for f in /usr/local/bin/kiosk-session /usr/local/bin/fleet-sb-enroll /usr/local/bin/fleet-hostapi /usr/sbin/fleet-enrol; do { [ -f \"$f\" ] && fapolicyd-cli --file add \"$f\"; } || true; done",
" fapolicyd-cli --update 2>/dev/null || true",
" echo '==> fapolicyd: trusted custom binaries (kiosk-session, fleet-sb-enroll, fleet-hostapi) + rebuilt trust db'",
"fi",
@@ -844,9 +853,8 @@ build {
# already downloaded to the host) — slims the erofs + cuts attack surface.
# Keep auditd (it's part of the hardened runtime). Best-effort.
"dnf remove -y openscap-scanner scap-security-guide openscap-utils 2>/dev/null || true",
# The Go toolchain was only needed to compile fleet-hostapi (2b-hostapi);
# the static binary ships, the compiler does not.
"dnf remove -y golang 2>/dev/null || true",
# (No Go toolchain to remove — fleet-hostapi + fleet-enrol are built OUT OF
# the VM on the CI runner and file-provisioned in as static binaries.)
# The build-time STIG reports must not ship inside the read-only rootfs.
"rm -f /root/stig-report.html /root/stig-arf.xml",
"dnf clean all",