NTS was inactive on the clean-build deploy (secure-ntp-d1621e9, comet-beacon-54): chronyc authdata showed a plain IP source in Mode '-', Cook 0, with no NTS-KE attempt at all -- chrony was on plain NTP. Root cause: NetworkManager runs dispatcher scripts from BOTH /etc/NetworkManager/dispatcher.d and /usr/lib/NetworkManager/dispatcher.d, merged and sorted by filename. The chrony RPM ships /usr/lib/NetworkManager/dispatcher.d/20-chrony-dhcp, which writes the DHCP option-42 server to the SAME /run/chrony-dhcp/<iface>.sources file our 10-chrony-dhcp-ntp uses. Sorting after 10-, it ran last and overwrote our `server ntp.fleet.lab iburst nts` line with `server 192.168.50.10 iburst`, then reloaded chrony -- last writer wins on the shared filename, silently defeating authenticated time. Isolation proof: removing the distro script + hand-writing the NTS source brought NTS up immediately (ntp.fleet.lab NTS, Cook 8, zero NAKs), confirming the host image, the fleet-signed NTS-KE cert, DNS, 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 (our dispatcher supersedes it in both modes); 20-chrony-onoffline is left in place (harmless). Documents the finding in the dispatcher header, docs/secure-ntp-design.md, and the CLAUDE.md secure_ntp note. BENCH-VERIFY on the next clean build: a normal boot reaches NTS/cookies>0 with no manual step.
73 lines
3.3 KiB
Bash
73 lines
3.3 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.
|
|
|
|
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
|
|
|
|
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
|