Adding secure NTP
This commit is contained in:
@@ -0,0 +1,127 @@
|
||||
# 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.
|
||||
Reference in New Issue
Block a user