agent: read chrony status from tracking.log instead of the command socket

Bench on comet-beacon-54 proved every command-socket route is blocked for the
hardened agent: chronyd's unix socket is owner-write-only (0755, so even a
chrony-group member can't connect), and STIG closes the UDP command port
(cmdport 0). Rather than re-open a port or expose chrony's privileged socket,
enable chronyd's tracking log and have the agent READ it — no port, no socket,
no group, no SELinux domain transition, full hardening intact.

- chrony.conf: enable 'log tracking' (idempotent) so chronyd appends
  source/stratum/offset to /var/log/chrony/tracking.log. Write volume is a ~130B
  line per clock update (backing off to ~maxpoll when stable) and capped by the
  chrony RPM's logrotate — negligible on the RAM overlay. cmdport 0 stays, so no
  STIG deviation.
- agent: chronyTracking() now reads the last data line of tracking.log via
  parseTrackingLog() (source f[2], stratum f[3], offset f[6], leap/sync f[7]);
  replaces the chronyc exec + parseChrony. Unit test swapped accordingly.
- revert the two abandoned approaches: drop the bindcmdaddress line (394ad10)
  and the fleet_agent_chronyc SELinux nnp_transition module + provisioner block
  (e6fd4a2, .te deleted) — no chronyc exec means no transition to allow. Removed
  stig-deviations #16 and #17 (both waivers no longer needed).

Needs a workstation image rebuild + reboot. BENCH-VERIFY: tracking.log is
world-readable so the DynamicUser agent can read it; under enforcing
unconfined_service_t can read chronyd_var_log_t (add a minimal file-read allow
if denied); and the offset sign matches chronyc on a synced host.
This commit is contained in:
Audiobuzz
2026-07-10 09:36:37 +00:00
parent 394ad10824
commit 4ca0b6dd06
5 changed files with 78 additions and 218 deletions
+11 -27
View File
@@ -405,17 +405,17 @@ build {
"restorecon /etc/NetworkManager/dispatcher.d/10-chrony-dhcp-ntp",
"sed -i -E 's/^[[:space:]]*(pool|server)[[:space:]]/#&/' /etc/chrony.conf",
"grep -q '^sourcedir /run/chrony-dhcp' /etc/chrony.conf || echo 'sourcedir /run/chrony-dhcp' >> /etc/chrony.conf",
# Bind chronyd's command socket on LOOPBACK UDP so the fleet-agent can query
# clock health (`chronyc -h 127.0.0.1 tracking`). chrony 4.x (RHEL 9) defaults
# the command interface to the UNIX socket /run/chrony/chronyd.sock ONLY
# (root or the chrony group + a writable /run/chrony for chronyc's reply
# socket). The agent runs hardened — DynamicUser (not in chrony group),
# ProtectSystem=strict (/run read-only) — so it can't use the unix socket; a
# loopback UDP command socket needs no fs write or group and rides the unit's
# already-allowed AF_INET. Localhost-only + monitoring-only (state changes
# still require the root unix socket), so the exposure is minimal. Documented
# in docs/stig-deviations.md #17.
"grep -q '^bindcmdaddress 127.0.0.1' /etc/chrony.conf || echo 'bindcmdaddress 127.0.0.1' >> /etc/chrony.conf",
# Enable chrony's tracking log so the fleet-agent can report clock health
# (source/stratum/offset) WITHOUT touching chronyd's command interface. That
# interface is unusable by the hardened agent: the unix socket is
# owner-write-only (0755) and the agent is DynamicUser + ProtectSystem=strict
# (/run read-only), and STIG closes the UDP command port (cmdport 0). Instead
# chronyd appends source/stratum/offset to /var/log/chrony/tracking.log, which
# the agent simply READS (world-readable; no port, no socket, no SELinux domain
# transition). Write volume is tiny (~one ~130B line per clock update, backing
# off to ~maxpoll when stable) and capped by the chrony RPM's logrotate; on the
# RAM overlay this is negligible. Keeps cmdport 0 intact — no STIG deviation.
"grep -qE '^log ([^#]*[[:space:]])?tracking' /etc/chrony.conf || echo 'log tracking' >> /etc/chrony.conf",
# Inject the fleet NIC drivers into the dracut config
"sed -i 's/@NIC_DRIVERS@/${var.nic_drivers}/' /etc/dracut.conf.d/90-diskless.conf",
@@ -910,22 +910,6 @@ 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)'",
]