From 5294860e7484e4de6e06df7aa17436ba9defb21c Mon Sep 17 00:00:00 2001 From: Audiobuzz Date: Sun, 12 Jul 2026 02:53:05 +0000 Subject: [PATCH] secure-ntp: label /run/chrony-dhcp so chronyd can read the NTS source under SELinux Second bench defect after removing the distro 20-chrony-dhcp clobberer (secure-ntp-5a3eaed, comet-beacon-54): the dispatcher wrote the correct `server ntp.fleet.lab iburst nts` line, but chrony loaded ZERO sources -- `chronyc reload sources` returned 200 OK yet authdata/sources stayed empty. Root cause (AVC): chronyd_t was DENIED read on the .sources files because they and /run/chrony-dhcp itself were labeled generic var_run_t; chronyd_t may only read its own chronyd_var_run_t. The dispatcher's `mkdir -p` creates the dir as var_run_t and files inherit it. The distro 20-chrony-dhcp we removed had been restorecon-ing them to the readable label, so removing it exposed a latent labelling gap. Stock RHEL policy already maps /run/chrony-dhcp -> chronyd_var_run_t (a restorecon relabels it and chrony immediately reaches NTS Cook 8). Fix: ship /etc/tmpfiles.d/chrony-dhcp.conf so systemd-tmpfiles pre-creates the dir with that fcontext at sysinit, before any NM event -- the dispatcher's files then inherit chronyd_var_run_t and chronyd can read them. No SELinux allow rules needed (bench-verified: after the fix a real NM event wrote the source as chronyd_var_run_t, no AVC, authdata -> NTS Cook 8). Applies in both secure_ntp modes. Also: the dispatcher now skips `lo` -- it was writing a spurious lo.sources duplicate of the same server (the distro script had skipped loopback). BENCH-VERIFY on the next clean build: a normal boot leaves the source file labeled chronyd_var_run_t (ls -Z), no lo.sources, and authdata reaches NTS/cookies>0 with no manual step. --- CLAUDE.md | 9 +++++- diskless.pkr.hcl | 10 +++++++ docs/secure-ntp-design.md | 29 ++++++++++++++++--- .../dispatcher.d/10-chrony-dhcp-ntp | 15 +++++++++- files/etc/tmpfiles.d/chrony-dhcp.conf | 22 ++++++++++++++ 5 files changed, 79 insertions(+), 6 deletions(-) create mode 100644 files/etc/tmpfiles.d/chrony-dhcp.conf diff --git a/CLAUDE.md b/CLAUDE.md index e375d40..604a838 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -282,7 +282,14 @@ instead of working around it. line in the shared `/run/chrony-dhcp/.sources` with the plain option-42 server — silently defeating NTS (authdata Mode `-`, Cook 0). The provisioner now `rm`s it (NTS then reached Cook 8 on the bench). Don't re-add a competing writer to - that file. **Toggle:** `secure_ntp=false` reverts the dispatcher to the legacy + that file. **Second bench finding (2026-07-12, FIXED):** removing that distro script + also removed the `restorecon` that had been labelling the sources — the dispatcher's + `mkdir -p` leaves `/run/chrony-dhcp` + files as generic `var_run_t`, which + `chronyd_t` is SELinux-DENIED reading (loads 0 sources, `authdata` empty). Fixed by + `/etc/tmpfiles.d/chrony-dhcp.conf` pre-creating the dir with the stock + `chronyd_var_run_t` fcontext at sysinit, so the dispatcher's files inherit a readable + label (no allow rules). The dispatcher also now skips `lo` (was writing a spurious + duplicate). **Toggle:** `secure_ntp=false` reverts the dispatcher to the legacy unauthenticated **DHCP option 42** path (plain `server iburst`). - `fleet` Plymouth theme: confirm (a) the script plugin package actually installed (`/usr/lib64/plymouth/script.so` — else the build fell back to diff --git a/diskless.pkr.hcl b/diskless.pkr.hcl index 0074210..48b1c11 100644 --- a/diskless.pkr.hcl +++ b/diskless.pkr.hcl @@ -457,6 +457,16 @@ build { # dispatcher supersedes it in BOTH modes, so drop it unconditionally. Leave # 20-chrony-onoffline (only marks sources online/offline — harmless). "rm -f /usr/lib/NetworkManager/dispatcher.d/20-chrony-dhcp", + # ...but the distro script was ALSO (accidentally) labelling /run/chrony-dhcp so + # chronyd could read the sources. Removing it exposed that gap: our dispatcher's + # `mkdir -p` makes the dir generic var_run_t and the source files inherit it, and + # chronyd_t (which only reads chronyd_var_run_t) is SELinux-DENIED reading them → + # NTS silently loads 0 sources (bench 2026-07-12, comet-beacon-54). This tmpfiles.d + # entry pre-creates /run/chrony-dhcp at sysinit with the stock chronyd_var_run_t + # fcontext (before any NM event), so the dispatcher's files inherit a readable + # label — no SELinux allow rules needed. Applies in BOTH modes. + "install -D -m0644 /tmp/etc-stage/tmpfiles.d/chrony-dhcp.conf /etc/tmpfiles.d/chrony-dhcp.conf", + "restorecon /etc/tmpfiles.d/chrony-dhcp.conf", "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", "if [ \"${var.secure_ntp}\" = \"true\" ]; then echo '==> secure NTP: NTS via <${var.ntp_prefix}>. (fleet-CA-authenticated)'; { printf '\\n# Secure NTP (NTS, RFC 8915) — verify the server against the fleet CA. docs/secure-ntp-design.md\\n'; printf 'ntstrustedcerts /etc/dot1x/radius-ca.pem\\n'; printf 'nocerttimecheck 1\\n'; } >> /etc/chrony.conf; else echo '==> legacy NTP: unauthenticated DHCP option-42'; fi", diff --git a/docs/secure-ntp-design.md b/docs/secure-ntp-design.md index 7189373..0eb9464 100644 --- a/docs/secure-ntp-design.md +++ b/docs/secure-ntp-design.md @@ -146,10 +146,31 @@ and tcp/4460 were **all already correct**; the collision was the sole defect. Fix: the provisioner removes `/usr/lib/NetworkManager/dispatcher.d/20-chrony-dhcp` at build time (our dispatcher supersedes it in both modes); `20-chrony-onoffline` -is left in place (harmless — it only marks sources online/offline). **BENCH-VERIFY -on the next clean build:** with the distro script gone, a normal boot leaves -`/run/chrony-dhcp/.sources` = the NTS line and `chronyc authdata` reaches -`NTS`/cookies > 0 with no manual intervention. +is left in place (harmless — it only marks sources online/offline). + +**2026-07-12 (comet-beacon-54, `secure-ntp-5a3eaed`) — SELinux label on the source +files (FIXED).** With the clobberer gone the dispatcher wrote the correct +`server ntp.fleet.lab iburst nts` line, but chrony still loaded **zero** sources — +`chronyc reload sources` returned `200 OK` yet `authdata`/`sources` stayed empty. +Root cause (AVC): `chronyd_t` was **denied read** on the `.sources` files because +they (and `/run/chrony-dhcp` itself) were labeled generic **`var_run_t`** — +`chronyd_t` may only read its own **`chronyd_var_run_t`**. The dispatcher's +`mkdir -p` creates the dir as `var_run_t` and files inherit it; the distro +`20-chrony-dhcp` we removed had been the thing `restorecon`-ing them to the readable +label, so removing it exposed a latent labelling gap. Stock RHEL policy already maps +`/run/chrony-dhcp` → `chronyd_var_run_t` (a `restorecon -R` relabels it and chrony +then reaches `NTS Cook 8`). Fix: ship `/etc/tmpfiles.d/chrony-dhcp.conf` +(`d /run/chrony-dhcp …`) so systemd-tmpfiles pre-creates the dir with the correct +fcontext at sysinit — files the dispatcher writes then **inherit** `chronyd_var_run_t` +(bench-verified: after the fix a real NM event wrote `enp0s31f6.sources` as +`chronyd_var_run_t`, no AVC, `authdata` → `NTS Cook 8`). No SELinux allow rules +needed. Also fixed: the dispatcher now **skips `lo`** (it was writing a spurious +`lo.sources` duplicate of the same server, which the distro script had skipped). + +**BENCH-VERIFY on the next clean build:** a normal boot leaves +`/run/chrony-dhcp/.sources` = the NTS line **labeled `chronyd_var_run_t`** +(`ls -Z`), no `lo.sources`, and `chronyc -N authdata` reaches `NTS`/cookies > 0 with +**no manual intervention**. ## Toggle diff --git a/files/etc/NetworkManager/dispatcher.d/10-chrony-dhcp-ntp b/files/etc/NetworkManager/dispatcher.d/10-chrony-dhcp-ntp index 71c55b8..451e8c1 100644 --- a/files/etc/NetworkManager/dispatcher.d/10-chrony-dhcp-ntp +++ b/files/etc/NetworkManager/dispatcher.d/10-chrony-dhcp-ntp @@ -27,7 +27,11 @@ # /usr/lib/NetworkManager/dispatcher.d/20-chrony-dhcp writing option-42 servers to # the SAME .sources. NM merges /etc + /usr/lib sorted by name, so it runs # AFTER us and would clobber the NTS line — the provisioner removes it (it defeated -# NTS on the bench: chrony fell back to plain NTP). This script supersedes it. +# NTS on the bench: chrony fell back to plain NTP). This script supersedes it. It +# was ALSO (accidentally) labelling the sources chronyd_var_run_t so chronyd could +# read them; we restore that via /etc/tmpfiles.d/chrony-dhcp.conf (see the dir note +# below) — without it, under enforcing SELinux chronyd can't read the sources and +# loads nothing (bench 2026-07-12). MODE="@NTP_MODE@" # nts | dhcp PREFIX="@NTP_PREFIX@" # nts: server name = . @@ -40,6 +44,15 @@ case "$action" in *) exit 0 ;; esac +# Loopback carries no DHCP NTP config; acting on it just writes a spurious +# lo.sources duplicate of the same server (the distro 20-chrony-dhcp skipped lo — +# we must too, or chrony sees two files for one server). Skip it. +[ "$interface" = lo ] && exit 0 + +# /run/chrony-dhcp is pre-created with the chronyd_var_run_t SELinux label by +# /etc/tmpfiles.d/chrony-dhcp.conf at boot, so the files we write below inherit a +# label chronyd_t can READ (else, under enforcing, chronyd is denied and loads no +# sources — bench 2026-07-12). This mkdir is just a fallback if tmpfiles didn't run. dir=/run/chrony-dhcp file="$dir/${interface}.sources" mkdir -p "$dir" diff --git a/files/etc/tmpfiles.d/chrony-dhcp.conf b/files/etc/tmpfiles.d/chrony-dhcp.conf new file mode 100644 index 0000000..2e5ba34 --- /dev/null +++ b/files/etc/tmpfiles.d/chrony-dhcp.conf @@ -0,0 +1,22 @@ +# Pre-create chrony's runtime NTP source directory with the CORRECT SELinux label +# at boot, before any NetworkManager event fires. The NM dispatcher +# (10-chrony-dhcp-ntp) writes .sources here for chronyd's +# `sourcedir /run/chrony-dhcp`. +# +# Why this exists: the dispatcher's own `mkdir -p /run/chrony-dhcp` creates the dir +# as generic `var_run_t`, and the source files inherit it — but `chronyd_t` is only +# allowed to read `chronyd_var_run_t`, so under enforcing SELinux chronyd is DENIED +# reading them and silently loads ZERO sources (bench 2026-07-12, comet-beacon-54: +# NTS never activated — `chronyc authdata` empty; AVC `denied { read } … chronyd_t +# … var_run_t`). The stock RHEL policy already maps /run/chrony-dhcp → +# `chronyd_var_run_t`, and systemd-tmpfiles applies that fcontext when it creates +# the dir here at sysinit (well before the dispatcher runs). Files the dispatcher +# then creates inside inherit `chronyd_var_run_t`, so chronyd can read them — no +# SELinux allow rules needed (verified on the bench). +# +# The distro chrony NM dispatcher (/usr/lib/.../20-chrony-dhcp) used to apply this +# label via a per-file restorecon; we removed it because it clobbered our NTS source +# (see diskless.pkr.hcl + the 10-chrony-dhcp-ntp header), so we restore the labelling +# here instead. Needed in BOTH secure_ntp modes (chronyd must read the sourcedir +# either way). +d /run/chrony-dhcp 0755 root root - -