Public telemetry#
Why#
Publishing a snapshot of the server’s state (chrony_stats.txt, served by
nginx) lets anyone audit the service’s quality without SSH access,
worthwhile transparency for a reference time service. But this publication
must be designed around one absolute security constraint: never expose
position data. A GNSS receiver also picks up latitude/longitude/altitude;
publishing this information, even approximately, would reveal the server’s
exact physical location.
How#
Atomic file generation#
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 (median)"
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.shWriting goes through a temp file, renamed atomically (mktemp + mv):
nginx never sees a half-written file, since rename() is an atomic
filesystem operation.
The GNSS filter: never position#
cat > /opt/stats/gnss_sky.sh <<'EOF'
#!/bin/bash
# Isolates ONLY gpsd's "SKY" JSON report (reception health: satellites,
# DOP). Deliberately ignores the "TPV" report (position/velocity).
set -euo pipefail
SKY=$(jq -c 'select(.class == "SKY")' | head -n1)
[ -z "$SKY" ] && { echo "GNSS: SKY report unavailable"; 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 used: ${N_USED:-unavailable}"
[ -n "$HDOP" ] && echo "HDOP: $HDOP"
EOF
chmod +x /opt/stats/gnss_sky.shThe script never reads the lat, lon, alt, track, speed
fields, not even by accident, since it exclusively isolates the SKY
report (reception health), not TPV (position/velocity).
Publishing via nginx#
location = /chrony_stats.txt {
allow all;
access_log off;
add_header Cache-Control "public, max-age=300";
}Regenerated every 5 minutes via a systemd timer (stats.timer +
stats.service), rather than computed on demand, avoids a client request
triggering an expensive real-time chrony read.
Pitfalls to avoid#
Per-satellite azimuth/elevation angles must never be published, even if they seem harmless. Combined with the public ephemeris of GNSS satellites (each satellite’s position is known at any given time) and a precise timestamp, these angles could in theory allow reconstructing an approximate position via inverse triangulation. This is a risk category of its own, “Location”, distinct from the usual performance or diagnostic risks: always refuse by default, regardless of the metric requested.
- Assuming gpsd’s JSON report always has the same shape. Some
receivers (particularly those using NMEA rather than a proprietary
binary protocol) emit a minimal
SKYreport, without the detailedsatellites[]array, only aggregated root-level fields (uSat,hdop). A script assuming thesatellites[]array is always present will silently produce a misleading “0 satellites” count instead of the real number, a robust script must handle both formats. - Mis-sizing the number of lines read versus the timeout. Requesting
too many lines (
gpspipe -n 30) with too short a timeout can cause systematic truncation, when a single completeSKYline is enough. A small-nvalue that completes naturally, with a generoustimeoutas a pure safety net, beats the reverse. - Using
printfto format numbers extracted from JSON. On a system with a non-English locale (fr_FR, for instance),printfinterprets the decimal separator as a comma and rejects a JSON-formatted number (period), combined withset -e, this silently truncates the output. Always delegate numeric formatting tojq, which is guaranteed locale-independent.
Verify#
sudo systemctl start stats.service
cat /var/time/chrony_stats.txt
# Static check: no position leak in the code
grep -iEw 'lat|lon|alt|track|speed|az|el' /opt/stats/gnss_sky.sh
# Should return NO executable line (only, possibly, explanatory comments)
curl -A "Mozilla/5.0" https://time.example.org/chrony_stats.txtnginx blocks
curl/wgetUser-Agents by default (NGINX chapter), to manually test public access, always pass a browser User-Agent (-A "Mozilla/5.0"), otherwise the test fails for a reason unrelated to telemetry itself.