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
2026-06-13 05:06:44 +00:00
2026-06-12 01:46:29 +00:00
2026-06-17 14:28:27 +00:00
2026-06-12 02:01:42 +00:00
2026-06-12 01:46:29 +00:00
2026-06-12 01:46:29 +00:00

Diskless Fleet Image Build (Packer)

Builds the fleet-generic boot artifacts for PXE/HTTP-booted diskless hosts that authenticate with 802.1X EAP-TLS (TPM-held key, certificate in TPM NV) and run MACsec on the access link. The root filesystem is an immutable, reproducible erofs image with an appended dm-verity hash tree, served as a single shared read-only NVMe/TCP namespace, with a tmpfs overlay for runtime state. Companion to docs/diskless-fleet-design-v2.pdf (see docs/design-v2-errata.md for as-built deltas on RHEL/Rocky 9).

Layout

diskless.pkr.hcl                          Packer template (qemu builder)
http/ks.cfg                               Kickstart for the throwaway build VM
files/dracut/95dot1x-macsec/              Custom dracut module
  module-setup.sh                           module definition
  dot1x-macsec-up.sh                        initqueue hook: NV cert -> @wpa_supplicant
                                            -> wait authorized + macsec0 -> DHCP
                                            -> hostnqn derived from cert
files/etc/dot1x/wpa_supplicant-dot1x.conf Fleet-generic supplicant config
files/etc/dot1x/radius-ca.pem             PLACEHOLDER - replace with real CA chain
files/etc/dracut.conf.d/90-diskless.conf  no-hostonly + nvmf/verity/erofs + drivers
files/etc/NetworkManager/                 marks NIC/macsec0 unmanaged post-pivot
scripts/extract-artifacts.sh              qcow2 -> vmlinuz + initramfs.img
                                                 + rootfs.erofs (+verity) + cmdline
scripts/build-oci.sh                      ukify (+sbsign) + roothash gate -> OCI image
serving/                                  Lab serving stack (docker compose):
                                          dnsmasq DHCP/DNS + nginx (UKI/HTTP) + SPDK
.gitea/workflows/                         Gitea CI: lint -> packer build -> push OCI
docs/diskless-fleet-design-v2.pdf         Architecture design (authoritative)

Build host requirements

  • Packer >= 1.10, qemu plugin (packer init .)
  • qemu-kvm with KVM available
  • libguestfs-tools, erofs-utils, cryptsetup, jq (artifact extraction)
  • systemd-ukify + systemd-boot-efi (the EFI stub ukify wraps; it's only a recommends of ukify) (+ sbsigntools when signing) — UKI assembly
  • buildah (or docker) — OCI release-artifact image build

The default iso_url/iso_checksum point at a version-pinned Rocky 9.x minimal ISO on the internal mirror. http/ks.cfg installs only core+sshd from that ISO; the diskless tooling is pulled online by the provisioner, so the build VM needs outbound network to the distro repos (and a fast mirror helps). Bump the URL and the pinned sha256 together when moving to a new point release.

Build

# 1. Replace the CA placeholder
cp /path/to/radius-ca-chain.pem files/etc/dot1x/radius-ca.pem

# 2. Set the NIC drivers for your real fleet hardware (default covers
#    common Intel + Mellanox); the build VM only has virtio, so anything
#    not listed here will not be in the initramfs.
packer init .
packer build \
  -var image_version=1.0.0 \
  -var nic_drivers="ice mlx5_core" \
  -var oci_image=registry.example.org/fleet-boot \
  -var oci_push=true \
  -var sb_key=/secure/ci/fleet-sb.key \
  -var sb_cert=/secure/ci/fleet-sb.crt \
  .

Omit sb_key/sb_cert for unsigned lab builds (the pipeline warns). Set the NVMe discovery address with -var nvmet_traddr=<dns-name> (default nvme.fleet.lab, which the serving/ stack's dnsmasq resolves); it is baked into the cmdline and immutable once signed, so prefer a per-environment DNS name over a raw IP. Deploy/boot-test instructions live in serving/README.md.

Test mode (auth disabled). Add -var disable_dot1x=true to bake rd.dot1x=0 into the cmdline. At boot the hook then skips 802.1X/MACsec and DHCPs the physical NIC directly to reach the NVMe/TCP target — for validating the UKI/rootfs/NVMe-oF path on an open switch port. The auth stack stays in the image (dormant); drop the var to restore the secure boot. See serving/ for the matching DHCP/HTTP/NVMe-oF lab stack.

Outputs: artifacts/<version>/ gains fleet-<version>.efi (the signed UKI) alongside rootfs.erofs/verity.json, and the build produces the release-artifact OCI image <oci_image>:<version> containing only /artifacts/{fleet-<version>.efi, rootfs.erofs, verity.json, SHA256SUMS} with the roothash recorded as an image label. The build hard-fails if the roothash baked into the UKI cmdline does not match verity.json.

Publishing a version

The OCI image is the release. The serving stack stays constant (separate, independently-patched SPDK and nginx/caddy images); each fleet release only changes which artifact image feeds them:

  1. Push <oci_image>:<version> (or let oci_push=true do it) and record the digest.
  2. In the fleet-boot deployment manifest (k8s Deployment or compose), bump the artifact initContainer / image-volume reference to that digest — one commit per fleet release; this manifest is the single place coupling UKI <-> root namespace.
  3. The initContainer copies /artifacts into the shared volume; nginx serves fleet-<version>.efi over HTTPS (firmware validates against the TlsCaCertificate CA, then Secure Boot verifies the UKI signature); SPDK exposes rootfs.erofs as a read-only NVMe/TCP namespace.
  4. Rollout = digest bump; rollback = revert the commit. cosign the image so the deploy pipeline verifies provenance (sbsign is for firmware, cosign is for your cluster).

Kernel command line (per build — emitted in cmdline.txt)

rd.neednet=1 systemd.volatile=overlay \
rd.dot1x.nvindex=0x01800100 rd.dot1x.timeout=60 \
rd.nvmf.discover=tcp,NVMET_TRADDR,4420 \
root=/dev/mapper/root rootfstype=erofs rootflags=ro \
systemd.verity=yes roothash=<from verity.json> \
systemd.verity_root_data=/dev/nvme0n1 \
systemd.verity_root_hash=/dev/nvme0n1 \
systemd.verity_root_options=hash-offset=<from verity.json>

systemd.volatile=overlay gives the writable tmpfs overlay over the read-only verity root — RHEL/Rocky dracut has no rootovl/overlayfs module (that is a Debian/third-party feature), and systemd's volatile overlay is the supported mechanism for dm-verity roots. Single-file layout: data and hash tree share one namespace, split by hash-offset. rd.dot1x.if= may pin the NIC; otherwise the first non-loopback interface is used. Test builds prepend rd.dot1x=0.

Bench-verify before fleet rollout

  1. erofs module present in the target RHEL kernel (modprobe erofs) — enabled in later RHEL 9 minors for image mode; if absent, fall back to journal-less ext4 (mkfs.ext4 -O ^has_journal -d <tree>) in extract-artifacts.sh.
  2. systemd.verity_root_options support in the shipped systemd/dracut (needs a reasonably recent systemd; otherwise publish data and hash as two separate namespaces and drop the hash-offset option).
  3. TPM key reference: confirm wpa_supplicant accepts private_key="handle:0x81000100" via the OpenSSL tpm2 provider; if not, switch to the tpm2-pkcs11 option in the supplicant config.
  4. CONFIG_MACSEC in the shipped wpa_supplicant build.
  5. Pivot survival: @wpa_supplicant survives switch-root; MKA continuity across it. NVMe/TCP keepalives must also ride out the pivot (they will, if the link never drops — that is the point of the @-daemon).
  6. network backend: the hook uses network-legacy/ifup, which requires dhclient. RHEL 9 deprecated it and the minimal ISO omits it, so the provisioner installs dhcp-client explicitly (without it dracut fails with "module network-legacy can't be installed"). dhclient is on its way out; the durable fix is to port the hook to the network-manager dracut module.
  7. hostnqn allow-listing: if the nvmet target filters hosts, the allowed-NQN list must be fed from the enrolment pipeline (NQNs are derived from certificate serials — see the hook).
  8. CI smoke test: boot artifacts in qemu/Proxmox with swtpm against hostapd-as-authenticator + FreeRADIUS (remember bridge group_fwd_mask=8 for EAPOL); assert macsec0 up, dm-verity root mapped, overlay rw.
  9. writable root overlay: provided by systemd.volatile=overlay, which needs systemd-volatile-root.service in the initramfs (bundled by recent dracut). If the root stays read-only at boot, add install_items+=" /usr/lib/systemd/systemd-volatile-root /usr/lib/systemd/system/systemd-volatile-root.service " to 90-diskless.conf. Known to misbehave with NFS root — N/A here (NVMe block).

What is deliberately NOT in the image

  • Private keys, certificates, host names, per-host config of any kind. All identity lives in each host's TPM (persistent handle + NV index); the NVMe hostnqn is derived from the certificate at boot.
S
Description
No description provided
Readme MIT
6.6 MiB
Languages
Go 47.2%
Shell 27.7%
HCL 15.5%
Svelte 4.5%
JavaScript 2%
Other 3%