Merge remote-tracking branch 'origin/main' into poam-hardening

# Conflicts:
#	CLAUDE.md
This commit is contained in:
Audiobuzz
2026-07-12 05:54:41 +00:00
18 changed files with 1277 additions and 84 deletions
@@ -1,16 +1,40 @@
#!/bin/bash
# NetworkManager dispatcher: feed DHCP option 42 (NTP servers) into chrony.
# 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:
#
# Replaces the old dhclient.d/chrony.sh bridge — we use NetworkManager's internal
# DHCP now (the network-manager dracut port), and NM exports the lease's NTP
# servers to dispatcher scripts as $DHCP4_NTP_SERVERS. chrony.conf carries
# `sourcedir /run/chrony-dhcp`, so writing <iface>.sources there and reloading
# makes chronyd use the DHCP-provided server. The image's baked public pool is
# stripped (see the provisioner), so the host syncs ONLY from the lab NTP server
# — no time egress from the workstation.
# 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).
#
# Installed 0755 root:root at /etc/NetworkManager/dispatcher.d/. Lenient on
# purpose: a hiccup here must never break networking.
# 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"
@@ -20,17 +44,42 @@ 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"
if [ -n "${DHCP4_NTP_SERVERS:-}" ]; then
: > "$file"
for s in $DHCP4_NTP_SERVERS; do
printf 'server %s iburst\n' "$s" >> "$file"
done
# Pick up the new sources without restarting chronyd.
chronyc reload sources >/dev/null 2>&1 || true
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
+22
View File
@@ -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 <iface>.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 - -
+15
View File
@@ -171,5 +171,20 @@
<xccdf:select idref="xccdf_org.ssgproject.content_rule_disable_ctrlaltdel_reboot" selected="false"/>
<xccdf:select idref="xccdf_org.ssgproject.content_rule_disable_ctrlaltdel_burstaction" selected="false"/>
<!-- NTP time source (secure_ntp): the fleet configures chrony's time source
DYNAMICALLY at runtime — a fleet-CA-authenticated NTS server derived from
the DHCP domain (secure_ntp=true) or DHCP option-42 (false) — via the NM
dispatcher + `sourcedir /run/chrony-dhcp`, NOT a static baked `server`
line. These rules remediate by INJECTING an unauthenticated
`server <pool> maxpoll 16` into chrony.conf; under secure_ntp that plain
server is a silent fallback that DEFEATS the NTS DoS-only guarantee (a
DNS/NTP spoofer could shift the clock through it — the exact F-03 risk).
Unselect so remediation adds no plain server; 2c-STIG-postfix also
re-strips any server/pool when secure_ntp=true (belt-and-suspenders).
The STIG intent (time IS synchronised, authenticated under NTS) is met.
Justified in docs/stig-deviations.md #17. -->
<xccdf:select idref="xccdf_org.ssgproject.content_rule_chronyd_specify_remote_server" selected="false"/>
<xccdf:select idref="xccdf_org.ssgproject.content_rule_chronyd_or_ntpd_set_maxpoll" selected="false"/>
</xccdf:Profile>
</xccdf:Tailoring>