Merge in main
This commit is contained in:
+80
-1
@@ -413,6 +413,17 @@ build {
|
||||
# AVC in Wazuh — that would also hide future rhsmcertd_t denials.)
|
||||
"systemctl mask rhsmcertd.service 2>/dev/null || true",
|
||||
"systemctl disable rhsmcertd.service 2>/dev/null || true",
|
||||
# Same class of noise, different trigger: dnf-makecache.timer fires ~10 min
|
||||
# after boot and imports the whole dnf Python stack (dnf/libdnf/librepo/rhsm).
|
||||
# Because the reproducible erofs `-T 0` reset makes every source .py look
|
||||
# newer than its baked .pyc, that import REWRITES bytecode into the tmpfs
|
||||
# overlay (measured 2026-07-03: ~MBs of dnf/libdnf/rhsm __pycache__) and
|
||||
# throws lib_t write/unlink AVCs. An immutable diskless host never refreshes
|
||||
# dnf caches (no persistent store, no runtime installs), so mask the timer to
|
||||
# remove the trigger. Belt-and-braces with the compileall unchecked-hash step
|
||||
# in 2d (which stops the rewrite at the source). See docs/TODO.md #16.
|
||||
"systemctl mask dnf-makecache.timer 2>/dev/null || true",
|
||||
"systemctl disable dnf-makecache.timer 2>/dev/null || true",
|
||||
# Read-only erofs/verity root; runtime state on a tmpfs overlay provided
|
||||
# by systemd.volatile=overlay (set in the cmdline by extract-artifacts.sh).
|
||||
# RHEL/Rocky dracut has no 'overlayfs'/'rootovl' module.
|
||||
@@ -855,6 +866,21 @@ build {
|
||||
"semodule -i /tmp/chrony_dhcp_dispatcher.pp",
|
||||
"rm -f /tmp/chrony_dhcp_dispatcher.mod /tmp/chrony_dhcp_dispatcher.pp",
|
||||
"semodule -l | grep -q '^chrony_dhcp_dispatcher' && echo '==> SELinux module chrony_dhcp_dispatcher installed' || { echo 'ERROR: chrony_dhcp_dispatcher module not installed' >&2; exit 1; }",
|
||||
# SELinux: allow fapolicyd to statfs (getattr) tmpfs. fapolicyd is the exec
|
||||
# guard for the WRITABLE overlay (dm-verity covers only the RO erofs lower);
|
||||
# it statfs's mounts to decide which to watch (tmpfs is in its default
|
||||
# watch_fs). This host's root is systemd.volatile=overlay, so statfs("/")
|
||||
# lands on the tmpfs upper (audit: dev=tmpfs ino=1 name="/") and the shipped
|
||||
# policy denies it -> fapolicyd can't fully guard the tmpfs mounts (/, /tmp,
|
||||
# /dev/shm) — a real exec-guard gap. Ship a minimal allow (docs/stig-deviations.md).
|
||||
# checkpolicy already ensured above. Same policy-store-rebuild reproducibility
|
||||
# caveat as chrony.
|
||||
"echo '==> supplemental: SELinux module fapolicyd_tmpfs (fapolicyd statfs the volatile-overlay tmpfs)'",
|
||||
"checkmodule -M -m -o /tmp/fapolicyd_tmpfs.mod /tmp/stig/fapolicyd-tmpfs.te",
|
||||
"semodule_package -o /tmp/fapolicyd_tmpfs.pp -m /tmp/fapolicyd_tmpfs.mod",
|
||||
"semodule -i /tmp/fapolicyd_tmpfs.pp",
|
||||
"rm -f /tmp/fapolicyd_tmpfs.mod /tmp/fapolicyd_tmpfs.pp",
|
||||
"semodule -l | grep -q '^fapolicyd_tmpfs' && echo '==> SELinux module fapolicyd_tmpfs installed' || { echo 'ERROR: fapolicyd_tmpfs module not installed' >&2; exit 1; }",
|
||||
"rm -rf /tmp/stig",
|
||||
"echo '==> STIG remediation complete (report at /root/stig-report.html, ARF at /root/stig-arf.xml)'",
|
||||
]
|
||||
@@ -917,9 +943,43 @@ build {
|
||||
# custom kiosk files: add them to fapolicyd's trust DB and rebuild it. weston
|
||||
# /chromium are RPM-installed so already trusted. (See docs/stig-deviations.md.)
|
||||
"if command -v fapolicyd-cli >/dev/null 2>&1; then",
|
||||
# RAM: fapolicyd's LMDB trust DB is rebuilt at BOOT onto the tmpfs overlay,
|
||||
# so its map size (db_max_size, in MB) is a fixed RAM RESERVATION, not a
|
||||
# ceiling it grows into. The EL9 default (100) was measured consuming a real
|
||||
# 100 MiB of overlay RAM on a booted host (2026-07-03) — ~80% of the entire
|
||||
# overlay. The minimal+kiosk trust set is a fraction of that (fapolicyd
|
||||
# upstream shipped db_max_size=50 for years as the default that covers a
|
||||
# FULL workstation rpmdb, which is larger than this image). Halve it to
|
||||
# reclaim ~50 MiB. MUST run before the --update below so the rebuilt db uses
|
||||
# the smaller map. BENCH-VERIFY the exec-guard still works end-to-end (boot +
|
||||
# kiosk render + admin SSH + sudo): undersize this and fapolicyd hits
|
||||
# MDB_MAP_FULL, allowlisting breaks, and login/kiosk exec is bricked (keep
|
||||
# fapolicyd working — it's the writable-overlay exec guard dm-verity can't
|
||||
# provide). Can drop lower after measuring `fapolicyd-cli --dump | wc -l` on
|
||||
# the bench; see docs/TODO.md.
|
||||
" if [ -f /etc/fapolicyd/fapolicyd.conf ]; then sed -ri 's/^[[:space:]]*#?[[:space:]]*db_max_size[[:space:]]*=.*/db_max_size = 50/' /etc/fapolicyd/fapolicyd.conf; grep -q '^db_max_size' /etc/fapolicyd/fapolicyd.conf || printf 'db_max_size = 50\\n' >> /etc/fapolicyd/fapolicyd.conf; echo '==> fapolicyd: db_max_size set to 50 MiB (was EL9 default ~100)'; fi",
|
||||
" for f in /usr/local/bin/kiosk-session /usr/local/bin/fleet-sb-enroll /usr/local/bin/fleet-hostapi /usr/local/bin/fleet-agent /usr/sbin/fleet-enrol; do { [ -f \"$f\" ] && fapolicyd-cli --file add \"$f\"; } || true; done",
|
||||
" fapolicyd-cli --update 2>/dev/null || true",
|
||||
" echo '==> fapolicyd: trusted custom binaries (kiosk-session, fleet-sb-enroll, fleet-hostapi, fleet-agent) + rebuilt trust db'",
|
||||
# watch_fs: the diskless root is OVERLAYFS (systemd.volatile=overlay), but the
|
||||
# shipped watch_fs lacks 'overlay', so fapolicyd marks NO overlay mount and
|
||||
# therefore intercepts NO exec on / — the exec guard for the writable overlay
|
||||
# (the whole reason we keep fapolicyd; dm-verity can't cover the tmpfs upper)
|
||||
# is effectively OFF at runtime. VERIFIED on the bench 2026-07-03: a novel
|
||||
# non-RPM binary in /tmp (overlayfs) ran freely under `permissive=0`. Add
|
||||
# 'overlay' so fapolicyd actually watches the root; trust is path-based
|
||||
# (integrity=none) so overlay paths resolve against the rpmdb trust as normal.
|
||||
# The #15 tmpfs getattr allow supports this (fapolicyd statfs's the overlay's
|
||||
# tmpfs upper). Idempotent. BENCH-VERIFY HARD — this turns app-allowlisting ON
|
||||
# for the WHOLE root at runtime for the first time: boot + kiosk render + admin
|
||||
# SSH + sudo + TPM enrolment must ALL still work (nothing legit is untrusted),
|
||||
# a novel /tmp binary must now be BLOCKED (EPERM), and watch for new AVCs.
|
||||
# enable_stig=false is the escape hatch if it regresses the boot/secure path.
|
||||
" if [ -f /etc/fapolicyd/fapolicyd.conf ]; then",
|
||||
" grep -Eq '^watch_fs.*overlay' /etc/fapolicyd/fapolicyd.conf || sed -ri '/^watch_fs *=/ s/$/,overlay/' /etc/fapolicyd/fapolicyd.conf",
|
||||
" grep -Eq '^watch_fs.*overlay' /etc/fapolicyd/fapolicyd.conf || { echo 'ERROR: failed to add overlay to fapolicyd watch_fs' >&2; exit 1; }",
|
||||
" echo '==> fapolicyd: watch_fs now includes overlay (guards the volatile-overlay root)'",
|
||||
" fi",
|
||||
"fi",
|
||||
]
|
||||
}
|
||||
@@ -1042,6 +1102,23 @@ build {
|
||||
# UKI initramfs shrinks too — see TODO #18. Nothing re-adds firmware after
|
||||
# that point, so the rootfs erofs is already trimmed by the time we get here.)
|
||||
#
|
||||
# .pyc invalidation vs reproducible mtimes: the erofs is built `-T 0`, which
|
||||
# zeroes every file's mtime. Python's DEFAULT .pyc invalidation is
|
||||
# timestamp-based (the .pyc header stores the source's mtime), so at runtime
|
||||
# every source .py looks newer than its baked .pyc and Python recompiles into
|
||||
# the tmpfs overlay — wasting RAM and, under enforcing SELinux, throwing
|
||||
# lib_t write/unlink AVCs (seen 2026-07-03: dnf/libdnf/rhsm __pycache__
|
||||
# rewrites 10 min after boot). Recompile ALL bytecode with hash-based,
|
||||
# UNCHECKED invalidation: the .pyc then carries a source hash flagged
|
||||
# 'do not check', so Python loads it WITHOUT stat-ing the source, and never
|
||||
# rewrites regardless of the `-T 0` mtimes. Bonus: hash .pyc embed no mtime,
|
||||
# so they're more reproducible than timestamp .pyc (reinforces the -T 0
|
||||
# invariant). MUST run LAST (after every dnf install/remove) so nothing lays
|
||||
# down a fresh timestamp .pyc afterward; -f forces existing .pyc to this mode.
|
||||
# Version-agnostic loop so an EL point-release python bump doesn't silently
|
||||
# skip it. See docs/TODO.md #16.
|
||||
"for d in /usr/lib/python3.* /usr/lib64/python3.*; do [ -d \"$d\" ] && python3 -m compileall -f -q --invalidation-mode unchecked-hash \"$d\" 2>/dev/null || true; done",
|
||||
#
|
||||
# ---- FINAL GUARD: assert the kiosk stack survived the whole pipeline ----
|
||||
# 2026-07-03 incident: chromium installed cleanly in the 2b kiosk step (the
|
||||
# footprint audit showed chromium-149 + chromium-common, 523 pkgs), but by
|
||||
@@ -1055,7 +1132,9 @@ build {
|
||||
# kiosk-critical package is still installed at the very end of the pipeline
|
||||
# (after remediation AND all 2d removes). This catches ANY future silent drop
|
||||
# — a new hard-Requires cascade, an oscap package_*_removed rule, autoremove.
|
||||
# enable_kiosk=false legitimately ships no browser, so gate on it.
|
||||
# Run AFTER the .pyc recompile (read-only rpm -q, lays down no bytecode, so it
|
||||
# doesn't violate the "recompile last" rule). enable_kiosk=false legitimately
|
||||
# ships no browser, so gate on it.
|
||||
"if [ \"${var.enable_kiosk}\" = \"true\" ] && [ -n \"${var.kiosk_url}\" ]; then for p in chromium chromium-common weston mesa-dri-drivers mesa-libEGL mesa-libgbm; do rpm -q \"$p\" >/dev/null 2>&1 || { echo \"ERROR: kiosk-critical package '$p' missing from the FINAL image — a later step removed it after the 2b kiosk install (e.g. a dnf remove whose dependency cascade took chromium, cf. the 2026-07-03 nss-mdns incident). Refusing to ship a bricked kiosk.\" >&2; exit 1; }; done; echo '==> guard: kiosk stack intact (chromium/weston/mesa present after remediation + hygiene)'; fi",
|
||||
]
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user