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.
Root cause of the trim-rpm-48dc3e1 'flashing cursor, no kiosk' + mysteriously
small (~500 MB) image: the 2d hygiene step ran 'dnf remove -y nss-mdns', but the
EPEL chromium package HARD-Requires nss-mdns (Chromium uses .local mDNS hostnames
for WebRTC ICE candidates). So dnf removed chromium as a dependent package and
then reaped its orphaned GUI stack (avahi, at-spi2, atk, adwaita, chromium-common
...) — a ~300 MB drop. The shipped image had no browser, so kiosk-session died at
the ${BROWSER:?} guard before ever starting weston: a bare KMS cursor.
The earlier nss-mdns commit checked what nss-mdns NEEDS (avahi) but not what NEEDS
nss-mdns (chromium).
Fix:
- Keep nss-mdns + avahi (satisfy chromium's Requires); instead MASK the avahi
mDNS daemon/socket so nothing listens on :5353 — that was the real
attack-surface concern, and nss-mdns is only an NSS module (no daemon needed).
- Add a FINAL GUARD at the end of 2d: assert chromium/chromium-common/weston/mesa
are still installed in the final image, else fail the BUILD. A missing browser
must brick the build, never the fleet. Catches any future silent removal
cascade, oscap package_*_removed rule, or autoremove.
853ae20's diagnostic did 'awk … | sort -rn | head -25'. head closes the pipe
after 25 lines, sort dies of SIGPIPE (141), and step 2's set -euxo pipefail turns
that into a build failure (the exact trap the lsinitrd '| grep -q' comment warns
about). The kiosk step's rpm-manifest '| head' is safe only because that step
runs plain set -e, no pipefail.
Fix with the codebase convention: sort into a file, then head the file — no live
pipe for head to break. (Bonus: the crash log confirms omit_drivers worked —
amdgpu/radeon/nouveau kmods are gone from the top-25; the new biggest initramfs
item is GenuineIntel.bin CPU microcode at 22 MB, a future trim candidate.)
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.
Investigating avahi/samba/rabbitmq in the image: ldd on libavformat.so confirms
samba (libsmbclient), librabbitmq, librist, libsrt and libzmq are all DT_NEEDED
by FFmpeg's libavformat-free — removing any breaks libavformat and thus future
HTML5 media in the kiosk, so they stay. avahi is the exception: it's absent from
libavformat's links and 'rpm -e --test avahi' shows its only reverse-dep is
nss-mdns (the mDNS/.local NSS resolver).
The fleet resolves via DHCP/DNS (boot.fleet.lab), never .local, so mDNS is dead
weight and a network-listening service unwanted on a locked-down kiosk. Remove
nss-mdns; dnf's clean_requirements_on_remove reaps avahi/avahi-glib/avahi-libs as
orphans. Belt-and-braces alongside install_weak_deps=False (which already drops
nss-mdns if it came in as a Recommends).
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.
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.
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.
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).
http/ks.cfg excludes firewalld (-firewalld) and sets firewall --disabled — the
fleet's security boundary is the network (802.1X/MACsec + switch); the host
firewall is a documented OFF item (stig-deviations #4). But a provisioner dnf
pulled firewalld back as a weak dep (Recommends) and it starts by default. Its
Python then tries to rewrite site-package .pyc bytecode, which the reproducible
erofs -T 0 mtime reset makes look stale, so firewalld_t gets write/unlink AVCs
on lib_t that flood the SIEM.
Remove firewalld in 2d hygiene (weak dep, nothing hard-needs it) and mask it as
a backstop against future weak-dep reinstalls. No firewalld => no daemon, no
AVC, matches the kickstart intent.
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).
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.
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).