Commit Graph
92 Commits
Author SHA1 Message Date
Audiobuzz 4b99816394 Merge in main 2026-07-10 07:10:40 +09:30
audiobuzz 08f2d82bd1 Merge pull request 'optimise-ram' (#11) from optimise-ram into main
Reviewed-on: https://git.bitasylum.org/audiobuzz/diskless-workstation/pulls/11
2026-07-05 06:46:52 +00:00
Audiobuzz 254ec79cae fleet-agent: install agent.env from /tmp/agent (complete staging fix)
Companion to the previous commit's file move: point the 2b-agent install at
/tmp/agent/agent.env (the agent's own upload) instead of the deleted
/tmp/etc-stage path, update the file-provisioner comment, the enable_agent var
description, and the TODO source-path reference. packer validate passes.
2026-07-05 06:36:54 +00:00
Audiobuzz d5c833addf 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.
2026-07-05 03:15:13 +00:00
Audiobuzz 3140d51d95 fleet-manager: document hardening status + remaining per-env items
DEPLOY.md gains a "Hardening status" section (done vs operator-to-close), and
TODO #14 records the deployment as CI-built + acme-cert + backup, with the open
items called out: network-segment the :9500/:9600 listeners to the mgmt net, run
server-pki.sh when the agent lands, choose :9500 vs a 443 proxy, and digest-pin
the images.
2026-07-05 00:36:01 +00:00
Audiobuzz 2ef0c1747d 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.
2026-07-04 08:44:31 +00:00
Audiobuzz e27f068aaf docs/stig-deviations #15: fapolicyd overlay watch VERIFIED enforcing (2026-07-04)
Bench pass on fapolicyd-tmpfs-12043d3 / comet-beacon-54: permissive=0, watch_fs has
overlay, / and /tmp both overlayfs; novel-hash /tmp binary DENIED (FANOTIFY resp=2,
obj_trust=0, exit=126); empty deny sweep = zero false denials (kiosk/weston/chromium/
NM-dispatcher all trusted, admin SSH+sudo work under staff_t). The exec guard went
from effectively OFF to enforcing on the whole overlay root, nothing legit broken.

Also records the scope caveat: integrity=none path-trust closes the UNTRUSTED-PATH
half (proven) but not the MODIFIED-TRUSTED-PATH half — an overlayfs copy-up of a
trusted path (e.g. /usr/bin/bash) lands a same-path tmpfs copy outside dm-verity that
path-trust still trusts. Closing it needs integrity=sha256 or an un-shadowable /usr
(TODO #15, preferred).
2026-07-04 02:52:04 +00:00
Audiobuzz 18aad0888c Merge origin/main (trim-rpm) into optimise-ram
Combines the image trim (#18: langpack/firmware slim, kiosk weak-deps, nss-mdns
KEEP+mask-avahi fix, final kiosk-stack build guard) with optimise-ram's overlay-RAM
work (#17 fapolicyd db_max_size cap, .pyc unchecked-hash recompile).

Conflict resolutions:
- diskless.pkr.hcl (2d hygiene tail): kept BOTH additions — the .pyc recompile
  runs first (the 'must run last after every dnf' mutation), then the read-only
  FINAL GUARD (rpm -q kiosk stack) as the absolute last step. The nss-mdns fix
  (mask avahi, keep the package) auto-merged above the conflict — no regression.
- docs/TODO.md: kept both #17 (fapolicyd db_max_size) and #18 (image trim), as the
  #18 stack-note predicted.
2026-07-04 02:49:55 +00:00
Audiobuzz bdc7e6b234 Merge remote-tracking branch 'origin/main' into fapolicyd-tmpfs 2026-07-04 12:11:05 +09:30
Audiobuzz 68dee79832 docs/TODO #18: record nss-mdns/chromium regression + fix; kiosk re-verified, image 1.8 GiB → 755 MiB
The '#18 render should be safe' call on the nss-mdns drop (5cc72cc) was wrong: it
bricked the kiosk on trim-rpm-48dc3e1 (chromium hard-Requires nss-mdns, so the 2d
remove cascaded chromium + its GUI stack out → 500 MB browserless image, flashing
cursor). Fixed in 2bc2daf (keep nss-mdns, mask avahi daemon; final build guard
asserts the kiosk stack survived). Re-verified booting into the kiosk on the 820;
container image measured 1.8 GiB → 755 MiB (~58%).
2026-07-04 02:24:57 +00:00
Audiobuzz de57a7de11 Merge branch 'main' into optimise-ram 2026-07-03 21:53:18 +09:30
Audiobuzz 12043d3444 stig: add overlay to fapolicyd watch_fs — actually guard the overlay root
Bench-verified the getattr allow (620a604) alone did NOT close the exec gap: with
the fapolicyd_tmpfs module installed (AVC gone, fapolicyd active, permissive=0), a
NOVEL non-RPM binary in /tmp still ran. Root cause: the diskless root is overlayfs
(stat -f / -> overlayfs), but the shipped watch_fs is
ext2,ext3,ext4,tmpfs,xfs,vfat,iso9660,btrfs — no 'overlay'. fapolicyd marks mounts
by fs type, so it never marked the overlay and intercepted no exec on / at all.
The exec guard we keep fapolicyd for was effectively OFF at runtime on every
diskless host.

Fix: the 2c fapolicyd step now appends 'overlay' to watch_fs (idempotent) so
fapolicyd watches the overlay root. Trust is path-based (integrity=none) so overlay
paths resolve against the rpmdb trust normally; the fapolicyd_tmpfs .te still
supports fapolicyd statfs'ing the overlay's tmpfs upper. Deviation #15 updated.

This turns app-allowlisting ON for the whole root at runtime for the first time.
BENCH-VERIFY HARD: boot + kiosk render + admin SSH + sudo + TPM enrolment all still
work (nothing legit untrusted), a novel /tmp binary is now BLOCKED (EPERM), no new
AVCs. enable_stig=false is the escape hatch if it regresses.
2026-07-03 11:56:13 +00:00
Audiobuzz 48dc3e11ae Merge remote-tracking branch 'origin/main' into trim-rpm 2026-07-03 20:14:51 +09:30
Audiobuzz f882ddae1f docs/TODO #6: targeted-connect test-path bench-verified (0bfe872 mounts one image)
0bfe872 builds, deploys, and the host now mounts ONLY its own version's image
instead of connect-all'ing every served version — the targeted 'nvme connect -n
rd.nvmf.nqn' works. Each host = 1 controller on its own subsystem, which is what
lets old versions drain to 0 and spdk-loader auto-reap. Still to confirm: the reap
payoff (migrate a host, watch its old version drain + reap), and the secure path's
new step-6 connect over macsec0.
2026-07-03 10:43:24 +00:00
Audiobuzz 620a60401f stig: ship fapolicyd_tmpfs SELinux module — close overlay exec-guard gap
fapolicyd is the app-allowlisting exec guard for the writable RAM overlay
(dm-verity covers only the read-only erofs lower). It statfs's mounts to decide
which to watch, and tmpfs is in its default watch_fs. This image's root is
systemd.volatile=overlay, so statfs("/") returns the overlay upper — a tmpfs —
and the shipped policy has no 'fapolicyd_t tmpfs_t:filesystem getattr' (no
ordinary host roots on tmpfs). Under enforcing the getattr is DENIED (audit:
dev=tmpfs ino=1 name="/", permissive=0, seen 2026-07-03 on comet-beacon-54),
so fapolicyd can't fully mark the tmpfs mounts (/, /tmp, /dev/shm) and its guard
over the writable overlay is incomplete — a real exec-guard gap, not just SIEM
noise.

Ship a minimal local allow module (allow, not dontaudit — we want fapolicyd to
see the tmpfs), compiled + semodule -i'd in 2c-STIG right after the chrony one.
Registered as deviation #15.

BENCH-VERIFY: the AVC is gone on an enforcing boot AND fapolicyd blocks a non-RPM
executable dropped in /tmp (proves the guard now covers the overlay), trusted
kiosk/appliance binaries still run, and the verity hash is reproducible across
two builds (semodule rebuilds the policy store).
2026-07-03 07:18:01 +00:00
Audiobuzz 8e105c8bf1 docs/TODO #18: kiosk render bench-verified on VM + 820 (i915 firmware confirmed)
f59eb79 now fully bench-verified: the fleet SPA renders on both the VM
(virtio-gpu) and the 820 (i915), so the i915 DMC/GuC firmware loads on real
hardware and the firmware keep-list is validated end-to-end. The two later
commits (nss-mdns drop, omit_drivers + keep-list tightening) don't touch i915,
so render should be safe, but still need their own build+boot.
2026-07-03 06:58:52 +00:00
Audiobuzz bf0c61a025 docs/TODO #18: record f59eb79 built + booted successfully
The ~800 MB trim (glibc langpacks en-only + linux-firmware strip to the i915
keep-list, run pre-dracut) builds and boots — the boot/root path survives it
(firmware 678->147 MB, initramfs bundles only the keep-list). Notes still-open
checks: kiosk RENDER on real i915 hardware ('booted' != 'display up'), and that
the two later commits (nss-mdns drop 5cc72cc; omit_drivers + keep-list tightening
853ae20) are NOT in f59eb79 and need separate bench-verify.
2026-07-03 06:55:52 +00:00
Audiobuzz 853ae20e68 trim: omit unused GPU drivers + tighten firmware keep-list (TODO #18)
First trim build confirmed firmware 678 MB -> 147 MB, but the initramfs came
out 97 MB and is now kernel-MODULE-bound, not firmware: --no-hostonly +
plymouth's drm dep instmods every GPU driver (amdgpu.ko etc.) though the fleet
is i915-only.

- omit_drivers += amdgpu radeon nouveau (90-diskless.conf): drop the discrete-GPU
  kmods (amdgpu.ko is the biggest single module in the initramfs); keep i915 +
  virtio-gpu/bochs for the splash. Shrinks the initramfs/UKI pulled over PXE on
  every boot.
- Firmware keep-list tightened: drop mellanox/ (mlxsw_spectrum SWITCH-ASIC blobs,
  not NIC fw — mlx5 NIC fw is on-card) and narrow intel/ to just ice (drop
  ish/sof/ibt sensor-hub/audio/bluetooth, none loaded at boot per the audit).
- Add an 'initramfs largest files' diagnostic (sort lsinitrd.txt by size) so the
  next build shows what dominates the .img.

BENCH-VERIFY: kiosk still renders on the 820 (i915 kept); watch the new initramfs
largest-files log line for the post-omit size.
2026-07-03 06:36:32 +00:00
Audiobuzz 0bfe872828 nvmf: targeted per-version connect (rd.nvmf.nqn) instead of connect-all
Hosts connected with 'nvme connect-all', which attaches EVERY subsystem in the
discovery log. Each host then mounts only its own by-UUID verity namespaces but
leaves a live controller on every other served version's subsystem. Observed via
spdk-release list: more controllers than workstations, each box on every version
(1/3/3/1/3 across 5 versions for ~3 boxes). Beyond wasted connections
(O(hosts x versions)), it defeated the rolling-update drain/reap: no version ever
reached 0 connections while any host was up, so spdk-loader's auto-reap could
never fire and old versions accumulated without bound.

Fix — targeted connect to this build's subsystem only:
- extract-artifacts.sh bakes the per-version SUBNQN (already in verity.json
  .subnqn, matched by the serving side) into the signed UKI cmdline as
  rd.nvmf.nqn=. NQN is version identity, not per-host state, and the cmdline is
  already per-version + immutable-per-signed-UKI, so invariant #1 holds.
- nvmf-connect.sh (test path) uses 'nvme connect -n <nqn>'; legacy UKIs without
  rd.nvmf.nqn fall back to connect-all with a warning so they still boot.
- dot1x-macsec-up.sh (secure path) gains an explicit step-6 targeted connect over
  macsec0 (after it's addressed, per invariant #2), replacing the delegation to
  the dracut nvmf module's discovery connect-all.

Now each host = 1 controller on its own subsystem; versions drain to 0 when their
hosts migrate, so auto-reap works and connections scale O(hosts). Requires a
rebuild + re-sign (cmdline change, invariant #6). BENCH-VERIFY both paths;
confirm the dracut nvmf module isn't also connect-all-ing on the secure path.
CLAUDE.md invariant #2 + TODO #6 updated.
2026-07-03 06:14:08 +00:00
Audiobuzz f59eb7942a trim: run firmware strip pre-dracut so the UKI initramfs shrinks too
Follow-up to the linux-firmware strip (prev commit put it in the 2d hygiene
block, which runs AFTER the dracut build — so it only shrank the rootfs erofs,
not the initramfs). Move the strip to step 2, before the dracut build: with
--no-hostonly, plymouth's drm dep instmods a broad DRM set and dracut bundles
each module's firmware into the initramfs (amdgpu being the classic UKI
bloater). Trimming first means dracut never sees it, so both the rootfs erofs
AND the UKI initramfs shrink. Keep-list is unchanged (i915 + intel/ + mellanox
+ intel-ucode), so the fleet splash GPU (i915) and ice DDP firmware the
initramfs actually needs are retained; dracut just warns 'firmware not found'
for the removed drm modules and continues.

Add a post-build diagnostic (initramfs .img size + bundled firmware, expected
i915 only) so the initrd win is measured in the build log, not estimated.
2026-07-03 05:36:51 +00:00
Audiobuzz 9464e7fc2a trim: slim image ~800 MB — glibc langpacks en-only + linux-firmware strip (TODO #18)
The 2b rpm-size manifest revealed the biggest packages aren't the kiosk but
base bloat: linux-firmware 678 MB and glibc-all-langpacks 228 MB (vs chromium
370 + llvm 129). The uncompressed erofs is read over NVMe/TCP on every boot,
so this is fleet-wide dead weight + attack surface.

glibc: swap glibc-all-langpacks (every language) for glibc-minimal-langpack +
glibc-langpack-en. ~220 MB; image is English-only.

linux-firmware: a live fleet audit (modinfo -F firmware over loaded modules +
sysfs/lspci driver bindings on the real 820-class hosts and the VMs) proved the
only firmware any host loads is Intel i915 (GPU). Every NIC is firmware-free
(e1000e reports on-chip NVM, not a fw file; virtio_net loads nothing), so the
catastrophic missing-NIC-firmware case is off the table. Keep i915 + intel/
(ice DDP insurance) + mellanox + intel-ucode; strip the ~600 MB of foreign
vendor blobs (amdgpu/nvidia/radeon/ath/qcom/...). Remove iwl*-firmware (Wi-Fi,
~83 MB) on a wired-only fleet. Strip-in-place since the package can't be
removed without losing i915; du reports the result in the build log.

BENCH-VERIFY: kiosk still renders on the 820 (i915 DMC/GuC load) — the one real
risk; widen the keep-list if any host has a non-Intel GPU or an ice/mlx5 NIC.
2026-07-03 05:23:49 +00:00
Audiobuzz db0bb28ebd docs/TODO #11: Dell 820 TPM enrolment bench-verified on real hardware
Test-path enrolment (rd.dot1x=0) succeeded end-to-end on the Dell 820 — the
hard case that previously found the un-clearable OEM platform NV at 0x01800001
and silently bypassed enrolment every boot. Confirms BOTH the NV-index move
(0x01800001 -> 0x01800100) and the signing-key handle move (EK slot -> 0x81000100)
on a physical TPM, not just the Proxmox swtpm: ensure ECDSA key -> CSR -> broker
fingerprint -> approve -> leaf cert -> NV write all run on the metal.

Mark #11 in-progress ([~]). Still to verify: the enrolled cert authenticating
EAP-TLS on the secure 802.1X/MACsec path (this run was test-mode), and
tpm2_nvreadpublic on other fleet hardware models to confirm 0x01800100 is free
fleet-wide.
2026-07-03 04:55:08 +00:00
Audiobuzz 796dfe9f4e reduce overlay RAM + kill .pyc AVC churn (TODO #16 item2, #17)
Two fixes from the booted-host overlay RAM audit (2026-07-03). The writable
root overlay (systemd.volatile=overlay, tmpfs) held 124.5 MiB of runtime
writes; almost all of it is runtime-regenerated state, not real fs deltas.

1. fapolicyd db_max_size 100 -> 50 (TODO #17). /var/lib/fapolicyd/data.mdb was
   100 MiB fully allocated (~80% of the overlay). fapolicyd's LMDB trust DB is
   rebuilt at boot onto the overlay, so db_max_size is a fixed RAM reservation.
   Sed it to 50 before the fapolicyd-cli --update rebuild. 50 was fapolicyd's
   long-time upstream default (covered a full workstation rpmdb), so it has
   headroom for this smaller image. Reclaims ~50 MiB.
   BENCH-VERIFY (STIG-critical exec guard): boot + kiosk + admin SSH + sudo +
   custom-binary exec must work (undersize -> MDB_MAP_FULL bricks exec).

2. .pyc invalidation fix (TODO #16 item 2). erofs -T 0 zeroes mtimes, so
   Python thinks every baked .pyc is stale and rewrites it into the overlay at
   runtime (wasting RAM + throwing lib_t write/unlink AVCs that flood the SIEM).
   Observed trigger: dnf-makecache.timer (~10 min post-boot) re-importing the
   dnf Python stack. Fix both ends: (a) compileall --invalidation-mode
   unchecked-hash over /usr/lib*/python3.* at the end of 2d (mtime-independent
   .pyc, no rewrite, also more reproducible); (b) mask dnf-makecache.timer next
   to the existing rhsmcertd mask (removes the trigger). Firewalld enablement
   (#16 items 1 & 3) still open.
2026-07-03 04:03:36 +00:00
Audiobuzz c0a08438b2 kiosk: disable weak deps + add package-footprint audit
The kiosk dnf transaction was measured at 229 pkgs / 1.0 GB. Nearly all
of the non-core packages are Recommends: a locked-down single-URL kiosk
never uses flatpak + its ostree/bubblewrap sandbox runtime, tracker-miners
(a filesystem indexer — dead weight and a threat surface), the pipewire/
pulseaudio audio stack, or libproxy-webkitgtk4 -> webkit2gtk3-jsc (an 8.9M
second browser engine dragged into a Chromium kiosk for libproxy's PAC
runner).

Add --setopt=install_weak_deps=False to cut them, and an rpm -qa size
manifest to the build log so the next CI build reports the exact new
footprint (turning 'seems unnecessary' into data for the next trim round:
gstreamer/media codecs, or mesa-dri-drivers if the GPU path is abandoned).

The immovable floor is chromium + chromium-common + llvm-libs (mesa sw-GL)
+ gtk3/adwaita + libicu, all hard deps. BENCH-VERIFY the kiosk still
renders with weak deps off (watch for a missing font or libva).
2026-07-03 03:27:58 +00:00
Audiobuzz 6fe623c727 tpm-enrol: move NV cert index off OEM-occupied 0x01800001 → 0x01800100
The Dell 820 kept "finding a cert" and bypassing enrolment despite repeated BIOS
TPM clears. tpm2_nvreadpublic revealed 0x01800001 on that hardware is a FIRMWARE
platform index (platformcreate|policydelete|writelocked, 70 bytes — not our
cert; siblings at 0x01800003/4 + 0x01810000). platformcreate survives TPM2_Clear
and policydelete needs the platform's secret policy, so it's permanent and
unusable by us — the blind check false-positived on it, and enrolment could
never write there anyway. Same class of bug as the EK-handle collision.

Move the NV cert index default 0x01800001 -> 0x01800100 (free on the 820, owner
range, overridable via rd.dot1x.nvindex): cmdline (extract-artifacts.sh),
fleet-enrol -nvindex, the dracut hooks, bench scripts, CLAUDE.md invariant #1,
and design/SSP docs. The signing-key handle (0x81000100) is unchanged.

Harden WriteNVCert: refuse a platformcreate index with a clear "pick a free
owner index via rd.dot1x.nvindex" error instead of a confusing undefine
failure. Regression simtest/TestWriteNVCertRefusesPlatformIndex; NV roundtrip +
ReadNVCert tests moved to 0x01800100. All go tests + packer validate pass.
2026-07-03 02:51:38 +00:00
Audiobuzz 33de300af8 docs/TODO: add #16 — enable a locked-down firewalld (defense-in-depth + STIG passes)
Captures the decision to re-enable a CONFIGURED firewalld (currently removed),
the required .pyc/-T0 root-cause fix (compileall --unchecked-hash), the 4 STIG
waivers it converts, and the load-bearing NVMe/TCP-survives-firewalld bench gate.
2026-07-03 01:31:03 +00:00
Audiobuzz 835ae7bc24 stig: ship SELinux allow module for chrony DHCP-NTP dispatcher (stop chronyd AVC)
The NM dispatcher 10-chrony-dhcp-ntp runs `chronyc reload sources` to load DHCP
option-42 NTP servers. That chronyc runs in the confined custom-dispatcher
domain (NetworkManager_dispatcher_custom_t), so its reply socket carries that
label and the shipped policy has no rule for chronyd_t to sendto it. Under
enforcing, chronyd does the reload but its ack is denied — chronyc reports
failure and the AVC floods the SIEM (seen 2026-07-03; permissive=1 only because
the box was booted enforcing=0).

Ship a minimal local allow module (files/stig/chrony-dhcp-dispatcher.te):
  allow chronyd_t NetworkManager_dispatcher_custom_t:unix_dgram_socket sendto;
Built + semodule -i'd in the 2c-STIG provisioner (checkpolicy installed if
absent), with an install assertion. `allow` not `dontaudit` — it also lets the
reload ack succeed so chronyc returns cleanly. Documented as
docs/stig-deviations.md #14 (not a rule waiver — a local functional allow).

BENCH-VERIFY: enforcing boot shows no AVC, chronyc reload returns 0, a
DHCP-handed NTP server appears in `chronyc sources`; and re-check verity-hash
reproducibility (semodule rebuilds the policy store, like the semanage step).
2026-07-03 01:16:26 +00:00
Audiobuzz 0e139f5346 serving: make the SPDK nvmeof target version-agnostic (stop recreating it on IMAGE_VERSION bump)
The nvmeof (spdk-target) service carried a versioned bootstrap mount
(type: image, source ${OCI_IMAGE}:${IMAGE_VERSION} -> /artifacts) plus an
IMAGE_VERSION env. Bumping IMAGE_VERSION therefore changed the service config,
so `docker compose up -d` recreated the container -> nvmf_tgt restarted ->
every connected host's NVMe/TCP root dropped at once. That is precisely the
fleet-wide outage the long-lived-target design exists to prevent.

Remove the /artifacts image-volume and IMAGE_VERSION env from nvmeof so its
config no longer depends on the version — compose leaves it running across a
bump. Bootstrap and rolling updates are already fully covered by spdk-loader
(stages the current release into ./spdk/releases and adds it online via
JSON-RPC); spdk-target.sh's seed block is guarded, so it just no-ops when
/artifacts isn't mounted. Verified nvmeof no longer references IMAGE_VERSION
(only spdk-loader does); compose YAML validates.

BENCH-VERIFY: bump IMAGE_VERSION + up -d with a host connected -> nvmeof
container not recreated, host root survives, loader adds the new version.
2026-07-03 01:07:16 +00:00
Audiobuzz 86a9839cc4 fleet-enrol: content-aware NV check (-check-only), not blind "index defined"
The enrol trigger keyed on "is NV 0x01800001 defined/readable", so leftover
state (an earlier tpm-provision run — bench-seen on the 820: "cert present —
normal boot" with no real identity) or random bytes (tpm-selftest) silently
suppressed enrolment. Now content-aware:

- tpmkey.ReadNVCert reads the full NV contents (chunked).
- classifyNVCert (TPM-agnostic, unit-tested): parses X.509, checks validity
  window, and when a real fleet CA bundle is present verifies the cert chains
  to it. Only a valid fleet cert = enrolled.
- New `fleet-enrol -check-only` (no network/no key-gen) exits 0=valid cert,
  20=enrol needed, 1=error. The full enrol path uses the same classification
  instead of NVPresent.
- The dracut hook calls -check-only as the single gate, replacing the blind
  tpm2_nvread (and its now-dead TPM2TOOLS_TCTI export). tpm2_nvread stays in
  the initramfs for the secure path (dot1x-macsec-up.sh).

Lab fallback: the shipped radius-ca.pem is a placeholder (not a cert) ->
loadCAPool returns nil -> chain-less "parseable + unexpired" acceptance, so
reboot-converge doesn't loop; production replaces radius-ca.pem with the real
fleet CA for strict chaining.

Tests: classifyNVCert table (valid/expired/garbage/foreign/no-CA) + simtest
ReadNVCert roundtrip across the 512B chunk. All pass; packer validate clean.
2026-07-03 01:02:25 +00:00
Audiobuzz 4d6f038e3c tpm-enrol: move signing key off the EK slot 0x81010001 → 0x81000100
Bench (Proxmox vTPM, host cygnus-cruiser-99) confirmed via tpm2_readpublic that
0x81010001 — the handle our design used for the fleet signing key — is the
TCG-standard RSA Endorsement Key slot: the object there is the swtpm-provisioned
EK (restricted|decrypt|adminwithpolicy, RSA-2048/AES128CFB, canonical EK
authPolicy 837197…, EK cert at NV 0x1c00002). Most firmware/swtpm TPMs occupy
that slot, so EnsureKey could never create our key there — it errored reading
the EK as ECC and enrolment died before the CSR/POST.

Move the fleet signing key to 0x81000100, a non-reserved owner persistent
handle (clear of EK 0x81010001/2 and SRK 0x81000001/2). Threaded through:
  - fleet-enrol -handle default + new rd.dot1x.handle= cmdline knob (baked by
    extract-artifacts.sh, overridable via SMBIOS)
  - files/etc/dot1x/wpa_supplicant-dot1x.conf private_key handle
  - the dracut hook, bench tpm-provision.sh / tpm-selftest.sh
  - CLAUDE.md invariant #1 + design/SSP docs + TODO #11
NV index 0x01800001 (owner range) is unchanged.

EnsureKey hardening: refuse to evict any restricted-decrypt object (EK/SRK) as
a safety net — never clobber the TPM endorsement/storage identity (also
protects the future EK/AK attestation path). Still self-heals a genuine stale
signing key. New regression TestEnsureKeyRefusesToEvictProtectedKey; the
stale-key regression and handle constants moved to 0x81000100.

Design note records the IDevID (0x81020001) option for the later EK/AK
attestation work. Follow-up items logged: content-aware NV presence check
(verify the cert chains to the fleet CA, so leftover/foreign NV can't suppress
enrolment) and a de-enrol/TPM-reset procedure.
2026-07-02 12:19:59 +00:00
Audiobuzz 4c55ed4b26 docs/TODO #11: record stale non-ECC key bench finding + SELinux hand-run caveat 2026-07-02 11:43:31 +00:00
Audiobuzz 1e1265b5a0 docs/TODO: add #15 — scope root overlay to per-subtree tmpfs (un-shadowable /usr + STIG mount options)
Captures the design + quantified STIG payoff (~21 mount/partition rules
notapplicable/waived -> pass, per reports/stig-arf.xml), the writable-set +
noexec/nosuid/nodev flag matrix, the kiosk-/tmp-noexec prerequisite, the
fstab-vs-runtime OVAL note, and that it's bench-gated after TPM enrolment (#11).
2026-07-02 11:31:33 +00:00
Audiobuzz 545d459c3d tpm-enrol: allow enrolment under rd.dot1x=0 (test-mode) for bench testing
Removes the hard no-op on the test path so the enrolment flow can be exercised
on a host with a TPM + a reachable broker WITHOUT standing up 802.1X/MACsec/
RADIUS. Networking is now mode-aware: in test mode ip=dhcp already owns the NIC
(and wrote resolv.conf), so the hook uses the existing lease instead of running
its own nmcli DHCP — avoiding the dracut "address already assigned" race
(CLAUDE.md "don't double-configure the NIC in test mode"). Secure mode still does
the nm-unmanaged override for the pre-auth lease. rd.fleet.enrol=0 disables
enrolment in either mode; production intent stays secure-path-only.
2026-07-02 10:00:26 +00:00
Audiobuzz 6278f2847d 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.
2026-07-02 09:55:54 +00:00
Audiobuzz 8e1d40a30b tpm-enrol: inline empty-NV first-boot enrolment (host side, TODO #11)
Builds the host side of TPM identity enrolment — the "inline single-UKI"
vehicle from docs/tpm-enrolment-design.md. A production (secure) UKI now
auto-enrols a fresh host with no client cert, then reboots into the normal
secure path.

New dracut hook files/dracut/95dot1x-macsec/tpm-enrol.sh (initqueue/settled
prio 25, BEFORE dot1x-macsec-up at 30):
  - no-op when a cert is already present in NV 0x01800001 (normal boot) or
    under rd.dot1x=0 (test path has no trustworthy identity to enrol);
  - on EMPTY NV: generate the signing key in the TPM @0x81010001 (openssl tpm2
    provider builds the CSR; key never leaves the TPM), pre-auth DHCP on the
    PHYSICAL NIC (nmcli re-manages en*, overriding the secure-mode unmanaged
    drop-in), POST CSR+context to rd.fleet.enrol_url, show the broker
    fingerprint on Plymouth, poll /status, write the issued leaf cert (DER) to
    NV, and reboot to converge.
  - Empty-NV and the secure path are mutually exclusive, so this never reorders
    the load-bearing boot sequence (invariant #2).

Wiring:
  - module-setup.sh: require+install the tpm2 key-gen/NV tools, curl, awk, and
    the openssl tpm2 PROVIDER (ossl-modules/tpm2.so, asserted present — tpm2-tss
    alone can't sign with a handle: key); install the hook.
  - diskless.pkr.hcl: install tpm2-openssl + curl; assert hook/tools/provider in
    the initramfs (invariant #9). New enrol_url var threaded to extract-artifacts.
  - extract-artifacts.sh: bake rd.fleet.enrol_url into the cmdline.
  - fleet.script: enrol-active/enrol-wait/enrol-done banner; fingerprint on the
    message line (public — states only, invariant #1).

Shell JSON build/parse/cert-extract pipeline (no jq in the initramfs) verified
end-to-end against a live broker. BENCH-VERIFY list in docs/TODO.md #11 (tpm2
provider key-ref on real/swtpm, empty-NV detect, pre-auth DHCP override, Plymouth
render, NV write + reboot convergence, prod HTTPS).
2026-07-02 09:00:18 +00:00
Audiobuzz a370f80332 TODO #11: step signer re-verified on the serving host (720h claim, chain) 2026-07-02 08:45:30 +00:00
Audiobuzz 8376f9a92e enrol-broker: run in serving stack + fix step signer path end-to-end
Verifying the step signer against a real step-ca surfaced two real bugs the
mock signer had hidden, plus made the bench CA actually initialise:

- signer.go: move --not-after from `step ca token` (validity caps ~1h ->
  "token validity too long") to `step ca sign` (it is the CERT lifetime).
  Without this the step signer never issues a cert.
- dot1x-bench compose: DOCKER_STEPCA_INIT_PASSWORD_FILE pointed at a file that
  does not exist at first init (it is an INPUT path), so `step ca init` failed
  and never wrote secrets/password. Use DOCKER_STEPCA_INIT_PASSWORD (the value)
  which the entrypoint writes to secrets/password, where bootstrap + broker read.
- bootstrap.sh: self-heal the root-owned ./pki Docker auto-creates; detect the
  CA password at $STEPPATH/password or secrets/password (varies by image
  version); raise authority.claims.maxTLSCertDuration so 720h fleet certs are
  allowed (stock cap 24h, which also blocked bootstrap's own RADIUS cert); fix
  RADIUS cert paths to write via the /export mount (were host paths run inside
  the container, so they never resolved).

- serving stack: add the enrol-broker service (temporary home; nginx already
  proxies /enrol/) defaulting to the zero-dependency lab path (mock signer +
  insecure loopback admin), with ENROL_* knobs to switch to step + mTLS.
- verify-serving.sh: exercise request -> approve -> cert through the deployed
  nginx /enrol/ proxy.
- TODO #11: record the step-signer verification + the two fixes.

Step signer VERIFIED end-to-end against a real step-ca: issued cert chains to
the fleet root, EKU clientAuth, hostnqn derived from serial.
2026-07-02 08:35:35 +00:00
Audiobuzz 9e1dbd3ef7 initial tpm broker 2026-06-28 10:33:14 +00:00
Audiobuzz 3bcf16e384 Merge remote-tracking branch 'origin/main' into tpm-enroll 2026-06-28 18:32:39 +09:30
Audiobuzz 5232b36614 tpm design 2026-06-28 18:32:36 +09:30
Audiobuzz bc14b8986d fixing final SPDK deployment 2026-06-28 08:44:49 +00:00
Audiobuzz 2cf7ad4ffb Apply chromium policy hardening 2026-06-28 03:18:54 +00:00
Audiobuzz ad6b7e3366 Add Host API and fix kiosk performance 2026-06-28 03:14:25 +00:00
Audiobuzz 02ffd7d885 Adding spdk loader 2026-06-27 10:44:42 +00:00
Audiobuzz e43112b0f1 Permanent SPDK trial 2026-06-27 19:29:19 +09:30
Audiobuzz ce5af959ad Add SIEM 2026-06-27 08:52:17 +00:00
Audiobuzz c3507cda6e Update SSP 2026-06-25 23:46:42 +00:00
Audiobuzz dea7ca451a Adding dot1x support 2026-06-22 20:50:48 +00:00
Audiobuzz 3226a44d5a Refactor dnsmasq to build as a container. Add signed iPXE image build 2026-06-21 09:11:23 +00:00
Audiobuzz d2b2156a22 Fixed the secure boot enrollment script 2026-06-20 12:16:37 +00:00