From 3ef34745c97c3d3574520b303fdb6a0fb55e2bee Mon Sep 17 00:00:00 2001 From: Audiobuzz Date: Wed, 17 Jun 2026 14:28:27 +0000 Subject: [PATCH 1/4] Adding Plymouth --- .gitea/workflows/gitea-main-ci.yaml | 13 ++++- CLAUDE.md | 15 ++++- diskless.pkr.hcl | 22 +++++++- docs/TODO.md | 27 ++++++--- files/etc/dracut.conf.d/90-diskless.conf | 8 +++ http/ks.cfg | 1 - scripts/extract-artifacts.sh | 15 +++++ serving/README.md | 3 + serving/docker-compose.yml | 4 ++ serving/http/default.conf | 26 +++++++++ serving/http/kiosk/index.html | 72 ++++++++++++++++++++++++ 11 files changed, 188 insertions(+), 18 deletions(-) create mode 100644 serving/http/default.conf create mode 100644 serving/http/kiosk/index.html diff --git a/.gitea/workflows/gitea-main-ci.yaml b/.gitea/workflows/gitea-main-ci.yaml index 45cd395..b01361b 100644 --- a/.gitea/workflows/gitea-main-ci.yaml +++ b/.gitea/workflows/gitea-main-ci.yaml @@ -49,8 +49,16 @@ env: # per-environment DNS name (e.g. nvme.fleet.lab) resolved at boot so one UKI # works across environments — the serving stack's dnsmasq resolves it. NVMET_TRADDR: "192.168.50.10" - # URL the fullscreen Chromium kiosk loads at boot. Empty disables the kiosk. - KIOSK_URL: "https://www.reddit.com" + # URL the fullscreen Chromium kiosk loads at boot. Defaults to the test page + # served by the boot HTTP server (serving/http/kiosk) at /kiosk/; boot.fleet.lab + # resolves to the serving host via the stack's dnsmasq. Empty disables the kiosk. + KIOSK_URL: "http://boot.fleet.lab/kiosk/" + # Plymouth boot splash. "false" => verbose console boot (rhgb quiet NOT baked; + # plymouth.enable=0 instead) — kept OFF in CI while the network-manager port is + # being bench-verified, so the boot chain stays debuggable. Flip to "true" to + # ship the graphical splash once boot is trusted. (Plymouth is in the image + # either way; this only flips the cmdline.) + BOOT_SPLASH: "false" # =========================================================================== # TEMPORARY: 802.1X + MACsec are DISABLED for this build (rd.dot1x=0) so the # UKI / rootfs / NVMe-oF boot path can be validated on an OPEN switch port. @@ -178,4 +186,5 @@ jobs: -var nvmet_traddr="${{ env.NVMET_TRADDR }}" \ -var ssh_authorized_key="${{ secrets.SSH_AUTHORIZED_KEY }}" \ -var kiosk_url="${{ env.KIOSK_URL }}" \ + -var boot_splash=${{ env.BOOT_SPLASH }} \ . diff --git a/CLAUDE.md b/CLAUDE.md index 218c108..ec3d422 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -161,9 +161,10 @@ instead of working around it. `network-manager` module hands its initramfs connections to the real-root NM (state under `/run/NetworkManager`), which renews the lease. No more `dhcp-client`/`dhclient` (deprecated). The hook drives macsec0 DHCP via `nmcli` - (not `ifup`). See invariant on the managed/unmanaged split. BENCH-VERIFY: lease - actually renews past expiry (test path); macsec0 managed + `en*` unmanaged with - MKA surviving switch-root (secure path). + (not `ifup`). See invariant on the managed/unmanaged split. Test path + VERIFIED on the bench 2026-06-17 (lease renews past expiry; host stays up). + STILL BENCH-VERIFY when `disable_dot1x=false`: macsec0 managed + `en*` + unmanaged with MKA surviving switch-root (secure path). - Writable root overlay is `systemd.volatile=overlay`, **not** `rootovl`: RHEL/Rocky dracut ships no `overlayfs`/`rootovl` module. Needs `systemd-volatile-root.service` in the initramfs (bundled by recent dracut; @@ -194,6 +195,14 @@ These bit us bringing the build up on Rocky 9.8; they are easy to regress. stack stays in the image, dormant. The matching lab DHCP/HTTP/NVMe-oF serving stack lives in `serving/` (docker compose). Re-enable security by dropping the var / setting `DISABLE_DOT1X: "false"` in the CI workflow. +- **Boot splash toggle**: `boot_splash=true` (default) bakes `rhgb quiet` for a + Plymouth graphical splash (theme `spinner`, set in the provisioner); `false` + bakes `plymouth.enable=0` for a verbose console boot — keep it OFF while + debugging the boot chain. Plymouth is always in the image + initramfs (the + `plymouth` dracut module, which pulls `drm`); only the cmdline changes. CI + currently sets `BOOT_SPLASH: "false"` during the network-manager bench-verify. + Press ESC at boot to see details behind a splash. ks.cfg no longer excludes + `plymouth`. ## Testing changes diff --git a/diskless.pkr.hcl b/diskless.pkr.hcl index a87b0d3..1e335aa 100644 --- a/diskless.pkr.hcl +++ b/diskless.pkr.hcl @@ -120,8 +120,14 @@ variable "enable_kiosk" { variable "kiosk_url" { type = string - description = "URL the fullscreen Chromium kiosk loads at boot (cage Wayland session on tty1, baked into the immutable root). Empty disables the kiosk install entirely. A runtime config-agent could override it later via /etc/kiosk/kiosk.env." - default = "https://www.reddit.com" + description = "URL the fullscreen Chromium kiosk loads at boot (weston session on tty1, baked into the immutable root). Defaults to the self-contained test page served by the lab boot HTTP server at /kiosk/ (boot.fleet.lab resolves to the serving host via the stack's dnsmasq) — validates the boot end-to-end without external connectivity. Empty disables the kiosk install entirely. A runtime config-agent could override it later via /etc/kiosk/kiosk.env." + default = "http://boot.fleet.lab/kiosk/" +} + +variable "boot_splash" { + type = bool + description = "true (default): bake 'rhgb quiet' into the UKI cmdline so Plymouth shows a graphical boot splash (covers the network-auth + NVMe/TCP + verity phase). false: bake 'plymouth.enable=0' for a fully verbose console boot — use while debugging the boot chain. Plymouth itself is always in the image; this only flips the cmdline. (The cmdline is immutable once signed, so this is a build-time choice.)" + default = true } variable "sb_key" { @@ -222,6 +228,14 @@ build { # dracut warns and silently falls back to gzip (a larger initramfs). "dnf -y install wpa_supplicant tpm2-tools tpm2-tss nvme-cli cryptsetup dracut-network NetworkManager zstd openssl", + # Plymouth boot splash. plymouth-scripts provides plymouth-set-default-theme; + # spinner is the clean modern theme. Set the default theme BEFORE the dracut + # build below so the (-no-hostonly) initramfs bundles it; no -R here because + # we rebuild the initramfs explicitly. Whether the splash shows at boot is + # decided by the cmdline (boot_splash var), not by installing this. + "dnf -y install plymouth plymouth-scripts plymouth-theme-spinner", + "plymouth-set-default-theme spinner", + # Stage etc files into place "install -D -m0644 /tmp/etc-stage/dot1x/radius-ca.pem /etc/dot1x/radius-ca.pem", "install -D -m0644 /tmp/etc-stage/dot1x/wpa_supplicant-dot1x.conf /etc/dot1x/wpa_supplicant-dot1x.conf", @@ -266,6 +280,8 @@ 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; }", + # 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; }", # ---- Build hygiene --------------------------------------------------- "dnf clean all", @@ -402,7 +418,7 @@ build { # home dirs) — we dropped mkfs.erofs --all-root, which used to mask this by # flattening everything to root:root. sudo is a no-op if already root, and # the runner has passwordless sudo (cf. the `sudo apt-get` install step). - "sudo bash -c \"NVMET_TRADDR='${var.nvmet_traddr}' bash scripts/extract-artifacts.sh output-qemu/diskless-build.qcow2 ${var.artifact_dir}/${var.image_version} ${var.disable_dot1x}\"", + "sudo bash -c \"NVMET_TRADDR='${var.nvmet_traddr}' BOOT_SPLASH='${var.boot_splash}' bash scripts/extract-artifacts.sh output-qemu/diskless-build.qcow2 ${var.artifact_dir}/${var.image_version} ${var.disable_dot1x}\"", # Hand the OUTPUT FILES back to the build user (their in-erofs ownership is # already baked correctly) so build-oci.sh can write the UKI and push with # the build user's `buildah login`. diff --git a/docs/TODO.md b/docs/TODO.md index 6ce83b6..698cfd0 100644 --- a/docs/TODO.md +++ b/docs/TODO.md @@ -5,13 +5,18 @@ Persistent backlog, kept in sync with the working session task tracker. Status: --- -## [~] 1. Port the dracut module off `network-legacy` → `network-manager` +## [x] 1. Port the dracut module off `network-legacy` → `network-manager` -**IMPLEMENTED 2026-06-17 — awaiting boot-bench verification.** Fixes the -confirmed lease-expiry outage: network-legacy's one-shot DHCP lease was never -renewed post-switch-root, so the address dropped at lease expiry → NVMe/TCP root -died ("keep alive timeout" / "no route to host"). NM hands its initramfs -connections to the real-root NM (`/run/NetworkManager` state), which renews. +**DONE — test path VERIFIED ON THE BENCH 2026-06-17** (host stays up past the +DHCP lease time; no more lease-expiry outage). Fixed the confirmed outage: +network-legacy's one-shot DHCP lease was never renewed post-switch-root, so the +address dropped at lease expiry → NVMe/TCP root died ("keep alive timeout" / +"no route to host"). NM hands its initramfs connections to the real-root NM +(`/run/NetworkManager` state), which renews. + +REMAINING: the **secure path** (macsec0 DHCP via `nmcli`, physical NIC unmanaged +via `nm-unmanaged.sh`) is only exercised once `disable_dot1x=false` — verify on +the swtpm + hostapd + FreeRADIUS bench when re-enabling security (see #2/cleanup). Done: - `90-diskless.conf` `add_dracutmodules`: `network-legacy` → `network-manager`. @@ -125,8 +130,11 @@ Path to getting here (don't re-discover): smaller image. **Re-enable with `-var enable_kiosk=true` when returning to kiosk work** — CI does not pass the var, so CI currently builds lean (no kiosk) too; add `-var enable_kiosk=true` to the Gitea workflow to ship it. -- `kiosk_url` Packer var (default `https://www.reddit.com`; empty disables the - whole install). Wired in CI as `KIOSK_URL` env. +- `kiosk_url` Packer var (default `http://boot.fleet.lab/kiosk/` — the + self-contained test page served by the boot HTTP server at + `serving/http/kiosk/`, via the `/kiosk/` nginx location; validates a boot + without external connectivity. Empty disables the whole install). Wired in CI + as `KIOSK_URL` env. - Provisioner enables EPEL, installs `weston chromium mesa-dri-drivers mesa-libEGL mesa-libgbm dejavu-sans-fonts`, creates a passwordless unprivileged `kiosk` user (video/input groups). @@ -156,7 +164,8 @@ Path to getting here (don't re-discover): Make the diskless workstation boot straight into a locked-down kiosk: a -fullscreen Chromium showing a configured site (initial target: Reddit). All +fullscreen Chromium showing a configured site (default: the boot HTTP server's +`/kiosk/` test page). All baked at **build time** into the root tree (immutable erofs/verity root; runtime is the tmpfs overlay), fleet-generic (no per-host state). diff --git a/files/etc/dracut.conf.d/90-diskless.conf b/files/etc/dracut.conf.d/90-diskless.conf index f959368..a58470e 100644 --- a/files/etc/dracut.conf.d/90-diskless.conf +++ b/files/etc/dracut.conf.d/90-diskless.conf @@ -15,6 +15,14 @@ hostonly="no" # lease was never renewed post-switch-root -> the address was dropped at lease # expiry -> NVMe/TCP root died ("keep alive timeout" / "no route to host"). add_dracutmodules+=" dot1x-macsec network-manager nvmf systemd-veritysetup " +# plymouth: graphical boot splash, shown from early initramfs through the long +# network-auth + NVMe/TCP + verity phase until the kiosk/login takes over. The +# plymouth dracut module depends on 'drm', which in --no-hostonly mode pulls a +# broad set of DRM/KMS drivers (incl. the VM's bochs/virtio-gpu); if a specific +# fleet GPU's splash doesn't render, add its drm kmod to add_drivers like the +# NICs. Whether the splash actually displays is gated by the cmdline (rhgb quiet +# vs plymouth.enable=0), set from the boot_splash var in extract-artifacts.sh. +add_dracutmodules+=" plymouth " filesystems+=" erofs overlay " add_drivers+=" macsec nvme-tcp nvme-fabrics dm-verity @NIC_DRIVERS@ " install_items+=" /etc/dot1x/radius-ca.pem /etc/dot1x/wpa_supplicant-dot1x.conf " diff --git a/http/ks.cfg b/http/ks.cfg index cab41f3..ef8c847 100644 --- a/http/ks.cfg +++ b/http/ks.cfg @@ -32,7 +32,6 @@ reboot kernel openssh-server chrony --plymouth -firewalld %end diff --git a/scripts/extract-artifacts.sh b/scripts/extract-artifacts.sh index 3e46851..d88f6ab 100755 --- a/scripts/extract-artifacts.sh +++ b/scripts/extract-artifacts.sh @@ -23,6 +23,10 @@ DISABLE_DOT1X=${3:-false} # environments. Defaults to the literal placeholder so build-oci.sh's warning # fires if it was never set. NVMET_TRADDR=${NVMET_TRADDR:-NVMET_TRADDR} +# Plymouth boot splash toggle (from the boot_splash Packer var). "true" bakes +# "rhgb quiet" so Plymouth shows the graphical splash; anything else bakes +# "plymouth.enable=0" for a fully verbose console boot (boot-chain debugging). +BOOT_SPLASH=${BOOT_SPLASH:-true} # Stable namespace UUIDs. Devices are referenced by-id # (/dev/disk/by-id/nvme-uuid.) rather than /dev/nvmeNnM because NVMe # controller/namespace indices are NOT stable (a transient discovery controller @@ -142,6 +146,16 @@ jq -n --arg hash "$ROOT_HASH" --arg erofs_uuid "$FIXED_UUID" \ # The device node assumes the root namespace is the host's first/only # NVMe/TCP namespace; adjust if not. NVMET_TRADDR: set per environment # *before* the UKI build, since the cmdline is immutable once signed. +# Plymouth: rhgb (Red Hat graphical boot) + quiet for a clean splash, or +# plymouth.enable=0 for a verbose debug boot. Press ESC at boot to see details +# behind the splash; or override at runtime via the SMBIOS Type 11 +# io.systemd.stub.kernel-cmdline-extra OEM string (adds args without a rebuild). +if [ "$BOOT_SPLASH" = "true" ]; then + SPLASH_FRAGMENT="rhgb quiet " +else + SPLASH_FRAGMENT="plymouth.enable=0 " +fi + DOT1X_FRAGMENT="" if [ "$DISABLE_DOT1X" = "true" ]; then echo "==> WARNING: building TEST cmdline with rd.dot1x=0 (802.1X + MACsec DISABLED)" >&2 @@ -154,6 +168,7 @@ if [ "$DISABLE_DOT1X" = "true" ]; then DOT1X_FRAGMENT="rd.dot1x=0 ip=dhcp " fi printf '%s' \ + "$SPLASH_FRAGMENT" \ "$DOT1X_FRAGMENT" \ "rd.neednet=1 systemd.volatile=overlay " \ "rd.dot1x.nvindex=0x01800001 rd.dot1x.timeout=60 " \ diff --git a/serving/README.md b/serving/README.md index 2b29afa..1cca9eb 100644 --- a/serving/README.md +++ b/serving/README.md @@ -58,6 +58,9 @@ docker compose ps # HTTP: UKI is served curl -sI http://${SERVER_IP}/fleet-${IMAGE_VERSION}.efi | head -1 # 200 OK +# HTTP: kiosk test page is served (what the diskless kiosk loads by default) +curl -sI http://${SERVER_IP}/kiosk/ | head -1 # 200 OK + # NVMe/TCP: target is listening + discoverable ss -lntp | grep 4420 sudo nvme discover -t tcp -a ${SERVER_IP} -s 4420 # lists the subsystem diff --git a/serving/docker-compose.yml b/serving/docker-compose.yml index 4bfb85c..793648c 100644 --- a/serving/docker-compose.yml +++ b/serving/docker-compose.yml @@ -89,6 +89,10 @@ services: subpath: artifacts # mount only /artifacts from the image # Fallback (Docker < 28 / offline): bind the local Packer output dir: # - ${ARTIFACT_SRC}:/usr/share/nginx/html:ro + # Serve the kiosk test page at /kiosk/ alongside the artifacts. Config + # adds the location; the page is lab content (NOT in the artifact image). + - ./http/default.conf:/etc/nginx/conf.d/default.conf:ro + - ./http/kiosk:/usr/share/nginx/kiosk:ro # For HTTPS boot (production), terminate TLS here with the boot-infra CA's # cert and switch the dhcp-boot URL above to https://. Left HTTP for the lab. diff --git a/serving/http/default.conf b/serving/http/default.conf new file mode 100644 index 0000000..d72a9d9 --- /dev/null +++ b/serving/http/default.conf @@ -0,0 +1,26 @@ +# Lab boot HTTP server (nginx). Replaces the stock default.conf so one server on +# :80 serves BOTH: +# / -> the OCI release artifacts (UKI fleet-.efi, etc.), mounted +# read-only from the image volume at /usr/share/nginx/html +# /kiosk/ -> the diskless kiosk test page (lab content bind-mounted at +# /usr/share/nginx/kiosk, deliberately NOT part of the artifact +# image, which stays FROM-scratch). +# A separate kiosk container was considered, but every service here uses host +# networking, so a second nginx would fight for :80; a single location is cleaner. +server { + listen 80 default_server; + server_name _; + + # UKI + release artifacts (firmware fetches /fleet-.efi from here). + location / { + root /usr/share/nginx/html; + autoindex on; + } + + # Kiosk test page. The diskless kiosk (kiosk_url) points at + # http://boot.fleet.lab/kiosk/ — resolved to this host by the stack's dnsmasq. + location /kiosk/ { + alias /usr/share/nginx/kiosk/; + index index.html; + } +} diff --git a/serving/http/kiosk/index.html b/serving/http/kiosk/index.html new file mode 100644 index 0000000..984f25f --- /dev/null +++ b/serving/http/kiosk/index.html @@ -0,0 +1,72 @@ + + + + + + +Fleet Kiosk — Boot OK + + + +

▣ FLEET KIOSK ONLINE

+
diskless boot validated — weston + chromium rendering local HTTP
+
+
--:--:--
+
+ served from… + page url… + user agent… + screen… +
+
boot HTTP server · replace this page or point kiosk_url elsewhere
+ + + From 0fdc7980ccc479953809c935243579d0b58f38f3 Mon Sep 17 00:00:00 2001 From: Audiobuzz Date: Wed, 17 Jun 2026 22:21:08 +0000 Subject: [PATCH 2/4] Enable boot splash --- .gitea/workflows/gitea-main-ci.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.gitea/workflows/gitea-main-ci.yaml b/.gitea/workflows/gitea-main-ci.yaml index b01361b..9315c38 100644 --- a/.gitea/workflows/gitea-main-ci.yaml +++ b/.gitea/workflows/gitea-main-ci.yaml @@ -58,7 +58,7 @@ env: # being bench-verified, so the boot chain stays debuggable. Flip to "true" to # ship the graphical splash once boot is trusted. (Plymouth is in the image # either way; this only flips the cmdline.) - BOOT_SPLASH: "false" + BOOT_SPLASH: "true" # =========================================================================== # TEMPORARY: 802.1X + MACsec are DISABLED for this build (rd.dot1x=0) so the # UKI / rootfs / NVMe-oF boot path can be validated on an OPEN switch port. From 838ffd600bf107a70d862a3f245b9e5694c8e7bb Mon Sep 17 00:00:00 2001 From: Audiobuzz Date: Wed, 17 Jun 2026 22:52:18 +0000 Subject: [PATCH 3/4] Lock down chromium policy --- diskless.pkr.hcl | 7 +++++- docs/TODO.md | 9 +++++++ files/kiosk/chromium-kiosk-policy.json | 34 ++++++++++++++++++++++++++ files/kiosk/kiosk-session | 14 +++++++++++ 4 files changed, 63 insertions(+), 1 deletion(-) create mode 100644 files/kiosk/chromium-kiosk-policy.json diff --git a/diskless.pkr.hcl b/diskless.pkr.hcl index 1e335aa..855e780 100644 --- a/diskless.pkr.hcl +++ b/diskless.pkr.hcl @@ -324,12 +324,17 @@ build { "install -D -m0755 /tmp/kiosk/kiosk-session /usr/local/bin/kiosk-session", "install -D -m0644 /tmp/kiosk/kiosk.service /etc/systemd/system/kiosk.service", "install -D -m0644 /tmp/kiosk/weston.ini /etc/xdg/weston/weston.ini", + # Chromium managed policy: stops the browser phoning home to Google + # (sign-in, variations, optimization-guide, component/auto update, metrics, + # safe browsing). EPEL chromium reads /etc/chromium/policies/managed/*.json. + # Pairs with the hardening flags in kiosk-session. + "install -D -m0644 /tmp/kiosk/chromium-kiosk-policy.json /etc/chromium/policies/managed/10-kiosk-lockdown.json", # URL from the var (weston renderer is set via --use-pixman in the unit). "install -d -m0755 /etc/kiosk", "printf 'KIOSK_URL=%s\\n' \"$KIOSK_URL\" > /etc/kiosk/kiosk.env", "chmod 0644 /etc/kiosk/kiosk.env", # Label the new files so they survive into the enforcing erofs. - "restorecon -R /usr/local/bin/kiosk-session /etc/systemd/system/kiosk.service /etc/xdg/weston /etc/kiosk", + "restorecon -R /usr/local/bin/kiosk-session /etc/systemd/system/kiosk.service /etc/xdg/weston /etc/kiosk /etc/chromium", # Boot graphical and start the kiosk (replaces the tty1 getty). "systemctl enable kiosk.service", "systemctl set-default graphical.target", diff --git a/docs/TODO.md b/docs/TODO.md index 698cfd0..5aa948d 100644 --- a/docs/TODO.md +++ b/docs/TODO.md @@ -155,6 +155,15 @@ Path to getting here (don't re-discover): avc`; the kiosk user is unconfined so likely none — but do NOT set permissive if they appear; add a policy module). Admin SSH (the key user) still works for debug even if the kiosk fails (it lands at graphical.target either way). +- **Phone-home lockdown (done 2026-06-17):** Chromium was dialing Google + (accounts/variations/optimization-guide/component+auto update/safe browsing — + seen in dnsmasq logs). Stopped via a managed policy + (`files/kiosk/chromium-kiosk-policy.json` → `/etc/chromium/policies/managed/`) + plus hardening flags in `kiosk-session` (`--disable-background-networking`, + `--disable-component-update`, `--disable-features=OptimizationHints,...`, etc.). + Verify with the dnsmasq query log after a rebuild — only `boot.fleet.lab` + (kiosk page) + chrony NTP should remain. NTP is the OS (chrony), not Chromium; + optionally point it at the serving host to fully contain egress. - **Image size:** Chromium+Mesa add hundreds of MB to the *uncompressed* erofs — feeds the "shrink the rootfs" concern (and #7); reconsider whether a lighter browser/render path is viable. diff --git a/files/kiosk/chromium-kiosk-policy.json b/files/kiosk/chromium-kiosk-policy.json new file mode 100644 index 0000000..1255fda --- /dev/null +++ b/files/kiosk/chromium-kiosk-policy.json @@ -0,0 +1,34 @@ +{ + "_comment": "Chromium managed policy for the diskless kiosk. Installed to /etc/chromium/policies/managed/. Stops Chromium phoning home to *.google.com / *.googleapis.com (sign-in, variations, optimization guide, component/auto updates, metrics, safe browsing) — none of which a fixed-URL kiosk needs. Pairs with the hardening flags in kiosk-session. Unknown keys are ignored by Chromium, so this is forward/backward safe.", + + "MetricsReportingEnabled": false, + "UrlKeyedAnonymizedDataCollectionEnabled": false, + "DeviceMetricsReportingEnabled": false, + + "BrowserSignin": 0, + "SyncDisabled": true, + + "SafeBrowsingProtectionLevel": 0, + "SafeBrowsingExtendedReportingEnabled": false, + + "ComponentUpdatesEnabled": false, + "ChromeVariations": 2, + "BrowserNetworkTimeQueriesEnabled": false, + + "SearchSuggestEnabled": false, + "DefaultSearchProviderEnabled": false, + "SpellCheckServiceEnabled": false, + "TranslateEnabled": false, + "NetworkPredictionOptions": 2, + + "BackgroundModeEnabled": false, + "PromotionalTabsEnabled": false, + "WelcomePageOnOSUpgradeEnabled": false, + "DefaultBrowserSettingEnabled": false, + + "AutofillAddressEnabled": false, + "AutofillCreditCardEnabled": false, + "PasswordManagerEnabled": false, + "ShoppingListEnabled": false, + "EnableMediaRouter": false +} diff --git a/files/kiosk/kiosk-session b/files/kiosk/kiosk-session index a17688a..59e46de 100644 --- a/files/kiosk/kiosk-session +++ b/files/kiosk/kiosk-session @@ -34,10 +34,24 @@ BROWSER=$(command -v chromium-browser || command -v chromium || true) done export WAYLAND_DISPLAY="$SOCK" while :; do + # --disable-* / --no-pings etc.: stop Chromium phoning home to Google + # (background networking = variations/field-trials + GCM, component & + # auto updates, domain reliability, crash upload, hyperlink pings). The + # --disable-features list kills the Optimization Guide hints fetch + # (optimizationguide-pa.googleapis.com), Translate, Media Router discovery + # and Autofill server calls. The comprehensive lockdown (sign-in, metrics, + # safe browsing, search suggest, ...) is the managed policy at + # /etc/chromium/policies/managed/10-kiosk-lockdown.json; these flags cover + # what policy can't. A fixed-URL kiosk needs none of it. "$BROWSER" \ --kiosk --noerrdialogs --disable-infobars --incognito --no-first-run \ --disable-session-crashed-bubble --start-maximized \ --ozone-platform=wayland --disable-gpu \ + --disable-background-networking --disable-component-update \ + --disable-sync --disable-domain-reliability --disable-breakpad \ + --no-pings --no-default-browser-check --metrics-recording-only \ + --disable-search-engine-collection \ + --disable-features=Translate,OptimizationHints,OptimizationTargetPrediction,MediaRouter,AutofillServerCommunication,InterestFeedContentSuggestions \ --user-data-dir=/tmp/kiosk-profile --disk-cache-dir=/tmp/kiosk-cache \ "$KIOSK_URL" || true sleep 2 From 8f8e24ac286a79f4212b53c0cecf72bfe8256e16 Mon Sep 17 00:00:00 2001 From: Audiobuzz Date: Thu, 18 Jun 2026 08:41:22 +0000 Subject: [PATCH 4/4] Update boot theme --- CLAUDE.md | 35 ++++-- diskless.pkr.hcl | 29 +++-- .../dracut/95dot1x-macsec/dot1x-macsec-up.sh | 14 +++ files/dracut/95dot1x-macsec/nvmf-connect.sh | 7 ++ files/plymouth/themes/fleet/fleet.plymouth | 8 ++ files/plymouth/themes/fleet/fleet.script | 109 ++++++++++++++++++ 6 files changed, 187 insertions(+), 15 deletions(-) create mode 100644 files/plymouth/themes/fleet/fleet.plymouth create mode 100644 files/plymouth/themes/fleet/fleet.script diff --git a/CLAUDE.md b/CLAUDE.md index ec3d422..ccd0b53 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -28,6 +28,9 @@ files/etc/dot1x/ Supplicant config + RADIUS CA (placeho files/etc/dracut.conf.d/90-diskless.conf no-hostonly + drivers + modules (NM unmanaged split is now runtime: 95dot1x-macsec/nm-unmanaged.sh, secure-mode only) +files/plymouth/themes/fleet/ Custom boot splash (scripted security + checklist; driven by hook status tokens) +files/kiosk/ weston+Chromium kiosk + chromium lockdown policy scripts/extract-artifacts.sh qcow2 -> kernel/initramfs/erofs+verity/cmdline scripts/build-oci.sh ukify + sbsign + roothash gate + OCI image serving/ Lab serving stack: dnsmasq + nginx + SPDK @@ -149,6 +152,13 @@ instead of working around it. build; the supplicant config carries both options. - erofs module availability on the target RHEL kernel (fallback: journal-less ext4 in `extract-artifacts.sh`). +- `fleet` Plymouth theme: confirm (a) the script plugin package actually + installed (`/usr/lib64/plymouth/script.so` — else the build fell back to + `spinner`, logged as a WARNING), (b) the scripted theme + a font got bundled + into the initramfs (`lsinitrd | grep -E 'fleet.script|script.so'`), and (c) it + renders (text via `Image.Text` needs the label plugin + a font in the early + initramfs). If blank, the splash silently no-ops but boot continues; ESC shows + the log. Untested in this workspace (needs a display). - `systemd.verity_root_options=hash-offset` is **NOT supported** by Rocky 9.8 systemd-veritysetup (verified: "unknown option hash-offset=" → "Failed to load verity superblock"). Resolved by the documented fallback — data and hash are @@ -195,14 +205,23 @@ These bit us bringing the build up on Rocky 9.8; they are easy to regress. stack stays in the image, dormant. The matching lab DHCP/HTTP/NVMe-oF serving stack lives in `serving/` (docker compose). Re-enable security by dropping the var / setting `DISABLE_DOT1X: "false"` in the CI workflow. -- **Boot splash toggle**: `boot_splash=true` (default) bakes `rhgb quiet` for a - Plymouth graphical splash (theme `spinner`, set in the provisioner); `false` - bakes `plymouth.enable=0` for a verbose console boot — keep it OFF while - debugging the boot chain. Plymouth is always in the image + initramfs (the - `plymouth` dracut module, which pulls `drm`); only the cmdline changes. CI - currently sets `BOOT_SPLASH: "false"` during the network-manager bench-verify. - Press ESC at boot to see details behind a splash. ks.cfg no longer excludes - `plymouth`. +- **Boot splash toggle**: `boot_splash=true` (default) bakes `rhgb quiet` for the + Plymouth splash; `false` bakes `plymouth.enable=0` for a verbose console boot — + keep OFF while debugging the boot chain. Plymouth is always in the image + + initramfs (the `plymouth` dracut module, which pulls `drm`); only the cmdline + changes. Press ESC at boot to see details behind a splash. ks.cfg no longer + excludes `plymouth`. +- **Splash theme = custom `fleet`** (`files/plymouth/themes/fleet/`, script + plugin, asset-free — drawn with `Image.Text`). It renders a security-handshake + checklist (TPM → 802.1X → MACsec → Network → NVMe-oF) driven by best-effort + `plymouth update --status=` calls from the dracut hooks + (`dot1x-macsec-up.sh`, `nvmf-connect.sh`). Tokens: `mode-test` (marks + auth/MACsec skipped), `tpm|dot1x|macsec|net|nvme`-`active|done`. **Rules:** the + emits are non-fatal (`plymouth … || true`) and must never reorder the + load-bearing boot sequence; show only STATES, never per-host data (invariant + #1). The provisioner installs the script plugin best-effort and **falls back to + the stock `spinner` theme** if `script.so` is absent, so a plugin-package-name + change can't break the build. ## Testing changes diff --git a/diskless.pkr.hcl b/diskless.pkr.hcl index 855e780..a3f445b 100644 --- a/diskless.pkr.hcl +++ b/diskless.pkr.hcl @@ -212,6 +212,13 @@ build { destination = "/tmp/" } + # Custom 'fleet' Plymouth theme (scripted security-handshake splash). Staged to + # /tmp/plymouth, installed + set default in the package step before dracut runs. + provisioner "file" { + source = "files/plymouth" + destination = "/tmp/" + } + # --- 2. Packages, configs, diskless adaptations -------------------------- provisioner "shell" { inline = [ @@ -228,13 +235,21 @@ build { # dracut warns and silently falls back to gzip (a larger initramfs). "dnf -y install wpa_supplicant tpm2-tools tpm2-tss nvme-cli cryptsetup dracut-network NetworkManager zstd openssl", - # Plymouth boot splash. plymouth-scripts provides plymouth-set-default-theme; - # spinner is the clean modern theme. Set the default theme BEFORE the dracut - # build below so the (-no-hostonly) initramfs bundles it; no -R here because - # we rebuild the initramfs explicitly. Whether the splash shows at boot is - # decided by the cmdline (boot_splash var), not by installing this. - "dnf -y install plymouth plymouth-scripts plymouth-theme-spinner", - "plymouth-set-default-theme spinner", + # Plymouth boot splash. The custom scripted 'fleet' theme renders the + # security-handshake checklist driven by the dracut hooks + # (`plymouth update --status=...`). plymouth-plugin-label gives Image.Text + # its font rendering; dejavu fonts back it. The script-plugin package name + # has varied across releases, so install it best-effort and FALL BACK to the + # stock 'spinner' theme if script.so is absent — never break the build over + # the theme. Set the default theme BEFORE the dracut build below so the + # (--no-hostonly) initramfs bundles the theme + its plugin + a font; no -R + # (we rebuild the initramfs explicitly). Whether the splash shows at boot is + # gated by the cmdline (boot_splash var), not by installing this. + "dnf -y install plymouth plymouth-scripts plymouth-plugin-label dejavu-sans-fonts", + "dnf -y install plymouth-plugin-script 2>/dev/null || dnf -y install plymouth-theme-script 2>/dev/null || true", + "cp -a /tmp/plymouth/themes/fleet /usr/share/plymouth/themes/fleet", + "if [ -e /usr/lib64/plymouth/script.so ]; then plymouth-set-default-theme fleet; echo '==> plymouth theme: fleet (scripted security splash)'; else dnf -y install plymouth-theme-spinner && plymouth-set-default-theme spinner; echo 'WARNING: plymouth script plugin (script.so) missing — using spinner; fleet security splash unavailable' >&2; fi", + "rm -rf /tmp/plymouth", # Stage etc files into place "install -D -m0644 /tmp/etc-stage/dot1x/radius-ca.pem /etc/dot1x/radius-ca.pem", diff --git a/files/dracut/95dot1x-macsec/dot1x-macsec-up.sh b/files/dracut/95dot1x-macsec/dot1x-macsec-up.sh index d718a01..3ab4416 100755 --- a/files/dracut/95dot1x-macsec/dot1x-macsec-up.sh +++ b/files/dracut/95dot1x-macsec/dot1x-macsec-up.sh @@ -15,6 +15,11 @@ STAMP=/run/dot1x/.done [ -e "$STAMP" ] && exit 0 mkdir -p /run/dot1x /run/wpa_supplicant +# Best-effort boot-splash status for the 'fleet' Plymouth theme. NEVER fatal and +# NEVER reorders anything — a splash call must not perturb the load-bearing +# auth -> macsec -> DHCP -> NVMe sequence below. No-op if plymouth isn't running. +ply() { plymouth update --status="$1" 2>/dev/null || true; } + # --- knobs (override on the kernel cmdline) --------------------------------- NIC=$(getarg rd.dot1x.if=) if [ -z "$NIC" ]; then @@ -42,6 +47,7 @@ info "dot1x-macsec: using interface $NIC, NV index $NVINDEX" # networks only. Remove rd.dot1x=0 from the cmdline to restore full security. if [ "$(getarg rd.dot1x=)" = "0" ]; then warn "dot1x-macsec: 802.1X/MACsec DISABLED (rd.dot1x=0) — TEST MODE, no link security" + ply mode-test # splash: mark TPM/802.1X/MACsec rows as skipped (not failed) # Networking in test mode is owned ENTIRELY by `ip=dhcp` on the cmdline: # dracut's network module brings the NIC up, gets a lease and writes # resolv.conf. Do NOT `ifup`/`ip link set up` the NIC here — a second @@ -70,6 +76,7 @@ if [ "$(getarg rd.dot1x=)" = "0" ]; then fi # --- 1. client certificate out of TPM NV ------------------------------------ +ply tpm-active if [ ! -s /run/dot1x/client.pem ]; then if ! tpm2_nvread -T device:/dev/tpmrm0 "$NVINDEX" -o /run/dot1x/client.der; then warn "dot1x-macsec: tpm2_nvread failed — cannot authenticate" @@ -77,6 +84,7 @@ if [ ! -s /run/dot1x/client.pem ]; then fi openssl x509 -inform DER -in /run/dot1x/client.der -out /run/dot1x/client.pem fi +ply tpm-done # --- 2. supplicant as a root storage daemon --------------------------------- ip link set "$NIC" up @@ -88,6 +96,7 @@ if [ ! -e /run/dot1x/wpa.pid ]; then -c /etc/dot1x/wpa_supplicant-dot1x.conf \ -P /run/dot1x/wpa.pid" & fi +ply dot1x-active # --- 3. wait for authentication + macsec0 ----------------------------------- i=0 @@ -104,9 +113,13 @@ if [ "$i" -ge "$TIMEOUT" ]; then exit 1 fi info "dot1x-macsec: port authorized, macsec0 up after ${i}s" +ply dot1x-done +ply macsec-active # --- 4. address the secure interface; netroot rides on macsec0 -------------- ip link set macsec0 up +ply macsec-done +ply net-active # DHCP on macsec0 via NetworkManager (NOT the old network-legacy `ifup`): NM owns # macsec0, so it RENEWS the lease in the running system after switch-root — the # whole point of the network-manager port. macsec0 was created externally by @@ -127,6 +140,7 @@ if [ "$i" -ge "$TIMEOUT" ]; then exit 1 fi info "dot1x-macsec: macsec0 addressed via NetworkManager (DHCP)" +ply net-done # --- 5. derive per-host NVMe identity from the TPM certificate -------------- # Keeps the image fleet-generic: the host NQN is a function of the machine diff --git a/files/dracut/95dot1x-macsec/nvmf-connect.sh b/files/dracut/95dot1x-macsec/nvmf-connect.sh index b16b8ad..cb29e60 100644 --- a/files/dracut/95dot1x-macsec/nvmf-connect.sh +++ b/files/dracut/95dot1x-macsec/nvmf-connect.sh @@ -29,6 +29,12 @@ _trsvcid=${_rest##*,} # nvme_tcp is usually auto-loaded; ensure it so /dev/nvme-fabrics exists. modprobe nvme_tcp 2>/dev/null +# Best-effort boot-splash status for the 'fleet' Plymouth theme (never fatal). By +# the time this runs the ip=dhcp link is up, so mark Network done and NVMe active. +ply() { plymouth update --status="$1" 2>/dev/null || true; } +ply net-done +ply nvme-active + # Keep-alive / reconnect tuning for the ROOT device. These persist across # switch-root (the in-kernel nvme-tcp controller and its opts survive). # -k 30 keep-alive timeout (s). The 5s default means a KA every 2.5s; under @@ -53,6 +59,7 @@ while [ "$_i" -lt 60 ]; do if [ -b "$_ns" ]; then mkdir -p /run/dot1x : > /run/dot1x/.nvmf-connected + ply nvme-done echo "nvmf-connect: connected ${_trtype}/${_traddr}/${_trsvcid} ($_ns)" exit 0 fi diff --git a/files/plymouth/themes/fleet/fleet.plymouth b/files/plymouth/themes/fleet/fleet.plymouth new file mode 100644 index 0000000..9fc93ea --- /dev/null +++ b/files/plymouth/themes/fleet/fleet.plymouth @@ -0,0 +1,8 @@ +[Plymouth Theme] +Name=Fleet +Description=Cyber sci-fi secure diskless bring-up splash (TPM / 802.1X / MACsec / NVMe-oF). Driven by `plymouth update --status=` tokens from the dracut hooks. +ModuleName=script + +[script] +ImageDir=/usr/share/plymouth/themes/fleet +ScriptFile=/usr/share/plymouth/themes/fleet/fleet.script diff --git a/files/plymouth/themes/fleet/fleet.script b/files/plymouth/themes/fleet/fleet.script new file mode 100644 index 0000000..d197b46 --- /dev/null +++ b/files/plymouth/themes/fleet/fleet.script @@ -0,0 +1,109 @@ +# Fleet — cyber sci-fi secure-bring-up splash (Plymouth "script" plugin). +# +# Asset-free on purpose: everything is drawn with Image.Text, so the repo carries +# no binary images. The dracut hooks drive it with `plymouth update --status=` +# (see dot1x-macsec-up.sh / nvmf-connect.sh); this script maps each token to a +# security-handshake checklist and pulses the in-progress step. Tokens are +# best-effort and order-independent — anything not reported just stays "pending", +# and in rd.dot1x=0 test mode the auth/MACsec rows are marked "skipped" (never +# "failed"). No per-host data is ever shown (only states), per the no-state rule. + +# ---- background (dark teal -> black) ---- +Window.SetBackgroundTopColor(0.00, 0.05, 0.07); +Window.SetBackgroundBottomColor(0.00, 0.00, 0.00); + +sw = Window.GetWidth(); +sh = Window.GetHeight(); + +# ---- title / subtitle ---- +title_img = Image.Text("F L E E T", 0.0, 1.0, 0.85, 1, "Sans Bold 36"); +title = Sprite(title_img); +title.SetX(sw / 2 - title_img.GetWidth() / 2); +title.SetY(sh / 2 - 150); + +sub_img = Image.Text("secure diskless bring-up", 0.30, 0.65, 0.65, 1, "Sans 14"); +sub = Sprite(sub_img); +sub.SetX(sw / 2 - sub_img.GetWidth() / 2); +sub.SetY(sh / 2 - 102); + +# ---- handshake stages ---- +# state per stage: 0 pending, 1 active, 2 done, 3 skipped +n = 5; +label[0] = "TPM identity"; +label[1] = "802.1X EAP-TLS"; +label[2] = "MACsec link"; +label[3] = "Network (DHCP)"; +label[4] = "NVMe-oF root"; + +col_x = sw / 2 - 150; +row_y = sh / 2 - 36; +row_h = 34; + +fun draw(i) { + glyph = "[ ]"; r = 0.22; g = 0.42; b = 0.42; # pending (dim teal) + if (state[i] == 1) { glyph = "[>>]"; r = 1.00; g = 0.72; b = 0.10; } # active (amber) + if (state[i] == 2) { glyph = "[OK]"; r = 0.10; g = 1.00; b = 0.50; } # done (neon green) + if (state[i] == 3) { glyph = "[--]"; r = 0.30; g = 0.30; b = 0.36; } # skipped (grey) + img = Image.Text(glyph + " " + label[i], r, g, b, 1, "Sans 16"); + row[i].SetImage(img); + row[i].SetX(col_x); + row[i].SetY(row_y + i * row_h); + row[i].SetOpacity(1.0); +} + +i = 0; +while (i < n) { + state[i] = 0; + row[i] = Sprite(); + draw(i); + i = i + 1; +} + +# ---- footer ---- +foot_img = Image.Text("erofs + dm-verity • NVMe/TCP • no per-host state", + 0.25, 0.45, 0.45, 1, "Sans 11"); +foot = Sprite(foot_img); +foot.SetX(sw / 2 - foot_img.GetWidth() / 2); +foot.SetY(sh / 2 + 150); + +# ---- status tokens from the boot hooks ---- +fun set(i, s) { state[i] = s; draw(i); } + +fun on_status(t) { + if (t == "mode-test") { set(0, 3); set(1, 3); set(2, 3); } + if (t == "tpm-active") set(0, 1); + if (t == "tpm-done") set(0, 2); + if (t == "dot1x-active") set(1, 1); + if (t == "dot1x-done") set(1, 2); + if (t == "macsec-active") set(2, 1); + if (t == "macsec-done") set(2, 2); + if (t == "net-active") set(3, 1); + if (t == "net-done") set(3, 2); + if (t == "nvme-active") set(4, 1); + if (t == "nvme-done") set(4, 2); +} +Plymouth.SetUpdateStatusFunction(on_status); + +# ---- optional free-text line (e.g. warnings via `plymouth message`) ---- +msg = Sprite(); +fun on_message(text) { + img = Image.Text(text, 0.55, 0.80, 0.80, 1, "Sans 12"); + msg.SetImage(img); + msg.SetX(sw / 2 - img.GetWidth() / 2); + msg.SetY(sh / 2 + 182); +} +Plymouth.SetMessageFunction(on_message); + +# ---- pulse the in-progress row ---- +tick = 0; +fun on_refresh() { + tick = tick + 1; + o = 1.0; + if (tick % 24 < 12) o = 0.45; + i = 0; + while (i < n) { + if (state[i] == 1) row[i].SetOpacity(o); + i = i + 1; + } +} +Plymouth.SetRefreshFunction(on_refresh);