Adding secure NTP

This commit is contained in:
Audiobuzz
2026-07-11 17:32:05 +09:30
parent 8325818c1b
commit fbb8100ecd
16 changed files with 929 additions and 71 deletions
+18 -3
View File
@@ -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
+52
View File
@@ -0,0 +1,52 @@
#!/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"; chmod 0640 "$OUT/nts.key"
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."
+4
View File
@@ -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