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
+63 -6
View File
@@ -164,6 +164,18 @@ variable "audit_host" {
default = "audit.fleet.lab"
}
variable "secure_ntp" {
type = bool
description = "true (default): SECURE, CA-AUTHENTICATED time via NTS (RFC 8915). The NM dispatcher derives the NTP server name at runtime as <ntp_prefix>.<DHCP-provided-domain> (NO FQDN baked — the domain can change without a rebuild) and chrony verifies the server's NTS-KE TLS cert against the fleet CA (/etc/dot1x/radius-ca.pem — same anchor as RADIUS/agent). A DHCP/NTP/DNS spoofer can then only DoS (cause no-sync), never shift the clock to resurrect expired/revoked certs (closes F-03/P-18; docs/secure-ntp-design.md). The serving NTP server needs a fleet-signed NTS cert with SAN <ntp_prefix>.<domain> (serving/chrony/nts-cert.sh). false: legacy UNAUTHENTICATED DHCP option-42 NTP (the dispatcher writes the handed IPs) — lab/interop only."
default = true
}
variable "ntp_prefix" {
type = string
description = "secure_ntp=true only: the label prepended to the DHCP-provided domain to form the NTS NTP server name (<ntp_prefix>.<domain>, e.g. ntp.fleet.lab). Deliberately NOT a full FQDN — only this label + the fleet CA are baked, so the environment's domain can change (new DHCP domain + reissued NTS cert) without rebuilding the image. Must match the serving NTS cert SAN and resolve via dnsmasq."
default = "ntp"
}
variable "sb_key" {
type = string
description = "Secure Boot signing key (PEM) for the UKI. Empty = unsigned lab build. Production: CI/HSM-held; never enters the build VM."
@@ -179,7 +191,7 @@ variable "sb_cert" {
variable "fleet_ca" {
type = string
description = "Path on the BUILD HOST to the fleet CA PEM baked to /etc/dot1x/radius-ca.pem — the trust anchor wpa_supplicant uses for RADIUS EAP-TLS, the fleet-agent uses to verify the fleet-manager, AND rsyslog uses to verify the audit collector (TLS offload, 2f). Defaults to the COMMITTED PLACEHOLDER (files/etc/dot1x/radius-ca.pem), which is NOT a usable CA (auth/telemetry/log-offload can't succeed with it). Override per-environment (CI secret -> temp file) with your real step-ca root (or a root+intermediate bundle for robustness). Never commit real internal CA material (CLAUDE.md)."
description = "Path on the BUILD HOST to the fleet CA PEM baked to /etc/dot1x/radius-ca.pem — the trust anchor wpa_supplicant uses for RADIUS EAP-TLS, the fleet-agent uses to verify the fleet-manager, rsyslog uses to verify the audit collector (TLS offload, 2f), AND chrony uses to verify the NTS time server (ntstrustedcerts, secure_ntp). Defaults to the COMMITTED PLACEHOLDER (files/etc/dot1x/radius-ca.pem), which is NOT a usable CA (auth/telemetry/log-offload/NTS can't succeed with it). Override per-environment (CI secret -> temp file) with your real step-ca root (or a root+intermediate bundle for robustness). Never commit real internal CA material (CLAUDE.md)."
default = "files/etc/dot1x/radius-ca.pem"
}
@@ -421,15 +433,51 @@ build {
# secure mode, reserving the physical NIC for wpa_supplicant.
"chmod 0755 /usr/lib/dracut/modules.d/95dot1x-macsec/*.sh",
# ---- NTP from DHCP (option 42) -> chrony, no public-pool egress --------
# NM dispatcher writes DHCP-provided NTP servers into chrony's sourcedir
# (replaces the removed dhclient.d/chrony.sh bridge). Strip the baked public
# pool so the host syncs ONLY from the lab NTP server handed via DHCP — the
# workstation never reaches *.pool.ntp.org.
# ---- NTP: CA-authenticated NTS (default) or legacy DHCP option 42 ------
# BOTH modes run through the SAME machinery: the NM dispatcher writes a
# source file into chrony's `sourcedir /run/chrony-dhcp`; the baked public
# pool is stripped so the host never egresses to *.pool.ntp.org.
# secure_ntp=true (NTS): the dispatcher derives the server NAME at runtime
# as <ntp_prefix>.<DHCP-provided-domain> and writes `server NAME iburst
# nts`. chrony verifies the NTS-KE TLS cert against the FLEET CA
# (ntstrustedcerts below). NOTHING host- or domain-specific is baked
# (only the `ntp` label + the CA) — the domain can change without a
# rebuild. A DHCP/NTP/DNS spoofer can only DoS; it can't present a
# fleet-CA-signed cert for the name it redirects to, so it can't shift
# the clock (F-03/P-18). No fallback to unauthenticated time.
# nocerttimecheck 1: a diskless host may boot with a bad/epoch clock (no
# reliable RTC), failing the cert validity window (chicken-and-egg);
# allow the FIRST measurement without the cert-time check to bootstrap,
# then enforce. Isolated LAN + one-measurement window bound the trade-off.
# secure_ntp=false (legacy): the dispatcher writes the DHCP option-42 IPs
# as plain `server <ip> iburst` — UNAUTHENTICATED, lab/interop only.
# The dispatcher is mode-aware via baked @NTP_MODE@/@NTP_PREFIX@ tokens.
"install -D -m0755 /tmp/etc-stage/NetworkManager/dispatcher.d/10-chrony-dhcp-ntp /etc/NetworkManager/dispatcher.d/10-chrony-dhcp-ntp",
"if [ \"${var.secure_ntp}\" = \"true\" ]; then NTP_MODE=nts; else NTP_MODE=dhcp; fi",
"sed -i \"s/@NTP_MODE@/$NTP_MODE/; s/@NTP_PREFIX@/${var.ntp_prefix}/\" /etc/NetworkManager/dispatcher.d/10-chrony-dhcp-ntp",
"restorecon /etc/NetworkManager/dispatcher.d/10-chrony-dhcp-ntp",
# Remove the chrony package's OWN NM dispatcher. NetworkManager merges
# dispatchers from /etc AND /usr/lib sorted by name, so /usr/lib/.../20-chrony-dhcp
# runs AFTER our 10-chrony-dhcp-ntp and writes the DHCP option-42 server to the
# SAME /run/chrony-dhcp/<iface>.sources — clobbering our NTS line and silently
# defeating authenticated time (bench 2026-07-11, comet-beacon-54: chrony fell
# back to plain NTP, authdata Mode '-', Cook 0; removing it → NTS Cook 8). Our
# 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}>.<dhcp-domain> (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",
# Enable chrony's tracking log so the fleet-agent can report clock health
# (source/stratum/offset) WITHOUT touching chronyd's command interface. That
# interface is unusable by the hardened agent: the unix socket is
@@ -1064,6 +1112,15 @@ build {
" echo '==> fapolicyd: watch_fs now includes overlay (guards the volatile-overlay root)'",
" fi",
"fi",
# secure_ntp fail-closed: STIG's chronyd_specify_remote_server remediation
# re-adds an UNAUTHENTICATED `server <pool> maxpoll 16` to chrony.conf AFTER
# the secure_ntp chrony step commented the baked servers out (2c runs later).
# That plain server is a silent fallback that defeats NTS's DoS-only guarantee
# (a DNS/NTP spoofer could shift the clock through it — the F-03 risk). When
# secure_ntp=true, re-strip any server/pool so ONLY the NTS sourcedir +
# ntstrustedcerts remain. The rules are also unselected in the tailoring; this
# is belt-and-suspenders. Bench 2026-07-11; docs/stig-deviations.md #17.
"if [ \"${var.secure_ntp}\" = \"true\" ] && [ -f /etc/chrony.conf ]; then echo '==> secure_ntp: re-stripping STIG-injected plain NTP servers from chrony.conf (fail-closed: NTS only)'; sed -i -E 's/^[[:space:]]*(pool|server)[[:space:]]/#&/' /etc/chrony.conf; fi",
]
}