diff --git a/diskless.pkr.hcl b/diskless.pkr.hcl index a0855ad..84f70ee 100644 --- a/diskless.pkr.hcl +++ b/diskless.pkr.hcl @@ -730,6 +730,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)'", ] diff --git a/docs/stig-deviations.md b/docs/stig-deviations.md index b229d0e..aa55ad6 100644 --- a/docs/stig-deviations.md +++ b/docs/stig-deviations.md @@ -167,6 +167,36 @@ re-check the verity hash is stable across two builds (same concern as the (the module install lives in the STIG-gated provisioner); a non-STIG *enforcing* build would still see the AVC. +## 15. Local SELinux policy module `fapolicyd_tmpfs` (added 2026-07-03) + +Not a STIG-rule waiver — a **minimal local `allow` module** shipped to close an +**exec-guard gap** under enforcing SELinux. Source: `files/stig/fapolicyd-tmpfs.te`; +built + `semodule -i`'d in the 2c-STIG provisioner (right after #14). + +**The one rule:** `allow fapolicyd_t tmpfs_t:filesystem getattr;` + +**Why:** fapolicyd is the app-allowlisting **exec guard for the writable RAM +overlay** — dm-verity covers only the read-only erofs lower, so a binary dropped +into the tmpfs overlay upper is exactly what fapolicyd must catch (this is why we +KEEP fapolicyd; see "Things that are NOT deviations"). fapolicyd `statfs`'s mounts +to decide which to watch, and **tmpfs is in its default `watch_fs`**. This image's +root is `systemd.volatile=overlay`, so `statfs("/")` returns the overlay **upper** +— a tmpfs (audit: `dev=tmpfs ino=1 name="/"`, `fapolicyd_t → tmpfs_t:filesystem +getattr`, `permissive=0`, seen 2026-07-03). The shipped policy has no such rule +(no ordinary host has its root on tmpfs), so under enforcing the `getattr` is +**denied**, fapolicyd cannot fully mark the tmpfs mounts (`/`, `/tmp`, `/dev/shm`), +and its guard over the writable overlay is **incomplete** — a real gap, not just +SIEM noise. An **`allow`** (not `dontaudit`): we *want* fapolicyd to see the tmpfs. + +**Scope:** one perm (`getattr`) on one class (`filesystem`), two well-known types. +Grants no file read/write, only the `statfs` fapolicyd needs to classify + watch +the mount. **BENCH-VERIFY:** (a) the AVC is gone on an enforcing boot; (b) fapolicyd +**blocks a non-RPM executable dropped in `/tmp`** (proves the guard now covers the +overlay tmpfs) while the trusted kiosk/appliance binaries still run; (c) +**reproducibility** — `semodule -i` rebuilds the policy store, so re-check verity +hash stability across two builds (same concern as #12/#14). NB: only shipped when +`enable_stig=true`; a non-STIG enforcing build would still see the AVC and the gap. + ## Things that are NOT deviations (kept / enforced) - **SELinux enforcing** (`selinux --enforcing`; labels preserved into the erofs). diff --git a/files/stig/fapolicyd-tmpfs.te b/files/stig/fapolicyd-tmpfs.te new file mode 100644 index 0000000..60636ac --- /dev/null +++ b/files/stig/fapolicyd-tmpfs.te @@ -0,0 +1,31 @@ +# Raw module-policy syntax (compiled with `checkmodule -M -m`), NOT the refpolicy +# `policy_module()` m4 macro — that macro is only expanded by the refpolicy +# Makefile and checkmodule rejects it. See chrony-dhcp-dispatcher.te for the why. +module fapolicyd_tmpfs 1.0.0; + +######################################## +# Let fapolicyd statfs (getattr) tmpfs filesystems. +# +# fapolicyd is the app-allowlisting EXEC GUARD for the writable RAM overlay: +# dm-verity covers only the read-only erofs lower, so an executable dropped into +# the tmpfs overlay upper is exactly what fapolicyd must catch. fapolicyd statfs's +# mounts to decide which to watch, and tmpfs is in its default watch_fs. This +# host's root is `systemd.volatile=overlay`, so statfs("/") returns the overlay +# UPPER — a tmpfs (the audit shows dev=tmpfs ino=1, name="/") — and the shipped +# policy has no `fapolicyd_t tmpfs_t:filesystem getattr` rule (no normal host has +# its root on tmpfs). Result under enforcing: the getattr is DENIED, fapolicyd +# cannot fully mark the tmpfs mounts (/, /tmp, /dev/shm), and its guard over the +# writable overlay is incomplete — a real exec-guard gap, not just SIEM noise. +# This single allow closes it. Minimal + auditable; justified in +# docs/stig-deviations.md. BENCH-VERIFY: after this, fapolicyd blocks a non-RPM +# executable dropped in /tmp (the overlay). If a follow-on fapolicyd AVC on the +# overlay appears on the bench, extend here. +######################################## + +require { + type fapolicyd_t; + type tmpfs_t; + class filesystem getattr; +} + +allow fapolicyd_t tmpfs_t:filesystem getattr;