From a46e5edf691c65eada1a82bfc06fe8cd6f193995 Mon Sep 17 00:00:00 2001 From: Audiobuzz Date: Fri, 10 Jul 2026 12:45:44 +0000 Subject: [PATCH] agent: add chrony supplementary group so it can read tracking.log (DAC) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The tracking.log read was still returning empty under enforcing with NO AVC — the tell of a DAC (not SELinux) denial. Root cause: /var/log/chrony is mode 0750 chrony:chrony (others have no access), so the agent's DynamicUser (group tss only) can't even traverse the directory to reach the 0644 tracking.log — EACCES on open, which produces no audit record. Manager confirmed genuinely on fleet-manager-1db3977 (chrony-capable), and uptime flowed while chrony didn't, proving the agent was sending empty chrony (failed read), not a manager gap. Earlier manual reads worked only as root (sudo) or because fleetadmin had been usermod'd into chrony during the socket test (reverted on reboot — /etc/group is on the volatile overlay). Fix: SupplementaryGroups=tss chrony on the unit — the chrony group has r-x on the 0750 dir so the agent can traverse it; the log file is world-readable. This is the DAC half; the fleet_agent_chronylog SELinux module (02a6621) is the TE half — BOTH are required (verified: group-only -> TE-denied; module-only -> DAC-denied silently). Corrected the now-stale 'no group' code comment; recorded the two-layer requirement in the .te and stig-deviations #16. Needs a workstation image rebuild + reboot. --- docs/stig-deviations.md | 9 +++++++++ files/agent/fleet-agent.service | 12 +++++++++--- files/agent/snapshot.go | 10 ++++++---- files/stig/fleet-agent-chronylog.te | 11 +++++++---- 4 files changed, 31 insertions(+), 11 deletions(-) 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 {