diff --git a/.gitea/workflows/gitea-main-ci.yaml b/.gitea/workflows/gitea-main-ci.yaml index 449e2ed..f521a62 100644 --- a/.gitea/workflows/gitea-main-ci.yaml +++ b/.gitea/workflows/gitea-main-ci.yaml @@ -49,9 +49,10 @@ 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. 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. + # URL the fullscreen Chromium kiosk loads at boot. Defaults to the kiosk SPA + # (serving/kiosk-app) reverse-proxied by the boot HTTP server 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 diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..27a3afb --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +reports diff --git a/CLAUDE.md b/CLAUDE.md index db5dad6..72ca1f3 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -34,6 +34,8 @@ files/kiosk/ weston+Chromium kiosk + chromium lockd 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 +serving/kiosk-app/ Svelte+Tailwind spaceship-terminal SPA + (kiosk endpoint; nginx reverse-proxies /kiosk/) .gitea/workflows/ Gitea CI: lint -> packer build -> push OCI docs/ Design PDF and supporting notes ``` @@ -152,9 +154,17 @@ 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`). -- STIG remediation: (a) verify the SSG datastream name on the target distro - (`ssg-rhel9-ds.xml` vs `ssg-rl9-ds.xml` — the provisioner tries both) and that - the `...content_profile_stig` profile id exists; (b) remediation must not break +- STIG remediation: (a) datastream **must match the distro**. The provisioner + maps `/etc/os-release` `$ID` → datastream (Rocky→`ssg-rl9-ds.xml`, + RHEL→`ssg-rhel9-ds.xml`, al9/cs9/ol9). **Do NOT run the RHEL9 datastream on + Rocky** — its CPE dictionary has no Rocky entry, so every rule returns + `notapplicable`, the scan evaluates **0** rules, remediation does nothing, and + the report is *falsely clean*. (Seen 2026-06-18: a build picked rhel9 via + `ls … | head -1` and produced 462 notapplicable + 1068 notselected, **0 + pass/fail**.) Two guards now exist: the provisioner aborts the build if 0 rules + are evaluated, and `stig-deviations.sh` flags an INVALID SCAN + exits 2. Also + confirm the `...content_profile_stig` profile id exists in the chosen + datastream; (b) remediation must not break boot/login (NM/NVMe, key-only sshd, kiosk, SELinux enforcing) — `enable_stig=false` is the escape hatch; (c) **reproducibility**: build twice and confirm identical `root_hash` in verity.json — if it drifts, oscap wrote something @@ -174,7 +184,10 @@ instead of working around it. 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). + the log. **RENDER-VERIFIED 2026-06-18** (test-path build): the scripted theme + draws and the handshake checklist updates from the hook status tokens. Still + unverified on the **secure path** (the auth/macsec rows reaching `done` — see + #8 bench items). - `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 diff --git a/diskless.pkr.hcl b/diskless.pkr.hcl index 3de45f1..f832e93 100644 --- a/diskless.pkr.hcl +++ b/diskless.pkr.hcl @@ -440,15 +440,31 @@ build { "if [ \"$ENABLE_STIG\" != \"true\" ]; then echo 'enable_stig=false -> skipping STIG hardening.'; mkdir -p /root; echo '
STIG hardening disabled for this build (enable_stig=false).' > /root/stig-report.html; : > /root/stig-arf.xml; exit 0; fi", "echo '==> Installing OpenSCAP + SCAP Security Guide'", "dnf install -y openscap-scanner scap-security-guide", - # SSG ships rhel9 content (usable on Rocky); some rebuilds name it rl9. - "DS=$(ls /usr/share/xml/scap/ssg/content/ssg-rhel9-ds.xml /usr/share/xml/scap/ssg/content/ssg-rl9-ds.xml 2>/dev/null | head -1)", - "[ -n \"$DS\" ] || { echo 'ERROR: SSG rhel9 datastream not found' >&2; exit 1; }", + # Pick the datastream that MATCHES THIS DISTRO. The RHEL9 datastream's CPE + # dictionary has no Rocky entry, so running it on Rocky makes EVERY rule + # 'notapplicable' -> the scan evaluates nothing and the report is falsely + # clean. SSG ships a Rocky-specific datastream (ssg-rl9-ds.xml); map by + # /etc/os-release ID (al9=AlmaLinux, cs9=CentOS Stream, ol9=Oracle). + ". /etc/os-release", + "case \"$ID\" in rocky) SSG=rl9;; almalinux) SSG=al9;; centos) SSG=cs9;; ol|oracle) SSG=ol9;; rhel|redhat) SSG=rhel9;; *) SSG=rhel9;; esac", + "DS=/usr/share/xml/scap/ssg/content/ssg-$SSG-ds.xml", + "[ -f \"$DS\" ] || DS=/usr/share/xml/scap/ssg/content/ssg-rhel9-ds.xml", # last-resort fallback + "[ -f \"$DS\" ] || { echo \"ERROR: no SSG datastream found for $ID ($DS)\" >&2; exit 1; }", + "echo \"==> Using SSG datastream $DS for $ID $VERSION_ID\"", "install -D -m0644 /tmp/stig/diskless-tailoring.xml /etc/stig/diskless-tailoring.xml", "echo \"==> Remediating against tailored STIG profile using $DS (this changes a lot)\"", # --remediate returns nonzero when rules remain failed (expected) — tolerate. "oscap xccdf eval --remediate --profile xccdf_org.ssgproject.content_profile_stig_diskless --tailoring-file /etc/stig/diskless-tailoring.xml --results-arf /root/stig-arf.xml --report /root/stig-report.html \"$DS\" || true", # Clean post-remediation eval = the report/ARF of record (no --remediate). "oscap xccdf eval --profile xccdf_org.ssgproject.content_profile_stig_diskless --tailoring-file /etc/stig/diskless-tailoring.xml --results-arf /root/stig-arf.xml --report /root/stig-report.html \"$DS\" || true", + # GUARD: a scan with 0 evaluated rules (everything notapplicable/notselected) + # means the datastream/CPE didn't match this host — NO hardening happened and + # the report would be falsely 'clean'. Fail the build rather than ship an + # unhardened image masquerading as STIG-compliant. (enable_stig=false is the + # intentional way to skip hardening.) + "evaluated=$(grep -oE '| Channel | +Band | +Status | +
|---|---|---|
| {c.name} | +{c.band} | +
{m.text}
+| Deck | +O₂ | +Temp | +
|---|---|---|
| {d.deck} | +{d.temp} | +
| Waypoint | +Range | +Status | +
|---|---|---|
| {w.name} | +{w.dist} | +