Skip to content

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.26.1\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.

Current Status

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.26.1 writes clockTimestamp=system_time and receiveTimestamp=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.26.1.

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-2 time to pool clients via MetalLB VIP 192.168.1.58.

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.

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.