From e6fd4a2dde0354b8e6a9a7ac89a795b159af90b1 Mon Sep 17 00:00:00 2001 From: Audiobuzz Date: Fri, 10 Jul 2026 08:39:27 +0000 Subject: [PATCH] image: SELinux allow fleet-agent chronyc probe under NoNewPrivileges MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit fleet-agent reports clock health via 'chronyc -n tracking', but the hardened unit runs with NoNewPrivileges (explicit + forced by DynamicUser/SystemCallFilter/ MemoryDenyWriteExecute). SELinux transitions the exec'd chronyc into chronyc_t, and NNP forbids that domain change without process2:nnp_transition — which the base policy doesn't grant for unconfined_service_t -> chronyc_t. Under enforcing the exec is denied (avc: denied { nnp_transition } comm=fleet-agent, seen on comet-beacon-54), so chronyc never runs and every chrony_* health field stays empty (Monitor chrony row shows '—'). uptime/cpu/mem are unaffected (no exec). Ship a minimal local module (files/stig/fleet-agent-chronyc.te) granting only that one nnp_transition to the MORE-confined chronyc_t domain — the safe direction the permission exists to gate — loosening nothing else in the agent sandbox. Built + semodule -i'd in 2c-STIG alongside the existing chrony_dhcp_dispatcher / fapolicyd_tmpfs modules. Justified in docs/stig-deviations.md #16. Needs a workstation image rebuild + reboot; same policy-store-rebuild reproducibility re-check as #14/#15. --- diskless.pkr.hcl | 16 +++++++++++++ docs/stig-deviations.md | 39 +++++++++++++++++++++++++++++++ files/stig/fleet-agent-chronyc.te | 38 ++++++++++++++++++++++++++++++ 3 files changed, 93 insertions(+) create mode 100644 files/stig/fleet-agent-chronyc.te diff --git a/diskless.pkr.hcl b/diskless.pkr.hcl index aa9147a..2272ae4 100644 --- a/diskless.pkr.hcl +++ b/diskless.pkr.hcl @@ -899,6 +899,22 @@ build { "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; }", + # SELinux: let the fleet-agent's chrony probe transition to chronyc_t under + # NoNewPrivileges. The agent (unconfined_service_t) shells out to `chronyc -n + # tracking` for clock offset/source/stratum; the shipped policy transitions + # chronyc into chronyc_t, but the hardened unit's NNP (explicit + forced by + # DynamicUser/SystemCallFilter/MDWE) makes SELinux require process2:nnp_transition + # for that move, which the base policy doesn't grant -> `denied { nnp_transition }` + # -> chronyc never runs -> all chrony_* health fields stay empty. Ship a minimal + # allow (docs/stig-deviations.md). Installed unconditionally like the modules + # above (the rule is inert without the agent). checkpolicy ensured above. Same + # policy-store-rebuild reproducibility caveat as chrony/fapolicyd. + "echo '==> supplemental: SELinux module fleet_agent_chronyc (fleet-agent chronyc under NNP)'", + "checkmodule -M -m -o /tmp/fleet_agent_chronyc.mod /tmp/stig/fleet-agent-chronyc.te", + "semodule_package -o /tmp/fleet_agent_chronyc.pp -m /tmp/fleet_agent_chronyc.mod", + "semodule -i /tmp/fleet_agent_chronyc.pp", + "rm -f /tmp/fleet_agent_chronyc.mod /tmp/fleet_agent_chronyc.pp", + "semodule -l | grep -q '^fleet_agent_chronyc' && echo '==> SELinux module fleet_agent_chronyc installed' || { echo 'ERROR: fleet_agent_chronyc 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 5f019e8..27c375f 100644 --- a/docs/stig-deviations.md +++ b/docs/stig-deviations.md @@ -248,6 +248,45 @@ surface rather than detecting abuse of it. Requires a write primitive to a root- path either way (already-privileged context), which is fapolicyd's defense-in-depth threat model — not an unprivileged bypass. +## 16. Local SELinux policy module `fleet_agent_chronyc` (added 2026-07-10) + +Not a STIG-rule waiver — a **minimal local `allow` module** shipped so the +`fleet-agent` telemetry client can read chrony status under enforcing SELinux + +NoNewPrivileges. Source: `files/stig/fleet-agent-chronyc.te`; built + `semodule +-i`'d in the 2c-STIG provisioner (right after #15). + +**The one rule:** `allow unconfined_service_t chronyc_t:process2 nnp_transition;` + +**Why:** `fleet-agent` reports the host's clock health (offset / NTP source / +stratum) by shelling out to `chronyc -n tracking`. The agent runs as +`unconfined_service_t`, and the shipped chrony policy has a `type_transition` that +moves the executed `chronyc` into the confined `chronyc_t` domain. But the +`fleet-agent.service` unit is hardened with **`NoNewPrivileges`** — set explicitly +and *also* forced implicitly by `DynamicUser=yes`, `SystemCallFilter=`, and +`MemoryDenyWriteExecute=yes` — and SELinux forbids an NNP process from +transitioning domains unless `process2:nnp_transition` is granted for that +source→target pair. The base policy grants it for some daemons but not +`unconfined_service_t → chronyc_t`, so under enforcing the exec is **denied** +(audit `comet-beacon-54` 2026-07-10: `avc: denied { nnp_transition } +comm="fleet-agent" scontext=…unconfined_service_t tcontext=…chronyc_t +tclass=process2 permissive=0`). Result: `chronyc` never runs, the agent's +`chronyTracking()` returns empty, and every `chrony_*` health field stays unset +(the manager's Monitor chrony row shows `—`). The other probes are unaffected — +`uptime_seconds`/cpu/mem read `/proc` directly and need no domain transition. + +**Scope:** one perm (`nnp_transition`) on one class (`process2`), two well-known +types. It permits a transition to a **more-confined** domain (`chronyc_t`) — the +exact safe direction `nnp_transition` exists to gate — and **loosens nothing** in +the agent's own sandbox (NNP, DynamicUser, syscall filter, MDWE all stay). It does +not grant any file read/write. Installed unconditionally alongside #14/#15 (the +rule is inert on a build without the agent). **BENCH-VERIFY:** (a) the AVC is gone +on an enforcing boot; (b) the manager's Monitor chrony row populates with +offset/source/stratum from the live NTP source; (c) no **follow-on** `chronyc_t` +AVC under the agent's mount namespace (extend the `.te` if any appears); (d) +**reproducibility** — `semodule -i` rebuilds the policy store, so re-check verity +hash stability across two builds (same concern as #12/#14/#15). NB: only shipped +when `enable_stig=true`; a non-STIG enforcing build would still see the AVC. + ## Things that are NOT deviations (kept / enforced) - **SELinux enforcing** (`selinux --enforcing`; labels preserved into the erofs). diff --git a/files/stig/fleet-agent-chronyc.te b/files/stig/fleet-agent-chronyc.te new file mode 100644 index 0000000..3a426cd --- /dev/null +++ b/files/stig/fleet-agent-chronyc.te @@ -0,0 +1,38 @@ +# 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 fleet_agent_chronyc 1.0.0; + +######################################## +# Let the fleet-agent's chrony probe transition to chronyc_t under NoNewPrivileges. +# +# fleet-agent (telemetry) shells out to `chronyc -n tracking` to report the host's +# clock offset / NTP source / stratum. The agent runs as unconfined_service_t, and +# the shipped chrony policy has a type_transition so executing chronyc moves the +# child into the confined chronyc_t domain. BUT the fleet-agent unit is hardened +# with NoNewPrivileges (set explicitly AND forced by DynamicUser + SystemCallFilter +# + MemoryDenyWriteExecute), and SELinux forbids an NNP process from transitioning +# domains unless process2:nnp_transition is granted for that source->target pair. +# The shipped policy has no such grant, so under enforcing the exec is DENIED: +# avc: denied { nnp_transition } comm="fleet-agent" +# scontext=...:unconfined_service_t tcontext=...:chronyc_t tclass=process2 +# chronyc never runs, chronyTracking() returns empty, and every chrony_* health +# field stays unset (the UI shows chrony as "—"). uptime/cpu/mem are unaffected — +# they read /proc directly and need no domain transition. +# +# This single allow lets the transition proceed to the MORE-confined chronyc_t +# domain (the safe direction NNP's nnp_transition permission exists to gate), with +# no loosening of the agent's own sandbox. Minimal + auditable; justified in +# docs/stig-deviations.md. BENCH-VERIFY: after this, the manager's Monitor chrony +# row populates (offset/source/stratum). If a FOLLOW-ON chronyc_t AVC appears on +# the bench (e.g. reaching chronyd's socket under the agent's mount namespace), +# extend here. +######################################## + +require { + type unconfined_service_t; + type chronyc_t; + class process2 nnp_transition; +} + +allow unconfined_service_t chronyc_t:process2 nnp_transition;