agent: query chrony over loopback UDP command socket (unix socket unusable under sandbox)

Root cause of the blank chrony fields (found on comet-beacon-54): chrony 4.x
(RHEL 9) defaults its command interface to the UNIX socket only
(/run/chrony/chronyd.sock), usable by root or the chrony group with a writable
/run/chrony. The fleet-agent unit is hardened — DynamicUser (group tss, not
chrony) + ProtectSystem=strict (/run read-only) — so it cannot open the unix
socket nor create chronyc's reply socket. Bench proof: normal-user 'chronyc
tracking' -> 506 Cannot talk to daemon; only sudo worked; 'chronyc -h 127.0.0.1'
failed and ss showed no UDP :323. So the probe always failed and every chrony_*
field stayed empty. SELinux permissive did NOT help — it was never (only) an
SELinux problem; the nnp_transition module (prev commit) is still needed for the
enforcing exec transition.

Fix: bind chronyd's command socket on 127.0.0.1 (bindcmdaddress in chrony.conf,
idempotent in the 2c provisioner) and point the probe at it (chronyc -n -h
127.0.0.1 tracking). UDP needs no fs write or group and rides the unit's
already-allowed AF_INET. Loopback + monitoring-only (state changes still need the
root unix socket), so exposure is minimal. Rejected the unix-socket route (chrony
group + ReadWritePaths=/run/chrony) because /run/chrony is 0750 (group has no
write). Documented in docs/stig-deviations.md #17; .te bench note updated for the
enforcing UDP path. Needs a workstation image rebuild + reboot.
This commit is contained in:
Audiobuzz
2026-07-10 09:02:05 +00:00
parent e6fd4a2dde
commit 394ad10824
4 changed files with 71 additions and 5 deletions
+42
View File
@@ -287,6 +287,48 @@ AVC under the agent's mount namespace (extend the `.te` if any appears); (d)
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.
**Not the whole story — see #17.** Permissive-mode bench (`comet-beacon-54`,
2026-07-10) showed the chrony fields STILL empty with SELinux permissive, proving
the module isn't the only blocker: chronyd's command interface was reachable only
via its **unix socket**, which the hardened agent can't use. #17 is the companion
fix; this module remains needed for the enforcing exec transition.
## 17. chronyd loopback UDP command socket for the fleet-agent probe (added 2026-07-10)
Not a STIG-rule waiver — a **one-line `chrony.conf` addition**
(`bindcmdaddress 127.0.0.1`) so the hardened `fleet-agent` can read clock health.
**Why:** the agent reports offset/source/stratum via `chronyc tracking`. On chrony
4.x (RHEL 9) the command interface defaults to the **unix socket only**
(`/run/chrony/chronyd.sock`), usable solely by root or the `chrony` group **and**
needing a writable `/run/chrony` for chronyc's reply socket. The agent unit is
hardened — `DynamicUser=yes` (ephemeral uid, group `tss` only, not `chrony`) and
`ProtectSystem=strict` (`/run` read-only) — so it cannot open the unix socket nor
create a reply socket. Bench (`comet-beacon-54`, 2026-07-10): as a normal user
`chronyc tracking` → `506 Cannot talk to daemon`; only `sudo chronyc` worked;
`chronyc -h 127.0.0.1` failed and `ss -lnup` showed **no UDP :323** — the loopback
command socket wasn't bound. Result: the probe always failed, every `chrony_*`
field stayed empty (Monitor chrony row `—`), and SELinux permissive did NOT help
(this is a sandbox/socket issue, orthogonal to #16).
**The change:** bind chronyd's command socket on `127.0.0.1` (UDP 323, loopback)
in the 2c provisioner (idempotent, next to the DHCP `sourcedir` edit); the agent
probe uses `chronyc -n -h 127.0.0.1 tracking`. UDP needs no filesystem write and no
group, and rides the unit's already-allowed `AF_INET` — the one transport that
works under the sandbox. The unix-socket alternative (add `chrony` group +
`ReadWritePaths=/run/chrony`) was rejected: `/run/chrony` is `0750` (group has no
write), so chronyc still couldn't create its reply socket.
**Scope / risk:** the command socket is bound **loopback-only** and is
**monitoring-only** — chronyd still requires the root unix socket for any state
change (`makestep`, `add server`, etc.), so a local caller can *read* clock status
but cannot alter time. No network exposure (127.0.0.1, not the fleet LAN). This is
also chrony's historical pre-4.x default. **BENCH-VERIFY:** (a) `ss -lnup | grep
:323` shows the loopback socket after boot; (b) the Monitor chrony row populates;
(c) under enforcing, no follow-on `chronyc_t → chronyd_t` UDP AVC (extend
`fleet-agent-chronyc.te` if so — see #16). Only applied when the NTP/DHCP provisioner
runs (always, independent of `enable_stig`).
## Things that are NOT deviations (kept / enforced)
- **SELinux enforcing** (`selinux --enforcing`; labels preserved into the erofs).