Chrony GPS NTP Server¶
The ntppool namespace runs a stratum-1 NTP server backed by a GPS receiver with PPS (Pulse Per Second) output, connected via USB to one of the cluster nodes.
Architecture¶
Signal capture — GPS to kernel¶
flowchart LR
gps["GPS receiver\nTTL UART 115200\nGP/GL/GA/BD"]
ft232["FT232R\nUSB-UART"]
pod_uart["ttyUSB0\nin ntppool pod"]
gpsd["gpsd 3.27.3\nSHM 0 = NMEA\nSHM 1 = PPS\n(TIOCMIWAIT on DCD)"]
gps -->|"NMEA + PPS\non DCD pin"| ft232 --> pod_uart --> gpsd
Time service — gpsd to NTP clients¶
flowchart LR
gpsd["gpsd\nSHM 0 + SHM 1"]
chrony["chronyd\nSHM 0 → NMEA (falseticker)\nSHM 1 → PPS (primary)"]
vip["MetalLB VIP\n192.168.1.58"]
clients["NTP Pool Project\nstratum-2 clients"]
gpsd -->|"shared memory\nslots"| chrony -->|"NTP :123 + NTS :4460"| vip --> clients
Key Configuration¶
refclock SHM 0 refid NMEA precision 1e-1
refclock SHM 1 refid PPS precision 1e-7 offset -0.100 lock NMEA
The offset -0.100 corrects for the FT232R's 100 ms early-fire characteristic on the PPS signal.
Status (typical)¶
| Source | Offset | Role |
|---|---|---|
| NMEA (SHM 0) | ~+118ms | Falseticker — used only for second identification |
| PPS (SHM 1) | ~-7µs ±9µs | Primary stratum-1 source |
Key Findings¶
- gpsd SHM timestamp inversion — gpsd 3.27.3 writes
clockTimestamp=system_timeandreceiveTimestamp=GPS_time, opposite of the NTP SHM spec. This is intentional and well-known. - Do not use ldattach / N_PPS ldisc — it drops all NMEA serial data on the same port. gpsd's TIOCMIWAIT-based PPS (SHM 1) is the correct approach.
- Startup order matters — gpsd must start first (
gpsd & sleep 3 && exec chronyd). Reversed order causes zombie accumulation and stale SHM slots. - gpsd SHM 2/3 not written — only SHM 0 (NMEA) and SHM 1 (PPS) are populated by gpsd 3.27.3.
Deployment Notes¶
The pod runs in the ntppool namespace on node qui (which has the USB GPS receiver). It participates in the public NTP Pool Project, serving stratum-1 time via MetalLB VIP 192.168.1.58 — pool clients that sync to it become stratum 2.
Lesson learned: probe the daemon, not the pod
Kubernetes ignores a container image's Docker HEALTHCHECK. Without an explicit probe, a daemon that exits after the entrypoint has forked still reports 1/1 Running — a silently dead chronyd can look healthy for hours. chronyd carries an exec liveness/readiness probe (chronyc -n tracking), so a stopped daemon surfaces as CrashLoopBackOff. A separate functional probe — the ntp_health Windmill flow — queries the server over NTP every 5 minutes: pod availability alone is not proof the service is answering.
Time Distribution¶
flowchart LR
gps["GPS + PPS"]
s1["Cluster stratum-1\nVIP 192.168.1.58\nNTP :123 + NTS :4460"]
v4["Pool clients (IPv4)\nnative"]
v6["Pool clients (IPv6)\nvia NAT64"]
router["BPI-R4 chronyd\nstratum 2"]
gps --> s1
s1 --> v4
s1 --> v6
s1 --> router
IPv6 pool service arrives via the router's NAT64 by design — the cluster is IPv4-only. A router-local chronyd answering the v6 pool directly was considered and rejected: it would be a stratum-2 server, and the pool listing is for the stratum-1 service. NAT64 keeps the v6 pool clients on the real stratum-1 source; in practice pool traffic dominates the NAT64 path (≈95% of translated packets).
The router's own chronyd does sync from the VIP as a stratum-2 downstream for local use, tracking within roughly ±130 µs.
Client Census¶
chronyd's client log is sized for a pool-scale audience — clientloglimit allows a ~4M-record client table. An hourly Windmill probe (ntppool_clients) walks it and ships per-family, per-country counts: typically ≈300k IPv4 + 85k IPv6 distinct clients per hour across 195 countries, charted in Grafana (/d/ntppool-clients).
NTS (Network Time Security)¶
Beyond plain NTP, chronyd also serves NTS (RFC 8915) over TCP/4460 on the same MetalLB VIP — clients can cryptographically authenticate the time source rather than trusting unsigned packets. This is the public time endpoint for mirror.mdapi.ch, listed in the jauderho/nts-servers public NTS server list and registered as a stratum-1 NTS server on support.ntp.org. cert-manager issues the mirror.mdapi.ch certificate, and the BPI-R4 forwards 4460/tcp to the VIP for off-net clients.