Merge remote-tracking branch 'origin/main' into poam-hardening
# Conflicts: # CLAUDE.md
This commit is contained in:
@@ -1,7 +1,8 @@
|
||||
# Lab NTP server (serving stack). Syncs from upstream and serves the boot LAN so
|
||||
# diskless workstations get time INTERNALLY (handed out as DHCP option 42 by
|
||||
# dnsmasq) instead of egressing to the public pool. Only this host reaches the
|
||||
# internet for time; the workstations never do.
|
||||
# diskless workstations get time INTERNALLY instead of egressing to the public
|
||||
# pool. Only this host reaches the internet for time; the workstations never do.
|
||||
# With secure_ntp=true the workstations use NTS (below) and authenticate this
|
||||
# server's identity before trusting its time.
|
||||
|
||||
# Upstream time source (this serving host has internet; the workstations don't).
|
||||
pool pool.ntp.org iburst
|
||||
@@ -10,6 +11,20 @@ pool pool.ntp.org iburst
|
||||
# to your subnet (e.g. `allow 192.168.50.0/24`) for anything less trusted.
|
||||
allow all
|
||||
|
||||
# ---- NTS (RFC 8915): CA-AUTHENTICATED time for the fleet -------------------
|
||||
# The workstations verify THIS server's identity before trusting its time
|
||||
# (secure_ntp=true in the image; closes F-03/P-18). The NTS-KE TLS cert is signed
|
||||
# by the FLEET CA (step-ca), so it chains to the same radius-ca.pem the hosts
|
||||
# already trust; its SAN must be the name the hosts use (ntp_server / NTP_HOST,
|
||||
# default ntp.fleet.lab). Issue it with ./nts-cert.sh (mounted at /etc/chrony/nts).
|
||||
# Without these files the server still answers plain NTP, but NTS clients get an
|
||||
# "NTS-KE handshake" failure. NTS-KE listens on tcp/4460 — open it on the boot-LAN
|
||||
# NIC (see docker-compose.yml), same trap as 123/udp. docs/secure-ntp-design.md.
|
||||
ntsservercert /etc/chrony/nts/nts.crt
|
||||
ntsserverkey /etc/chrony/nts/nts.key
|
||||
# Persist NTS server keys/cookies across restarts (best-effort on the container fs).
|
||||
ntsdumpdir /var/lib/chrony
|
||||
|
||||
# Serve our own clock at a high stratum if upstream is unreachable, so a diskless
|
||||
# boot never blocks waiting for time.
|
||||
local stratum 10
|
||||
|
||||
Executable
+73
@@ -0,0 +1,73 @@
|
||||
#!/usr/bin/env bash
|
||||
# nts-cert.sh — issue the NTS-KE server certificate for the fleet NTP server,
|
||||
# signed by the fleet step-ca so booted hosts (secure_ntp=true) authenticate it
|
||||
# against the SAME radius-ca.pem they already trust (closes F-03/P-18). Run once
|
||||
# after the CA is up (serving/dot1x-bench bootstrap.sh), from serving/chrony/.
|
||||
# Writes ./nts/{nts.crt,nts.key}, which docker-compose mounts at /etc/chrony/nts.
|
||||
# Pairs with docs/secure-ntp-design.md. BENCH-VERIFY the `step` flags vs your version.
|
||||
set -euo pipefail
|
||||
|
||||
cd "$(dirname "$0")"
|
||||
OUT="nts"
|
||||
# step-ca lives in the dot1x-bench compose project; reach it there (same pattern
|
||||
# as dot1x-bench/step-ca/bootstrap.sh issuing the RADIUS cert).
|
||||
BENCH="../dot1x-bench"
|
||||
[ -d "$BENCH" ] || { echo "ERROR: $BENCH not found (need the step-ca compose project)." >&2; exit 1; }
|
||||
[ -f "$BENCH/.env" ] && . "$BENCH/.env"
|
||||
CA_PORT=${CA_PORT:-8443}
|
||||
CA_CONNECT=${CA_CONNECT:-localhost}
|
||||
CA_URL="https://${CA_CONNECT}:${CA_PORT}"
|
||||
PROVISIONER=${PROVISIONER:-enrol}
|
||||
# The DNS SAN the booted host uses. The image derives it at runtime as
|
||||
# <ntp_prefix>.<DHCP-domain> (default ntp.<domain>), so this MUST equal
|
||||
# ntp.${BOOT_DOMAIN} and resolve via the fleet dnsmasq (NTP_HOST -> SERVER_IP).
|
||||
NTP_SAN=${NTP_HOST:-ntp.fleet.lab}
|
||||
|
||||
PKI="$BENCH/pki"
|
||||
PW="$PKI/stepca/password"
|
||||
[ -e "$PW" ] || { [ -e "$PKI/stepca/secrets/password" ] && PW="$PKI/stepca/secrets/password"; }
|
||||
[ -r "$PW" ] || { echo "ERROR: CA password $PW missing — run dot1x-bench bootstrap.sh first." >&2; exit 1; }
|
||||
CONTAINER_PW="/home/step/${PW#"$PKI/stepca/"}"
|
||||
|
||||
incca() { ( cd "$BENCH" && docker compose exec -T step-ca "$@" ); }
|
||||
|
||||
echo "==> issuing NTS-KE server cert (CN/SAN ${NTP_SAN}) from the fleet CA"
|
||||
incca mkdir -p /export/nts
|
||||
# Leaf with serverAuth (default) + the NTS server name as SAN. --not-after within
|
||||
# the CA's max claim (bootstrap.sh raised it to 2160h).
|
||||
incca step ca certificate "${NTP_SAN}" \
|
||||
/export/nts/nts.crt /export/nts/nts.key \
|
||||
--ca-url "$CA_URL" --root /export/root_ca.crt \
|
||||
--provisioner "${PROVISIONER}" --provisioner-password-file "$CONTAINER_PW" \
|
||||
--not-after 2160h --force
|
||||
|
||||
# The CA wrote into $BENCH/pki/nts via the /export mount; copy to ./nts for the
|
||||
# ntp container's bind mount.
|
||||
mkdir -p "$OUT"
|
||||
cp "$PKI/nts/nts.crt" "$OUT/nts.crt"
|
||||
cp "$PKI/nts/nts.key" "$OUT/nts.key"
|
||||
chmod 0644 "$OUT/nts.crt"
|
||||
|
||||
# The ntp container's chronyd runs as a non-root 'chrony' user (post-PRIVDROP) —
|
||||
# NOT the host user that wrote this key. So `0640` owned by the host user is
|
||||
# UNREADABLE inside the container ("Missing read access ... : Permission denied" ->
|
||||
# "Could not set credentials"), and every NTS-KE TLS handshake then fails with
|
||||
# "The TLS connection was non-properly terminated" (bench 2026-07-11 on
|
||||
# comet-beacon-54). `0644` works but chrony warns "World-readable permissions". The
|
||||
# right answer: chown the key to the container's chrony uid + keep 0640. The ntp
|
||||
# service is in the PARENT compose project (serving/), so query it from there.
|
||||
chmod 0640 "$OUT/nts.key"
|
||||
NTP_DIR="$(cd .. 2>/dev/null && pwd)"
|
||||
CHRONY_UID="$(cd "$NTP_DIR" 2>/dev/null && docker compose exec -T ntp sh -c 'awk -F: "/chrony/{print \$3; exit}" /etc/passwd' 2>/dev/null | tr -d '\r ' || true)"
|
||||
if [ -n "$CHRONY_UID" ]; then
|
||||
chown "$CHRONY_UID" "$OUT/nts.key" 2>/dev/null || sudo chown "$CHRONY_UID" "$OUT/nts.key"
|
||||
echo " NTS key owned by the ntp container's chrony uid ($CHRONY_UID), mode 0640."
|
||||
else
|
||||
chmod 0644 "$OUT/nts.key"
|
||||
echo " WARNING: couldn't determine the ntp container's chrony uid (is the ntp"
|
||||
echo " service running?). Left the key 0644 (world-readable) so chrony"
|
||||
echo " can read it — chown it to the chrony uid + 0640 for production."
|
||||
fi
|
||||
echo "==> wrote $OUT/nts.crt + $OUT/nts.key"
|
||||
echo " Ensure the image's ntp_server = ${NTP_SAN}, dnsmasq resolves ${NTP_SAN},"
|
||||
echo " open tcp/4460 (NTS-KE) on the boot NIC, and restart the ntp service."
|
||||
@@ -0,0 +1,4 @@
|
||||
# NTS-KE server cert/key (fleet-signed, per-environment) — issued by ../nts-cert.sh.
|
||||
# Never commit key material or environment CA-signed certs (see CLAUDE.md invariant).
|
||||
nts.crt
|
||||
nts.key
|
||||
@@ -91,6 +91,11 @@ AUDIT_HOST=audit.fleet.lab
|
||||
# *.fleet.lab like the boot/nvme/audit names. Must match the agent server cert SAN
|
||||
# (server-pki.sh AGENT_SANS).
|
||||
MGMT_HOST=manager.fleet.lab
|
||||
# NTS-authenticated NTP server name (secure_ntp). Baked into the image's
|
||||
# chrony.conf (Packer var ntp_server) so every host syncs authenticated time here;
|
||||
# resolved per-environment to this serving host. MUST match the NTS server cert SAN
|
||||
# issued by serving/chrony/nts-cert.sh.
|
||||
NTP_HOST=ntp.fleet.lab
|
||||
|
||||
# --- NVMe/TCP target -------------------------------------------------------
|
||||
# Per-version subsystem NQN base. The long-lived target serves each rootfs
|
||||
|
||||
@@ -108,6 +108,9 @@ services:
|
||||
# Fleet-manager AGENT plane (:9600) — baked into the image so enrolled hosts
|
||||
# dial the control plane by name; resolved here like the other fleet names.
|
||||
- --address=/${MGMT_HOST}/${SERVER_IP}
|
||||
# NTS-authenticated NTP server name (secure_ntp): baked into the image's
|
||||
# chrony.conf; must match the NTS server cert SAN. Resolved to this host.
|
||||
- --address=/${NTP_HOST}/${SERVER_IP}
|
||||
# Kiosk is LOCAL-ONLY (loads boot.fleet.lab), so sinkhole Chromium's
|
||||
# call-home to 0.0.0.0 — telemetry can't leave the lab even if the browser
|
||||
# tries. Defense-in-depth behind the in-image Chromium policy/flags; in
|
||||
@@ -122,9 +125,14 @@ services:
|
||||
- --dhcp-range=${DHCP_RANGE_START},${DHCP_RANGE_END},${DHCP_LEASE}
|
||||
- --dhcp-option=option:router,${GATEWAY_IP}
|
||||
- --dhcp-option=option:dns-server,${SERVER_IP}
|
||||
# NTP server (option 42): point diskless hosts at the lab 'ntp' service
|
||||
# below so they sync time internally and never reach the public NTP pool.
|
||||
# The image's NM dispatcher (10-chrony-dhcp-ntp) feeds this into chrony.
|
||||
# Domain name (option 15): the image derives its NTS NTP server name at
|
||||
# runtime as ntp.<this-domain> (secure_ntp — no baked FQDN), so hosts MUST
|
||||
# receive the domain. NM exports it to the dispatcher as $DHCP4_DOMAIN_NAME.
|
||||
# Explicit (not just --domain) so the option is reliably sent + matches the
|
||||
# NTS cert SAN (ntp.${BOOT_DOMAIN}) and the dnsmasq --address for NTP_HOST.
|
||||
- --dhcp-option=option:domain-name,${BOOT_DOMAIN}
|
||||
# NTP server (option 42): used only in the LEGACY unauthenticated path
|
||||
# (secure_ntp=false). With NTS the image ignores this and dials ntp.<domain>.
|
||||
- --dhcp-option=option:ntp-server,${SERVER_IP}
|
||||
# ---- Per-host names (creative starship hostnames, server-assigned) ----
|
||||
# The workstation image is hostname-neutral; naming is done HERE. fleet-namer
|
||||
@@ -220,15 +228,20 @@ services:
|
||||
environment:
|
||||
HASS_URL: ${HASS_URL}
|
||||
HASS_TOKEN: ${HASS_TOKEN}
|
||||
# --- NTP server (serves time to the boot LAN; handed out via DHCP opt 42) ---
|
||||
# --- NTP server (serves time to the boot LAN; NTS-authenticated for secure_ntp) ---
|
||||
ntp:
|
||||
build: ./chrony
|
||||
network_mode: host # serve NTP on udp/123 across the boot LAN
|
||||
network_mode: host # serve NTP on udp/123 (+ NTS-KE tcp/4460) across the boot LAN
|
||||
cap_add:
|
||||
- SYS_TIME # discipline the (host-shared) clock it serves
|
||||
restart: unless-stopped
|
||||
volumes:
|
||||
- ./chrony/chrony.conf:/etc/chrony/chrony.conf:ro
|
||||
# NTS-KE server cert/key (fleet-signed) so booted hosts CA-AUTHENTICATE the
|
||||
# time source (secure_ntp; F-03/P-18). Issue with ./chrony/nts-cert.sh.
|
||||
# If the dir is empty the server still answers plain NTP; NTS clients then
|
||||
# fail the KE handshake — so run nts-cert.sh before shipping secure_ntp images.
|
||||
- ./chrony/nts:/etc/chrony/nts:ro
|
||||
# NOTE: with host networking + SYS_TIME this chronyd disciplines the Docker
|
||||
# HOST clock (the clock isn't namespaced) — intended: the serving host is the
|
||||
# lab time source. Drop SYS_TIME (and add `chronyd -x`) if the host already
|
||||
@@ -236,11 +249,14 @@ services:
|
||||
#
|
||||
# FIREWALL (easy to miss — same trap as the audit collector's 514/tcp): host
|
||||
# networking means Docker does NOT open the port; the serving host's own
|
||||
# firewall governs 123/udp. Without a rule, workstations get 192.168.50.10 as
|
||||
# their NTP server (DHCP opt 42) but their packets are dropped — chrony logs
|
||||
# "Added source … / unreachable" and never syncs. Open it on the boot-LAN NIC:
|
||||
# firewall governs the NTP ports. Without a rule, workstations get 192.168.50.10
|
||||
# as their NTP server but their packets are dropped — chrony logs
|
||||
# "Added source … / unreachable" and never syncs. Open BOTH on the boot-LAN NIC:
|
||||
# udp/123 = NTP; tcp/4460 = NTS-KE (secure_ntp key establishment)
|
||||
# ufw: sudo ufw allow in on <boot-nic> to any port 123 proto udp comment 'fleet NTP'
|
||||
# firewalld: sudo firewall-cmd --permanent --add-service=ntp && sudo firewall-cmd --reload
|
||||
# sudo ufw allow in on <boot-nic> to any port 4460 proto tcp comment 'fleet NTS-KE'
|
||||
# firewalld: sudo firewall-cmd --permanent --add-service=ntp \
|
||||
# && sudo firewall-cmd --permanent --add-port=4460/tcp && sudo firewall-cmd --reload
|
||||
|
||||
# --- Remote audit / log collector (RECEIVE side) ---------------------------
|
||||
# rsyslog receiver: booted diskless hosts (whose RAM-only overlay root loses
|
||||
|
||||
Reference in New Issue
Block a user