Files
Audiobuzz 5294860e74 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.
2026-07-12 02:53:05 +00:00

86 lines
4.1 KiB
Bash

#!/bin/bash
# NetworkManager dispatcher: point chrony at the fleet NTP server, learned from
# DHCP at runtime (no FQDN baked into the image). Two modes, baked at build time:
#
# nts (secure_ntp=true, default): AUTHENTICATED time. Derive the server NAME
# from the DHCP-provided domain as <PREFIX>.<domain> and write
# `server NAME iburst nts`. chrony verifies the NTS-KE TLS cert against the
# fleet CA (chrony.conf: ntstrustedcerts). Because the domain comes from
# DHCP, a domain change needs only a new DHCP domain + reissued NTS cert —
# no image rebuild. A DHCP/NTP/DNS spoofer can only DoS (no fleet-CA-signed
# cert for the name it redirects to => NTS fails => no sync); it cannot
# shift the clock. See docs/secure-ntp-design.md (F-03/P-18).
#
# dhcp (secure_ntp=false): LEGACY, UNAUTHENTICATED. Write the DHCP option-42
# server IPs as plain `server <ip> iburst`. Lab/interop only.
#
# We use NetworkManager's internal DHCP (the network-manager dracut port); NM
# exports the lease's domain as $DHCP4_DOMAIN_NAME (option 15) / $DHCP4_DOMAIN_SEARCH
# (option 119) and NTP servers as $DHCP4_NTP_SERVERS (option 42). chrony.conf
# carries `sourcedir /run/chrony-dhcp`, so writing <iface>.sources there and
# reloading makes chronyd use them. The baked public pool is stripped (provisioner).
#
# Installed 0755 root:root. Lenient on purpose: a hiccup here must never break
# networking. @NTP_MODE@/@NTP_PREFIX@ are substituted at build time.
#
# NB: the chrony package ships its OWN dispatcher at
# /usr/lib/NetworkManager/dispatcher.d/20-chrony-dhcp writing option-42 servers to
# the SAME <iface>.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. 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 = <PREFIX>.<dhcp-domain>
interface="$1"
action="$2"
case "$action" in
up|dhcp4-change|reapply) ;;
*) 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"
if [ "$MODE" = nts ]; then
# Prefer option 15 (domain-name); fall back to the first option-119 search
# domain; finally fall back to resolv.conf's search/domain. NM does not export
# $DHCP4_DOMAIN_NAME on every dispatcher event — at boot the source wasn't
# written because the firing event carried no DHCP domain in env, yet
# resolv.conf already had `search <domain>` (bench 2026-07-11). Reading it here
# makes the NTS source get written reliably regardless of which event fires.
domain="${DHCP4_DOMAIN_NAME:-}"
[ -z "$domain" ] && domain="${DHCP4_DOMAIN_SEARCH%% *}"
[ -z "$domain" ] && domain=$(awk '/^(search|domain)[[:space:]]/{print $2; exit}' /etc/resolv.conf 2>/dev/null)
if [ -n "$domain" ]; then
printf 'server %s.%s iburst nts\n' "$PREFIX" "$domain" > "$file"
# Pick up the new source without restarting chronyd.
chronyc reload sources >/dev/null 2>&1 || true
fi
else
if [ -n "${DHCP4_NTP_SERVERS:-}" ]; then
: > "$file"
for s in $DHCP4_NTP_SERVERS; do
printf 'server %s iburst\n' "$s" >> "$file"
done
chronyc reload sources >/dev/null 2>&1 || true
fi
fi
exit 0