Chrony : le cœur du système NTP/NTS#

Pourquoi#

Tout ce qui précède dans ce guide, le PPS câblé, le noyau temps réel, les IRQ isolées sur des cœurs dédiés, n’existe que pour nourrir un seul programme : chronyd. C’est lui qui reçoit les impulsions PPS et les trames GNSS, filtre le bruit de mesure, discipline l’horloge système, et sert le temps aux clients via NTP et NTS. Tout le reste de l’installation est de la plomberie au service de ce démon.

chrony (par opposition au ntpd historique) est le choix retenu ici pour plusieurs raisons concrètes : une convergence plus rapide dans les conditions imparfaites (redémarrage, connexion intermittente), un support natif de NTS (chiffrement du protocole NTP, RFC 8915), une base de code nettement plus petite et auditable (~30 000 lignes contre ~120 000 pour ntpd), et un sandboxing seccomp activé par défaut sur les distributions récentes.

Comment#

La source PPS : le cœur du cœur#

refclock PPS /dev/pps0 lock GPS poll 2 filter 16 precision 1e-7 \
             rate 1 width 0.5 maxlockage 10 prefer

Décomposons chaque option :

OptionSignification
/dev/pps0Le device exposé par le module kernel pps-gpio
lock GPSN’utilise le PPS que si la source GPS (voir plus bas) confirme un fix valide, évite de discipliner l’horloge sur une impulsion sans repère temporel
poll 2Intervalle entre échantillons : 2² = 4 secondes
filter 16Médiane statistique sur les 16 derniers échantillons : c’est ce filtre qui transforme un jitter brut de plusieurs microsecondes en RMS offset effectif autour de la microseconde
precision 1e-7Précision de cette refclock, en secondes décimales du recepteur GPS, ici arrondi à 100 ns
maxlockage 10Tolère jusqu’à 10 absences de confirmation GPS avant de “délocker”
preferCette source est favorisée dans l’algorithme de sélection si elle est disponible
refclock SHM 0 refid GPS offset 0.125 noselect poll 4

Cette seconde ligne reçoit les trames GNSS (position, date) via gpsd, en mémoire partagée (shared memory, canal 0). Elle ne sert qu’à lever l’ambiguïté de seconde du PPS (lock GPS ci-dessus), la directive noselect l’exclut explicitement de tout rôle de source de temps propre : la précision de l’horloge repose uniquement sur le PPS. offset 0.125 compense le délai moyen entre l’instant réel du point GNSS et la réception de la trame NMEA correspondante sur le port série (une trame texte prend un temps non nul à être émise puis parsée), une valeur déterminée empiriquement, sans conséquence sur la précision finale puisque cette source n’est jamais sélectionnée pour l’horodatage lui-même.

Les sources de secours#

server ntp-p1.obspm.fr    minpoll 6 iburst xleave
server ptbtime4.ptb.de    minpoll 6 iburst xleave
server ntp1.npl.co.uk     minpoll 6 iburst xleave
server ntp1.sp.se         minpoll 6 iburst xleave
server ntp2.oma.be        minpoll 6 iburst xleave
server time.nist.gov      minpoll 6 iburst xleave

Même un Stratum 1 GNSS garde des sources amont classiques, pour deux raisons : détecter une dérive silencieuse du PPS (un GPS qui continue d’émettre des impulsions régulières mais légèrement décalées, par exemple à cause d’un problème d’antenne), et fournir un filet de sécurité en cas de panne GNSS totale. xleave (mode interleaved, RFC 5905) réduit le biais lié à l’asymétrie du réseau ; minpoll 6 fixe l’intervalle minimal d’interrogation à 2⁶ = 64 secondes, l’étiquette à respecter pour toute source institutionnelle (chapitre Performances), pas plus qu’une requête par minute. Le choix des six lignes ci-dessus privilégie des laboratoires nationaux de métrologie plutôt qu’un pool anonyme :

  • ntp-p1.obspm.fr (France, LNE-SYRTE / Observatoire de Paris) offre un ancrage institutionnel directement rattaché à la réalisation française du temps légal.
  • ptbtime4.ptb.de (Allemagne, Physikalisch-Technische Bundesanstalt).
  • ntp1.npl.co.uk (Royaume-Uni, National Physical Laboratory).
  • ntp1.sp.se (Suède, RISE / SP).
  • ntp2.oma.be (Belgique, Observatoire royal de Belgique).
  • time.nist.gov (États-Unis, National Institute of Standards and Technology) : seul repère hors Europe de cette liste, il diversifie la zone géographique de dépendance.

Chacun de ces six serveurs est un Stratum 1 institutionnel connu, dont l’écart à UTC est publié mensuellement dans la Circular T du BIPM (le chapitre Performances recense l’ensemble des laboratoires nationaux de métrologie exposant un service NTP public, bien au-delà de ces six-là).

Combien de sources ? Pour une machine qui tire son heure du réseau (un client ordinaire, ou ce serveur en secours GNSS), un minimum de 3 serveurs est conseillé : avec 2, un désaccord est impossible à trancher ; à partir de 3, la majorité isole une source aberrante, et 4 permettent de conserver cette majorité même pendant la panne d’une source (algorithme de sélection, chapitre Vue d’ensemble). À l’autre extrémité, 5 à 6 sources constituent un maximum raisonnable : au-delà, le gain statistique devient négligeable tandis que la charge imposée aux serveurs publics augmente inutilement. L’exemple ci-dessus totalise 6 sources, cinq laboratoires européens et un nord-américain, le haut de la fourchette.

local stratum 14 orphan distance 0.999

Ce mode “orphelin” ne s’active que si aucune source amont n’est sélectionnable (panne GNSS et réseau coupé) : le serveur continue d’émettre l’heure (au stratum 14, donc clairement dégradé) plutôt que de s’arrêter complètement de répondre.

Servir les clients : allow et ratelimit#

Tout ce qui précède configure ce que ce serveur interroge en amont. Encore faut-il l’autoriser à répondre à ses propres clients, sans quoi tout ce guide ne produirait qu’une horloge parfaitement disciplinée mais muette.

allow
ratelimit interval 2 burst 8
ntsratelimit interval 3 burst 8

Sans allow, chrony refuse par défaut toutes les requêtes clientes. C’est le piège le plus trompeur de ce chapitre : un serveur parfaitement configuré côté sources amont (PPS, GNSS, secours réseau) mais sans directive allow disciplinera sa propre horloge avec une précision exemplaire, et ne répondra à aucun client externe. Rien dans les journaux ne signale cette absence comme une erreur : les requêtes entrantes sont simplement ignorées silencieusement. allow (sans argument) autorise tous les clients ; on peut aussi restreindre à une plage (allow 203.0.113.0/24).

ratelimit limite le débit de requêtes NTP au niveau du protocole lui-même, interval 2 correspond à un intervalle minimal de 2² = 4 secondes entre deux requêtes tolérées d’une même IP, burst 8 autorise une rafale initiale de 8 requêtes avant que la limite ne s’applique (compatible avec iburst côté client). ntsratelimit fait de même spécifiquement pour les négociations NTS-KE, avec ses propres valeurs : interval 3 porte l’intervalle minimal à 2³ = 8 secondes plutôt que 4, une poignée de main NTS-KE complète (négociation TLS 1.3) coûtant nettement plus cher en calcul qu’un simple paquet NTP, la limite est logiquement plus prudente.

Cette limite est un troisième niveau de protection, distinct des deux autres déjà vus dans ce guide : le rate-limiting réseau via nftables (chapitre nftables, aveugle au protocole, agit avant même que le paquet n’atteigne chrony) et le Kiss-o’-Death (chapitre Anatomie du paquet NTP, une rétorsion protocolaire coopérative). ratelimit se situe entre les deux : c’est chrony lui-même qui décide, au niveau applicatif, de droper ou non une requête excédentaire, et c’est précisément ce mécanisme qui déclenche l’envoi d’un code Kiss RATE au client fautif.

NTS : chiffrer le protocole#

ntsport 4460
ntsservercert /etc/chrony/fullchain.pem
ntsserverkey  /etc/chrony/privkey.pem

NTS (Network Time Security, RFC 8915) ajoute une couche d’authentification cryptographique au protocole NTP historiquement en clair. Le client établit d’abord une session TLS sur le port TCP 4460 (NTS-KE, Key Establishment) pour obtenir des clés à usage unique, puis échange des paquets NTP normaux mais signés avec ces clés. Concrètement, cela réutilise le même certificat TLS que le site web (Let’s Encrypt), rien de plus à gérer côté certificats.

L’alternative plus ancienne : clé symétrique partagée#

NTS n’est pas la seule façon d’authentifier NTP, ni même la première. chrony sait aussi authentifier un serveur particulier via une clé symétrique pré-partagée (mécanisme natif de NTP décrit dans la RFC 5905 elle-même), un ancien mécanisme, mais toujours pris en charge et activement utilisé, y compris en parallèle de NTS.

# /etc/chrony/chrony.keys, clé de démonstration, usage réservé aux tests
# La clef 42 générée le 26/02/2026 sur time.example.org
42 SHA256 HEX:A4E57E0554C9860BD5E2C1384768FBA42E305E3617C83085FB655E480879A29F
# /etc/chrony/chrony.conf
keyfile /etc/chrony/chrony.keys
server time.example.org key 42 iburst xleave minpoll 6
pool pool.ntp.org iburst

keyfile indique à chrony où trouver le fichier de clés ci-dessus, sans cette directive, la référence key 42 d’une ligne server ne résoudrait vers rien et chrony refuserait de démarrer l’association authentifiée. Chaque paquet échangé avec ce serveur particulier est ensuite signé par un HMAC utilisant la clé 42 partagée à l’avance, le client comme le serveur doivent connaître cette clé. minpoll 6 fixe l’intervalle minimal d’interrogation à 2⁶ = 64 secondes, une valeur classique pour un serveur NTP distant sur une liaison de confiance de ce type, largement suffisante une fois la convergence initiale atteinte et moins gourmande en requêtes qu’un minpoll par défaut plus court.

Ne pas confondre trois mécanismes distincts :

  • Clé symétrique (ci-dessus, RFC 5905) : simple, toujours utilisée aujourd’hui, mais la distribution de la clé est manuelle : il faut la transmettre hors-bande à chaque client autorisé, ce qui ne passe pas à l’échelle d’un serveur public anonyme.
  • Autokey (RFC 5906, conçu par D. Mills) : une tentative d’automatiser cette distribution de clé via de la cryptographie à clé publique. Des faiblesses cryptographiques identifiées par la recherche académique (vulnérabilité à des attaques par déni de service et par usurpation) ont conduit la communauté à l’abandonner : c’est bien Autokey qui a disparu, pas l’authentification par clé symétrique.
  • NTS (RFC 8915) : la solution moderne au même problème d’automatisation, en s’appuyant sur TLS 1.3 plutôt que sur le schéma propre à Autokey.

Sur ce projet, la clé symétrique reste utile pour un lien de confiance point à point où les deux parties peuvent échanger une clé hors-bande (test, liaison interne) ; NTS reste la bonne réponse pour un serveur public répondant à des clients anonymes qui ne peuvent pas récupérer une clé au préalable.

Confiner chronyd en temps réel sur le Core 1#

chronyd calcule et applique en continu la correction de fréquence de l’horloge système à partir du PPS : une préemption tardive dégraderait directement la précision de cette boucle de discipline. Un override systemd le place en ordonnancement temps réel FIFO, juste sous la priorité de l’IRQ PPS (99), sur le même cœur que phc2sys.

mkdir -p /etc/systemd/system/chrony.service.d/
cat > /etc/systemd/system/chrony.service.d/override.conf <<'EOF'
[Unit]
After=network-online.target
Wants=network-online.target

[Service]
# Ordonnancement temps réel FIFO, priorité 98 (juste sous l'IRQ PPS à 99)
CPUSchedulingPolicy=fifo
CPUSchedulingPriority=98
CPUAffinity=1

# Accès en écriture au tmpfs NTS
ReadWritePaths=/var/nts/chrony

# Délai pour laisser le pilote PPS et l'horloge PHC se présenter
ExecStartPre=/bin/sleep 5

Restart=on-failure
RestartSec=10s
EOF

systemctl daemon-reload
systemctl restart chrony

Ne jamais ajouter After=gpsd.service dans ce drop-in. Cela créerait un cycle de dépendances au démarrage entre chrony et gpsd : les deux services attendent alors indéfiniment l’un l’autre.

Persistance de la dérive : driftfile#

driftfile /var/lib/chrony/chrony.drift

chrony réestime en continu la dérive de fréquence de l’oscillateur local et l’écrit périodiquement dans ce fichier. Au redémarrage, il relit cette valeur et l’applique immédiatement, plutôt que de repartir de zéro : la convergence vers une précision optimale est nettement plus rapide qu’à un premier démarrage. C’est le même principe que la table tempcomp ci-dessous (un état persistant entre reboots), en plus simple : une seule valeur de dérive de fond, sans dépendance à la température.

Compensation thermique dynamique : tempcomp#

Un oscillateur à quartz dérive avec sa température : le Pi 5 n’embarque qu’un quartz nu, non compensé, et les pentes mesurées sur ce serveur vont de 0,06 à 0,2 ppm/°C selon la zone (chapitre Du quartz nu au pseudo-TCXO), en plus de la stabilisation passive déjà décrite (chapitre Stabilisation thermique). chrony propose la directive tempcomp pour compenser activement cette dérive, à partir d’une sonde de température lue périodiquement :

tempcomp /sys/class/thermal/thermal_zone0/temp 20 /var/lib/chrony/chrony.tempcomp

/sys/class/thermal/thermal_zone0/temp est la même sonde SoC que celle pilotant déjà la courbe du ventilateur (chapitre Firmware) : aucune sonde dédiée au quartz n’existe sans modification matérielle. La table de points (plutôt qu’une simple formule quadratique) permet de coller à une courbe réelle qui n’est pas une parabole propre.

log tracking measurements tempcomp

Cette directive est ce qui alimente concrètement tracking.log et tempcomp.log, sans elle, tempcomp-refit (plus bas) n’aurait aucune donnée à relire, la table resterait figée à ses valeurs initiales.

Un script (tempcomp-refit, lancé par un timer systemd quotidien) reconstruit cette table à partir de l’historique observé (tempcomp.log + tracking.log), plutôt que de figer une calibration unique. Pour cela, il découpe la plage de température en tranches de 0,5 °C (un bin, au sens statistique du terme : un intervalle regroupant tous les échantillons tombant dedans) et calcule, pour chaque bin ayant assez d’échantillons, la compensation moyenne observée à cette température :

systemctl enable --now tempcomp-refit.timer

Trois points méritent attention pour qui voudrait reproduire ce mécanisme :

  • chrony ne relit le fichier de points qu’au démarrage. Pas de rechargement à chaud, même via SIGHUP (vérifié dans le code source de chrony). Une nouvelle table n’a donc aucun effet tant que chrony n’est pas redémarré. Le script ne redémarre jamais chrony lui-même : il écrit la table et journalise qu’elle est prête, le redémarrage reste un geste manuel choisi par l’opérateur.
  • chrony extrapole sans plafond au-delà de la table. Une température jamais observée (canicule, panne ventilateur) prolongerait la pente du dernier segment mesuré, potentiellement bruitée. Le script ajoute donc systématiquement une marge plate de part et d’autre de la plage observée, pour que la compensation reste figée à la dernière valeur fiable plutôt que de diverger.
  • une mesure prise pendant un régime transitoire n’est pas fiable. Un test réel de refroidissement forcé sur ce serveur a montré que la fréquence mesurée juste après un choc thermique brutal peut dépasser largement ce que prédit le comportement en régime stable, avant de revenir progressivement à la normale. D’où des seuils de volume minimum de données avant tout déploiement automatique.
  • un bin déjà déployé est rejeté s’il s’écarte trop. Un bin peut continuer à s’affiner d’un passage à l’autre, mais si la nouvelle valeur diffère trop de celle actuellement en place, la mise à jour est rejetée et journalisée plutôt qu’écrite. Objectif : suivre le vieillissement naturel, lent, du quartz, tout en bloquant un saut brutal causé par un échantillon contaminé par un transitoire.

Erreurs à éviter#

La théorie n’est pas toujours confirmée par la mesure : seule la mesure tranche. Un raisonnement a priori solide suggérait qu’avec un PPS à 1 Hz, poll 2 (échantillonnage toutes les 4 s) ne remplit que 4 des 16 emplacements du filtre médian (filter 16), et qu’un poll 4 (16 s) le remplirait réellement, donc, en théorie, une meilleure réjection du bruit. Testé en conditions réelles sur ce projet : poll 2 a mesurément donné un meilleur RMS offset que poll 4. La leçon : sur un système avec autant de facteurs couplés (charge IRQ, comportement du filtre, dérive thermique), une hypothèse théorique doit être validée par une mesure avant/après sur 24 h avant d’être adoptée définitivement. Certains de ces facteurs ne sont pas de simples paramètres qu’on pourrait rejouer à l’identique : ils sont, pour partie, intrinsèquement non déterministes. La gigue d’interruption du noyau en est un exemple concret : elle varie de façon stochastique d’un échantillon PPS à l’autre (état du cache processeur, contention du bus mémoire au moment précis de l’interruption), ce qui modifie la structure de corrélation des échantillons que filter 16 moyenne, un raisonnement purement théorique ne peut pas l’anticiper.

  • Commentaires en fin de ligne sur les directives server. Certaines versions de chrony n’acceptent pas de commentaire # après une directive server sur la même ligne, le parseur peut se comporter de façon inattendue. Les commentaires explicatifs doivent être placés au-dessus de la ligne.
  • chronyc qui échoue silencieusement sous un service systemd durci. Si chronyd tourne dans un sandbox systemd restrictif (recommandé, voir le chapitre durcissement), la commande chronyc par défaut (qui utilise un socket UDP local) peut échouer sans message clair.
  • Confondre noselect et absence de source. refclock SHM 0 ... noselect reste actif et fournit des données à lock GPS, noselect signifie seulement “ne pas utiliser directement comme référence de temps”, pas “désactivé”.
  • Redémarrer chronyd pour recharger un certificat NTS. Un systemctl restart casse la boucle de discipline en cours (la PLL perd son état). Pour recharger un certificat renouvelé, envoyer un SIGHUP au processus suffit et préserve la synchronisation.

Vérifier#

$ chronyc tracking
Reference ID    : 50505330 (PPS0)
Stratum         : 1
RMS offset      : 0.000000071 seconds
Skew            : 0.007 ppm
Leap status     : Normal
  • Reference ID = PPS0 confirme que c’est bien le PPS qui pilote l’horloge, pas une source réseau de secours.
  • RMS offset : l’écart type de l’erreur mesurée, autour de la microseconde ici, c’est l’objectif du projet.
  • Skew : la dérive de fréquence de l’oscillateur local, en ppm (parties par million). Une valeur stable et faible (< 0,05 ppm) est le signe d’un système bien discipliné et thermiquement stable.
# Confinement RT effectif : classe FF (FIFO), priorité 98, cœur 1
ps -eLo psr,cls,rtprio,comm | grep chronyd
# Attendu : 1 FF 98 chronyd
$ chronyc sources -v

  .-- Source mode  '^' = server, '=' = peer, '#' = local clock.
 / .- Source state '*' = current best, '+' = combined, '-' = not combined,
| /             'x' = may be in error, '~' = too variable, '?' = unusable.
||                                                 .- xxxx [ yyyy ] +/- zzzz
||      Reachability register (octal) -.           |  xxxx = adjusted offset,
||      Log2(Polling interval) --.      |          |  yyyy = measured offset,
||                                \     |          |  zzzz = estimated error.
||                                 |    |           \
MS Name/IP address         Stratum Poll Reach LastRx Last sample
===============================================================================
#* PPS0                          0   2   377     3    +18ns[  +16ns] +/-  802ns
#? GPS                           0   4   377    13  -1082us[-1082us] +/-  277us
^- ntp-p1.obspm.fr               1   9   377   244   -278us[ -250us] +/- 1289us
^- ptbtime4.ptb.de               1   9   377   301   -462us[ -418us] +/-   10ms
^- ntpsvr1.npl.co.uk             1   8   377   170   -552us[ -536us] +/- 5236us
^- 193.11.166.8                  1  10   377   185  -1208us[-1190us] +/-   19ms
^- 129.6.15.25                   1   8   377    39   -330us[ -330us] +/-   44ms
^- heppen.be                     1  10   377   29m   +116us[ +462us] +/- 8549us

Sortie réelle de ce serveur (l’option -v ajoute la légende) :

  • #* : source actuellement sélectionnée comme référence (le # indique une horloge de référence locale, pas un serveur réseau). Ici le PPS, à ±802 ns d’incertitude estimée.
  • #? : source non sélectionnable actuellement (normal pour GPS qui n’a que le rôle de désambiguïsation, son offset de l’ordre de la milliseconde ne gêne en rien).
  • ^- : source réseau, validée mais non sélectionnée (comportement attendu tant que le PPS répond correctement). Les six sources amont de ce serveur sont elles-mêmes des Stratum 1 institutionnels (Observatoire de Paris, PTB allemande, NPL britannique, RISE suédois, NIST américain, plus un serveur communautaire belge), en cohérence avec la fourchette de 5-6 sources de la section Sources de secours.
  • Reach 377 (octal) : les 8 dernières interrogations ont toutes répondu ; LastRx 29m montre qu’une source espacée à Poll 10 (2¹⁰ ≈ 17 min) peut afficher un dernier échantillon ancien sans que ce soit un défaut.
$ chronyc -N authdata

permet de vérifier qu’un client s’est bien authentifié via NTS.


📚 Pour aller plus loin

Protocole on-wire (calcul d’offset). Pour chaque échange, quatre timestamps sont collectés (T1 départ requête client, T2 réception serveur, T3 départ réponse serveur, T4 réception client) :

offset = ((T2 - T1) + (T3 - T4)) / 2
délai  = (T4 - T1) - (T3 - T2)

Hypothèse implicite : symétrie du réseau dans les deux sens. Toute asymétrie introduit un biais égal à la moitié de l’asymétrie (D. Mills, Computer Network Time Synchronization, 2006, ch. 3).

Sélection des sources. Quand plusieurs serveurs sont interrogés, l’algorithme dérive de l’algorithme de Marzullo (K. Marzullo, thèse de doctorat, Stanford, 1984), adapté par Mills pour NTP : chaque source fournit un intervalle de confiance [offset - délai, offset + délai], et l’algorithme cherche l’intersection maximale cohérente entre intervalles pour exclure les sources aberrantes (falsetickers). C’est ce qui justifie de garder plusieurs sources amont même sur un Stratum 1 : détecter au moins un falseticker nécessite au minimum 4 sources.

Références normatives :

  • RFC 5905, Network Time Protocol Version 4: Protocol and Algorithms Specification, D. Mills et al., 2010.
  • RFC 8915, Network Time Security for the Network Time Protocol, D. Franke, D. Sibold et al., 2020.