diff --git a/docs/stig-deviations.md b/docs/stig-deviations.md index 0a346a3..f3f7891 100644 --- a/docs/stig-deviations.md +++ b/docs/stig-deviations.md @@ -275,6 +275,15 @@ reach it. No write, no other chrony access, and — unlike the earlier abandoned approach — **no domain transition** (the agent reads a file; it does not exec `chronyc`). This replaces the never-shipped `fleet_agent_chronyc` nnp-transition module (that whole command-socket path was dropped for reading the log instead). + +**This is the SELinux half of a two-layer requirement.** `/var/log/chrony` is +mode `0750 chrony:chrony` (others have no access), so DAC also blocks the agent's +`DynamicUser` from traversing the dir — a denial that produces **no AVC** (it's +DAC, not TE). The DAC half is therefore the **`chrony` supplementary group** on +`fleet-agent.service` (traverse the `0750` dir; the log file itself is `0644`). +Both this module and that group are needed; verified 2026-07-10 — with the group +but no module the read is TE-denied, and with the module but no group it's +DAC-denied (silently, no AVC — which is what made this the hardest layer to spot). **BENCH-VERIFY:** (a) the manager's Monitor chrony row populates on an enforcing boot; (b) no other chrony-log AVC (extend here if one appears); (c) **reproducibility** — `semodule -i` rebuilds the policy store, re-check verity hash diff --git a/files/agent/fleet-agent.service b/files/agent/fleet-agent.service index 901fb01..66ae2b7 100644 --- a/files/agent/fleet-agent.service +++ b/files/agent/fleet-agent.service @@ -16,10 +16,16 @@ Restart=always RestartSec=5 # --- sandboxing (STIG-friendly; looser than hostapi — needs the TPM + egress) --- -# Runs as a transient unprivileged user in the tss group for /dev/tpmrm0 (the -# private key stays in the TPM; the agent only signs the TLS handshake through it). +# Runs as a transient unprivileged user. Groups: +# tss — /dev/tpmrm0 (the private key stays in the TPM; the agent only signs +# the TLS handshake through it). +# chrony — TRAVERSE /var/log/chrony to read tracking.log for the clock-health +# probe. That dir is mode 0750 chrony:chrony (others have no access), so +# DAC needs group membership to even enter it — a bare 0644 on the log +# file isn't enough. (SELinux read of the chronyd_var_log_t label is a +# SEPARATE layer, granted by the fleet_agent_chronylog module.) DynamicUser=yes -SupplementaryGroups=tss +SupplementaryGroups=tss chrony # Expose ONLY the TPM resource-manager node (no PrivateDevices — that would hide it). DeviceAllow=/dev/tpmrm0 rw DevicePolicy=closed diff --git a/files/agent/snapshot.go b/files/agent/snapshot.go index 8d81861..f961838 100644 --- a/files/agent/snapshot.go +++ b/files/agent/snapshot.go @@ -314,10 +314,12 @@ const chronyLogPath = "/var/log/chrony/tracking.log" // We read a FILE rather than query chronyd's command socket by design: that unix // socket is owner-write-only (0755) so the hardened non-root agent (DynamicUser, // not the chrony user) can't connect, and STIG closes the UDP command port -// (cmdport 0). `log tracking` writes the same fields to a world-readable log the -// agent can read under ProtectSystem=strict — no port, no socket, no group, no -// SELinux domain transition. Tradeoff: the offset is as fresh as chrony's last -// update (up to ~maxpoll old), which is fine for a slow-moving health metric. +// (cmdport 0). `log tracking` writes the same fields to this log and no domain +// transition is needed. Two access layers must both be satisfied (the unit does): +// the chrony group to TRAVERSE /var/log/chrony (0750) at the DAC layer, and the +// fleet_agent_chronylog SELinux module to read the chronyd_var_log_t label under +// enforcing. Tradeoff: the offset is as fresh as chrony's last update (up to +// ~maxpoll old), which is fine for a slow-moving health metric. func chronyTracking() chrony { b, err := os.ReadFile(chronyLogPath) if err != nil { diff --git a/files/stig/fleet-agent-chronylog.te b/files/stig/fleet-agent-chronylog.te index 1ed8ff4..0f0d494 100644 --- a/files/stig/fleet-agent-chronylog.te +++ b/files/stig/fleet-agent-chronylog.te @@ -18,10 +18,13 @@ module fleet_agent_chronylog 1.0.0; # # This grants ONLY read (open/read/getattr) on the chrony log type plus dir search # to reach the file — no write, no other chrony access, and NO domain transition -# (the agent reads a file; it does not exec chronyc). Minimal + auditable; -# justified in docs/stig-deviations.md #16. BENCH-VERIFY: the manager's Monitor -# chrony row populates on an enforcing boot; extend here only if a related chrony -# log AVC appears. Same policy-store-rebuild reproducibility caveat as #14/#15. +# (the agent reads a file; it does not exec chronyc). This is the SELinux (TE) +# half; the DAC half is the `chrony` supplementary group in fleet-agent.service, +# needed to traverse the 0750 /var/log/chrony dir — BOTH are required to read the +# log. Minimal + auditable; justified in docs/stig-deviations.md #16. BENCH-VERIFY: +# the manager's Monitor chrony row populates on an enforcing boot; extend here only +# if a related chrony log AVC appears. Same policy-store-rebuild reproducibility +# caveat as #14/#15. ######################################## require {