Télémétrie publique#

Pourquoi#

Publier un instantané de l’état du serveur (chrony_stats.txt, servi par nginx) permet à quiconque d’auditer la qualité du service sans accès SSH, transparence appréciable pour un service de référence temporelle. Mais cette publication doit être conçue avec une contrainte de sécurité absolue : ne jamais exposer de données de position. Un récepteur GNSS capte aussi la latitude/longitude/altitude ; publier ces informations, même approximatives, révélerait l’emplacement physique exact du serveur.

Comment#

Génération atomique du fichier#

cat > /opt/stats/stats.sh <<'EOF'
#!/bin/bash
set -e

OUT=/var/time/chrony_stats.txt
TMP=$(mktemp /var/time/.stats/chrony_stats.XXXXXX)
trap 'rm -f "$TMP"' EXIT

CHRONYC="/usr/bin/chronyc -h /run/chrony/chronyd.sock"

{
    echo "--- NTP ---"
    $CHRONYC -m tracking serverstats 'sources -v'

    echo "--- PTP ---"
    PHC_MEDIAN=$(/usr/local/sbin/phc-offset-median /dev/ptp0 5)
    [ -n "$PHC_MEDIAN" ] && echo "offset ${PHC_MEDIAN}ns (médiane)"

    echo "--- GNSS ---"
    timeout 10 gpspipe -w -n 8 2>/dev/null | /opt/stats/gnss_sky.sh
} > "$TMP"

chmod 0644 "$TMP"
mv "$TMP" "$OUT"
EOF
chmod +x /opt/stats/stats.sh

L’écriture passe par un fichier temporaire renommé atomiquement (mktemp + mv) : nginx ne voit jamais un fichier à moitié écrit, puisque rename() est une opération atomique du système de fichiers.

Le filtre GNSS : jamais de position#

cat > /opt/stats/gnss_sky.sh <<'EOF'
#!/bin/bash
# N'isole QUE le rapport JSON "SKY" de gpsd (santé de réception :
# satellites, DOP). Ignore délibérément le rapport "TPV" (position/vitesse).
set -euo pipefail

SKY=$(jq -c 'select(.class == "SKY")' | head -n1)
[ -z "$SKY" ] && { echo "GNSS : rapport SKY indisponible"; exit 0; }

HAS_SAT_ARRAY=$(jq -r 'if .satellites then "1" else "0" end' <<<"$SKY")

if [ "$HAS_SAT_ARRAY" = "1" ]; then
    N_USED=$(jq -r '[.satellites[] | select(.used == true)] | length' <<<"$SKY")
else
    N_USED=$(jq -r '.uSat // empty' <<<"$SKY")
fi
HDOP=$(jq -r '.hdop // empty' <<<"$SKY")

echo "Satellites utilisés : ${N_USED:-indisponible}"
[ -n "$HDOP" ] && echo "HDOP : $HDOP"
EOF
chmod +x /opt/stats/gnss_sky.sh

Le script ne lit jamais les champs lat, lon, alt, track, speed, même par accident, puisqu’il isole exclusivement le rapport SKY (santé de réception), pas TPV (position/vitesse).

Publication via nginx#

location = /chrony_stats.txt {
    allow all;
    access_log off;
    add_header Cache-Control "public, max-age=300";
}

Régénération toutes les 5 minutes via un timer systemd (stats.timer + stats.service), plutôt qu’un calcul à la demande, évite qu’une requête cliente ne déclenche une lecture chrony coûteuse.

Erreurs à éviter#

Les angles azimut/élévation par satellite ne doivent jamais être publiés, même s’ils semblent inoffensifs. Combinés à l’éphéméride publique des satellites GNSS (position connue de chaque satellite à tout instant) et à un horodatage précis, ces angles permettraient de reconstituer une position approximative par triangulation inverse. C’est une catégorie de risque à part entière, “Localisation”, distincte des risques de performance ou de diagnostic habituels : toujours refuser par défaut, quelle que soit la métrique demandée.

  • Supposer que le rapport JSON de gpsd a toujours la même forme. Certains récepteurs (notamment ceux utilisant NMEA plutôt que le protocole binaire propriétaire) émettent un rapport SKY minimal, sans le tableau satellites[] détaillé, seulement des champs agrégés au niveau racine (uSat, hdop). Un script qui suppose la présence du tableau satellites[] produira un décompte de “0 satellite” trompeur au lieu du vrai chiffre, silencieusement : un script robuste doit gérer les deux formats.
  • Dimensionner mal le nombre de lignes lues vs le timeout. Demander trop de lignes (gpspipe -n 30) avec un timeout trop court peut provoquer une troncature systématique, alors qu’une seule ligne SKY complète suffit. Mieux vaut une petite valeur -n qui se termine naturellement, avec un timeout large en pur filet de sécurité, que l’inverse.
  • Utiliser printf pour formater des nombres extraits de JSON. Sur un système en locale non-anglophone (fr_FR par exemple), printf interprète le séparateur décimal comme une virgule et rejette un nombre au format JSON (point), combiné à set -e, cela tronque silencieusement la sortie. Toujours déléguer le formatage numérique à jq, qui est garanti indépendant de la locale.

Vérifier#

sudo systemctl start stats.service
cat /var/time/chrony_stats.txt

# Vérification statique : aucune fuite de position dans le code
grep -iEw 'lat|lon|alt|track|speed|az|el' /opt/stats/gnss_sky.sh
# Ne doit renvoyer AUCUNE ligne exécutable (seulement, éventuellement,
# des commentaires explicatifs)

curl -A "Mozilla/5.0" https://time.example.org/chrony_stats.txt

nginx bloque les User-Agent curl/wget par défaut (chapitre NGINX), pour tester manuellement l’accès public, toujours passer un User-Agent de navigateur (-A "Mozilla/5.0"), sinon le test échoue pour une raison qui n’a rien à voir avec la télémétrie elle-même.