Files
diskless-workstation/docs/secure-ntp-design.md
T
2026-07-11 17:32:05 +09:30

6.8 KiB

Secure NTP design — CA-authenticated time via NTS (F-03 / P-18)

Status: implemented (host + serving scaffold), BENCH-VERIFY. Branch secure-ntp. Closes SAR finding F-03 / POA&M P-18.

Problem

The host clock is an input to every certificate-validity decision (EAP-TLS to RADIUS, agent mTLS, the SSH host-cert flow, future log TLS). Today chrony syncs from a DHCP-handed NTP server (option 42) with no authentication. An attacker on the pre-auth segment who can spoof DHCP or NTP can shift the clock — e.g. forward it to make an expired/revoked certificate look valid. The isolated LAN and the cert-serial inventory gate are only partial compensations.

Approach — NTS (RFC 8915), CA-signed server authentication

NTS is the modern, CA-based way to authenticate an NTP server. The server runs an NTS-KE (key-establishment) endpoint over TLS 1.3 on tcp/4460; the client verifies the server's TLS certificate against a trust anchor, then uses the negotiated keys to authenticate the NTP (udp/123) packets that carry the time.

This maps onto the fleet's existing PKI: the NTS-KE cert is issued by the fleet step-ca, so it chains to the same radius-ca.pem the hosts already trust. No new trust anchor.

Symmetric-key NTP and Autokey were rejected: neither is CA-based (symmetric needs per-server shared secrets; Autokey is deprecated and broken).

No baked FQDN — the server name is derived from the DHCP domain

NTS validates the server's TLS cert against the name/address the client used. We deliberately do not bake an NTP FQDN into the image (a site's domain may change and we don't want to rebuild images for that). Instead, at runtime the NM dispatcher takes the DHCP-provided domain ($DHCP4_DOMAIN_NAME, option 15; falls back to the first $DHCP4_DOMAIN_SEARCH, option 119) and forms:

    server <ntp_prefix>.<dhcp-domain> iburst nts      # e.g. ntp.fleet.lab

Only the label ntp_prefix (default ntp) and the fleet CA are baked — both stable. Change the site domain? Hand a new DHCP domain and reissue the NTS cert with the new SAN; no image rebuild. The cert uses a clean DNS SAN (not an IP SAN, which deriving from DHCP option-42 IPs would have forced).

Architecture

 serving 'ntp' (chrony):
   pool pool.ntp.org iburst              # upstream (serving host only)
   allow all                            # serve the boot LAN
   ntsservercert /etc/chrony/nts/nts.crt   # FLEET-signed, SAN = ntp.<domain>
   ntsserverkey  /etc/chrony/nts/nts.key
        ▲  tcp/4460 NTS-KE (TLS 1.3)  +  udp/123 authenticated NTP
        │
 dnsmasq: option 15 domain-name = <domain>;  A ntp.<domain> -> serving host
        │
 host (secure_ntp=true):
   NM dispatcher (10-chrony-dhcp-ntp, mode=nts):
     domain=$DHCP4_DOMAIN_NAME  ->  /run/chrony-dhcp/<if>.sources:
        server ntp.<domain> iburst nts
   chrony.conf:
     sourcedir /run/chrony-dhcp
     ntstrustedcerts /etc/dot1x/radius-ca.pem   # the fleet CA verifies the server
     nocerttimecheck 1                          # bootstrap a cold boot clock once

Components

Piece Where Notes
mode + prefix, chrony NTS directives diskless.pkr.hcl (chrony provisioner) secure_ntp (default true), ntp_prefix (default ntp); bakes ntstrustedcerts/nocerttimecheck + sourcedir
runtime name derivation files/etc/NetworkManager/dispatcher.d/10-chrony-dhcp-ntp mode-aware: nts derives ntp.<dhcp-domain>; dhcp = legacy option-42 IPs
NTS server directives serving/chrony/chrony.conf ntsservercert/ntsserverkey/ntsdumpdir
NTS server cert issuance serving/chrony/nts-cert.sh step ca certificate (DNS SAN = NTP_HOST = ntp.<domain>)
compose mount + firewall + domain handout serving/docker-compose.yml mounts ./chrony/nts; udp/123 and tcp/4460; DHCP option 15
DNS resolution dnsmasq --address=/${NTP_HOST}/ + NTP_HOST in defaults.env ntp.fleet.lab → serving host

Why nocerttimecheck — the chicken-and-egg

NTS-KE is TLS, so the client checks the server cert's validity window — which needs a roughly-correct clock. A diskless host has no reliable RTC, so its boot clock can be far enough off (or epoch) to make a valid cert look not-yet-valid/expired, and NTS would never bootstrap. nocerttimecheck 1 lets the first measurement set the clock without the cert-time check; every subsequent update enforces it. The trade-off (one measurement could accept an expired cert) is bounded by the isolated LAN and the single-measurement window, and is the standard chrony guidance for RTC-less devices.

Security properties

  • Authenticated time: a DHCP/NTP/DNS spoofer can only cause no-sync (DoS) — it cannot present a cert that chains to the fleet CA for the name it redirects you to, and NTS never falls back to unauthenticated time. So the clock cannot be shifted (the whole point of F-03). This holds because of NTS, even though DHCP/DNS themselves are unauthenticated.
  • No baked FQDN, no per-host state (invariant #1): only the ntp label + the fleet CA (already baked) are fixed; the domain is learned from DHCP at runtime.
  • No unauthenticated source mixed in: in nts mode the dispatcher writes only the NTS source; the option-42 IP handout is ignored.
  • Availability: local stratum 10 on the server keeps time serving if upstream is down; a host that can't reach NTS-KE simply holds its clock (no bad time).

Bench checklist (BENCH-VERIFY)

  1. Serving: run serving/chrony/nts-cert.sh (after the CA is up); confirm serving/chrony/nts/{nts.crt,nts.key} exist with DNS SAN = ntp.<domain>; restart the ntp service; open tcp/4460 + udp/123 on the boot NIC.
  2. Domain handout: on a booted host confirm the DHCP domain arrived — nmcli -g DHCP4.OPTION.* dev show <if> | grep domain or the dispatcher wrote /run/chrony-dhcp/<if>.sources with server ntp.<domain> iburst nts.
  3. Host: chronyc -N authdata shows the source in NTS mode with cookies > 0; chronyc sources shows ntp.<domain> reachable and selected.
  4. Spoof test: a rogue NTP/DHCP/DNS source is not trusted (NTS handshake fails ⇒ not selected) — DoS at worst, never a clock shift.
  5. Cold-clock test: boot with the RTC set far off; confirm nocerttimecheck 1 lets the first sync land, then the clock tracks.
  6. Domain-change test: change the DHCP domain + reissue the NTS cert with the new SAN; confirm the SAME image picks up ntp.<new-domain> with no rebuild.
  7. STIG: confirm remediation doesn't strip ntstrustedcerts/nocerttimecheck or the nts option in the sourcedir (host is a client; STIG-compatible).

Toggle

secure_ntp=false reverts the dispatcher to the legacy unauthenticated DHCP option-42 path (plain server <ip> iburst) for lab/interop. Keep it true for production.