Réinstaller un serveur NTP Stratum 1 GPS en 2 heures : notre journal
Le 23 octobre 2026, nous devions remplacer en douceur notre serveur Stratum 1,
ntp-0-hq.rdem-systems.com : un Raspberry Pi avec récepteur GPS, en service depuis 2018.
Le plan était de monter une machine neuve à côté de l'ancienne, de les comparer quelques jours, puis de basculer.
À la fin de la journée, la machine neuve était à l'arrêt (le récepteur GPS commandé pour elle ne démarre pas sur le
nouveau Pi ; nous y reviendrons), l'ancienne avait perdu sa carte SD,
et c'est l'ancien Raspberry Pi, réinstallé de zéro, qui servait de nouveau l'heure en Stratum 1, cette fois
avec chrony et le NTS, en IPv4 et en IPv6.
Cette page reprend le journal tenu pendant l'intervention : les commandes, ce qu'elles ont renvoyé, et les pièges, y compris ceux que nous avons posés nous-mêmes. Les coordonnées GPS, les adresses internes et les secrets sont retirés ; le reste est tel que relevé.
Avant et après
| Élément | Avant (installé en 2018) | Après (23 octobre 2026) |
|---|---|---|
| Carte | Raspberry Pi 3 Model B Rev 1.2 (la même) | |
| Récepteur GPS | Carte 52Pi, module Quectel L80 (puce MediaTek MT3339), reliée par les broches GPIO (série + PPS sur GPIO4), antenne extérieure OTB | |
| Système | Raspbian 10 (buster), noyau 5.10 | Raspberry Pi OS Lite Trixie (Debian 13), noyau 6.18.50, aarch64 |
| Démon NTP | ntpd 4.2.8p12 | chrony 4.6.1 |
| Lecture du PPS | programme en espace utilisateur (rpi_gpio_ntp) vers une mémoire partagée | pilote noyau pps-gpio → /dev/pps0, lu par chrony ; NMEA par gpsd 3.25 |
| NTS | non | oui, IPv4 et IPv6 |
| IPv6 | adresse configurée, aucun AAAA publié | AAAA publié, NTP et NTS ouverts |
| Configuration | à la main | rôle Ansible (chrony, sources, NTS, supervision) |
La sauvegarde de l'ancien, et ce qu'elle a révélé
Avant de toucher quoi que ce soit, nous avons archivé /etc, /boot, /usr/local,
/root et /home de l'ancien serveur, avec un relevé de son état (pairs NTP, ports ouverts,
paquets) et les empreintes SHA-256 des fichiers. Vingt fichiers clés ont été comparés à leur original sur la machine :
identiques. Cette archive a servi plus tard dans la journée, quand la carte SD a lâché.
Relire une configuration vieille de huit ans a aussi ses surprises :
- le PPS ne passait pas par le noyau, mais par un programme en espace utilisateur lancé depuis
rc.local; - ce même
rc.localarrêtait ntpd, recalait l'heure avecntpdate, puis relançait ntpd ; - deux fautes de frappe dans
ntp.conf(inurstau lieu deiburst,npt.polytechnique.frau lieu dentp.) : deux sources de contrôle n'avaient jamais été utilisées ; - une adresse IPv6 fixe était configurée et servait, mais aucun enregistrement AAAA ne la publiait.
La carte SD de l'ancien rend l'âme
Pour un essai sur la machine neuve, l'ancienne carte GPS a été retirée de l'ancien serveur ; l'ancien serveur a
donc été éteint. Remise en place, il ne redémarrait plus : sa carte SD de 2018, qui venait de subir une coupure de
courant (notre onduleur ne tient plus la charge) puis l'arrêt, était morte. Pendant cette panne, ntp-0-hq ne répondait plus ; nos serveurs
Stratum 2 se sont rabattus sur leurs autres sources, et l'heure qu'ils servent est restée juste.
Restaurer l'ancien système aurait voulu dire remettre en place Raspbian 10, qui n'est plus maintenu, et ntpd sans NTS. Nous avons préféré réinstaller l'ancien Pi de zéro sur une carte SD neuve, avec l'ancienne carte GPS et l'antenne extérieure : la cible prévue pour la machine neuve, sur le matériel qui fonctionnait. De la perte de la carte SD au retour en Stratum 1, réactivation de l'IPv6 et mise en place du NTS comprises, la réinstallation a pris deux heures.
La réinstallation, étape par étape
1. Système de base
Raspberry Pi OS Lite Trixie, apt full-upgrade, puis raspi-config : SSH, nom d'hôte, fuseau
Europe/Paris, clavier, noms d'interface prévisibles, et dans Interface Options → Serial Port, pas de
console sur le port série mais le port matériel activé. Sous Trixie, l'adresse fixe se règle avec NetworkManager
(nmcli ou nmtui) : raspi-config ne sait plus le faire. Sur un Pi 3, l'interface
Ethernet passe par l'USB et s'appelle enx suivi de l'adresse MAC.
2. Port série et PPS
Dans /boot/firmware/config.txt, section [all] :
enable_uart=1
dtoverlay=disable-bt
dtoverlay=pps-gpio,gpiopin=4
Sur un Pi 3, disable-bt rend au GPS l'UART PL011 que le Bluetooth occupe par défaut ; sinon le GPS se
retrouve sur le mini-UART, dont la vitesse dépend de la fréquence du processeur. Le PPS de cette carte arrive sur
GPIO4. cmdline.txt ne doit plus contenir console=serial0,115200, sinon la console
parle au GPS. Après redémarrage :
$ ls -l /dev/serial0 /dev/pps0
crw------- 1 root root 249, 0 Oct 23 16:54 /dev/pps0
lrwxrwxrwx 1 root root 7 Oct 23 16:54 /dev/serial0 -> ttyAMA0
Les trames arrivent à 9600 bauds, avec une position corrigée (qualité 2, SBAS) sur 8 satellites et
$GPTXT,01,01,02,ANTSTATUS=OK : l'antenne est reconnue. gpsd identifie le module :
$ gpspipe -w -n 6 | grep DEVICES
"driver":"MTK-3301","subtype":"AXN_2.5_3339_15092100,0004,QUECTEL-L80,1.0"
Un module Quectel L80 sur une puce MediaTek MT3339. « MTK-3301 » est le nom du pilote MediaTek de gpsd, pas celui de la puce : c'est de là que venait la mention erronée de notre ancienne page. Le PPS, une impulsion par seconde :
$ ppstest /dev/pps0
source 0 - assert 1792767887.000497314, sequence: 296 - clear 0.000000000, sequence: 0
source 0 - assert 1792767888.000499248, sequence: 297 - clear 0.000000000, sequence: 0
source 0 - assert 1792767889.000502276, sequence: 298 - clear 0.000000000, sequence: 0
Ce relevé est antérieur à chrony : l'horloge du Pi, livrée à elle-même, avance d'environ 0,5 ms sur le GPS et l'écart grandit d'environ 2,4 µs par seconde.
3. gpsd
Paquets gpsd gpsd-clients pps-tools chrony, puis /etc/default/gpsd :
DEVICES="/dev/serial0 /dev/pps0"
GPSD_OPTIONS="-n"
USBAUTO="false"
-n fait lire le GPS en permanence, même sans client connecté : chrony n'est pas un client de gpsd, il lit la
mémoire partagée que gpsd remplit. Il faut aussi activer le service ; nous l'avons appris au redémarrage
(plus bas).
4. Ansible
Le reste de la configuration vient de notre rôle Ansible : utilisateurs et clés, chrony (sources, horloges de référence,
autorisations, secondes intercalaires par leapseclist), certificat NTS et supervision. Une remarque qui vaut
pour tout chrony géré de cette façon : les fichiers sources.d/*.sources n'acceptent que les directives
server, pool et peer. Les refclock vont dans conf.d/.
chrony : PPS, NMEA et la seconde fausse
Les deux horloges de référence en service :
# /etc/chrony/conf.d/refclock.conf
refclock SHM 0 refid NMEA noselect poll 4 precision 1e-1 delay 0.2
refclock PPS /dev/pps0 refid PPS poll 4
NMEA, en noselect, n'est jamais choisi comme référence : la liaison série à 9600 bauds le décale
d'environ 175 ms. Le PPS porte la précision. Six serveurs extérieurs servent de témoins, dont quatre en NTS
(Observatoire de Paris, École polytechnique, PTB ×2, Netnod, Cloudflare), avec nos propres serveurs : ils ne donnent
pas l'heure tant que le PPS est là, mais ils feraient écarter un PPS qui se mettrait à mentir.
Pourquoi nous avons retiré lock NMEA
La première version de la configuration portait lock NMEA sur la ligne PPS : chrony numérote alors chaque
impulsion avec la seconde des trames NMEA. Or, quelques minutes après le démarrage à froid, le récepteur datait ses trames
avec une seconde d'avance : la trame annonçant 15:20:34 arrivait à 15:20:33,170 à l'horloge système, elle-même
calée sur le réseau à 26 µs près. Le défaut avait disparu après 22 minutes de fonctionnement :
15:35:55.259 UTC <- $GPZDA,153555.000
15:35:56.258 UTC <- $GPZDA,153556.000
Notre hypothèse, cohérente avec ce comportement mais non vérifiée dans le firmware : tant qu'il n'a pas reçu les paramètres UTC diffusés par les satellites (jusqu'à 12,5 minutes), le récepteur applique une valeur ancienne de l'écart entre le temps GPS et UTC, 17 secondes au lieu de 18.
Avec le réseau disponible, la seconde fausse est sans conséquence : les témoins répondent en quelques secondes,
le PPS met une à deux minutes à être retenu, et chrony l'écarte. Le cas dangereux est celui d'une coupure de courant,
où le Pi redémarre avant la box et le routeur. Avec lock NMEA, le PPS serait alors la seule source, et chrony,
autorisé à recaler l'horloge d'un coup, la mettrait sur une seconde fausse avant de la servir en Stratum 1. Sans
lock, chrony n'utilise le PPS qu'une fois l'horloge calée à mieux qu'une demi-seconde par une autre source.
Faute de réseau, le serveur se déclare non synchronisé et ses clients l'ignorent. Pas d'heure plutôt qu'une heure
fausse. C'était déjà, à notre lecture de sa configuration, le principe de l'ancien serveur : son PPS était numéroté par l'horloge système.
NTS et IPv6
Le certificat NTS est émis par Let's Encrypt avec une validation DNS-01, aux noms
ntp-0-hq.rdem-systems.com (le nom public) et ntp-0.hq.rdem-systems.com (le nom d'administration).
Son renouvellement est intégré au renouvellement centralisé de nos certificats.
Le NTS-KE écoute sur le port TCP 4460, défini par le RFC 8915 ;
les requêtes d'heure protégées passent ensuite par le port NTP habituel, UDP 123.
En IPv4, le serveur est derrière un double NAT : il faut rediriger UDP 123 et TCP 4460 sur les deux équipements.
En IPv6, il n'y a rien à traduire, seulement un pare-feu à ouvrir. Le Pi a reçu une adresse fixe, ajoutée à côté de son
adresse automatique. Un mtr depuis l'extérieur a montré où le trafic s'arrêtait : il traversait la box
(saut 10) et atteignait le routeur (saut 11), puis plus rien. Une règle ICMPv6 visait la mauvaise zone, et un changement
d'adresse côté box bloquait encore le NTP. Une fois corrigé :
10.|-- 2a01:e0a:808:97c0::1 0.0% 3 3.1 3.4 3.1 3.8 0.4
11.|-- 2a01:e0a:808:97c0:265a:4c 0.0% 3 3.9 3.8 3.7 3.9 0.1
12.|-- 2a01:e0a:808:97c4::123:12 0.0% 3 4.5 4.3 3.9 4.5 0.3
Un mtr ne prouve que le ping. Le service se vérifie avec un vrai client, ici chronyd -Q depuis un
de nos serveurs à Paris (AS206014), qui interroge sans toucher à l'horloge locale :
$ chronyd -Q -4 'server ntp-0-hq.rdem-systems.com nts iburst'
System clock wrong by -0.000062 seconds (ignored)
$ chronyd -Q -6 'server ntp-0-hq.rdem-systems.com nts iburst'
System clock wrong by -0.000308 seconds (ignored)
Interrogé par son adresse IPv6 plutôt que par son nom, le NTS-KE répond The name in the certificate does not match
the expected. C'est attendu, puisque le certificat porte des noms et non des adresses, et ça prouve au passage que le
NTS-KE répond bien en IPv6.
Les deux pièges du redémarrage
Nous avons fini par un redémarrage de contrôle. Il a révélé deux défauts qu'un fonctionnement continu aurait cachés.
chrony tournait avec l'ancienne configuration
Le rôle Ansible avait bien déposé ses fichiers, mais chronyd, démarré une heure plus tôt, n'avait pas été redémarré :
il suivait encore le pool Debian, sans horloge de référence ni autorisation pour les clients. Les fichiers étaient justes,
l'état réel ne l'était pas. Une vérification simple : systemctl show chrony -p ActiveEnterTimestamp doit être
postérieur au dernier changement de configuration.
gpsd ne démarrait pas
Au redémarrage, NMEA et PPS sont restés à Reach 0. Sous Debian 13, le paquet gpsd active
gpsd.socket mais pas gpsd.service : gpsd ne démarre qu'à la connexion d'un client. chrony
n'en étant pas un, la mémoire partagée restait vide. Avant le redémarrage, tout fonctionnait parce que gpsd avait été
lancé à la main. Correctif : systemctl enable gpsd.service.
Autre constat de ce redémarrage : cette carte n'a pas d'horloge sauvegardée. Le Pi est reparti avec l'heure de son dernier arrêt, 1 h 36 en retard, et chrony l'a recalée d'un coup. En revanche, le GPS, resté alimenté pendant ce redémarrage à chaud, a gardé ses paramètres UTC, et la seconde fausse n'est pas réapparue.
Résultat, et ce qui reste à faire
$ chronyc sources
#? NMEA 0 4 377 22 +182ms[ +182ms] +/- 200ms
#* PPS 0 4 377 23 +2670ns[+4937ns] +/- 213ns
$ chronyc tracking
Reference ID : 50505300 (PPS)
Stratum : 1
ntp-0-hq.rdem-systems.com est de nouveau un Stratum 1 calé
sur le PPS, à quelques microsecondes près, et sert le NTP et le NTS en IPv4 comme en IPv6. Le même soir, d'après le champ refid de leurs réponses, relevé vers 18 h 30,
7 de nos 16 serveurs Stratum 2 le suivaient ; les autres suivaient l'Observatoire de Paris (5), la PTB (3)
et le LINX (1). chrony choisit et peut changer de source à tout moment : ce relevé décrit un état, pas une garantie.
Restent ouverts :
- remplacer l'onduleur, qui ne tient plus la charge, et garder une copie de la carte SD ;
- comprendre pourquoi le nouveau récepteur GPS ne démarre pas sur le nouveau Pi (ce sera l'objet d'un prochain journal), puis reprendre le plan initial : rodage en parallèle, comparaison, bascule.
Sources
- RFC 8915 — Network Time Security for NTP (port 4460, NTS-KE)
- Documentation de chrony.conf (4.6.1) — directives
refclock(optionlock),sourcedir,makestep - GPSD Time Service HOWTO — gpsd, mémoire partagée et chrony
- Interne — Notre serveur Stratum 1, notre infrastructure, NTS sur chrony