neenv

Time

The problem

Certificates have a start and an end, revocation lists have a next-update time, tokens expire, backups are judged by their age, and every log line carries a timestamp. All of it assumes the machines agree on the time. A machine whose clock drifts refuses valid certificates, accepts expired ones, or writes log lines that no longer line up with the others, and nothing says why.

Debian's default answers half of this. systemd-timesyncd asks one server, believes it, and keeps no record of whether the answer was honest. This pattern makes every machine ask several servers, believe them only when they agree, authenticate one of them, and report when it cannot.

What runs here

chrony on every machine, declared by configuration management. Before this was written, ten machines here ran timesyncd against one pool server each and three ran chrony from their images; nothing declared which, and nothing watched the result. Now every server and development machine runs chrony against up to five sources:

None of them smears leap seconds. Google's servers do, spreading a leap second over a day; mixed with servers that step, the two disagree by up to half a second around a leap. Use one kind or the other, never both.

Agreement before action. minsources 2: chrony moves the clock only when at least two sources are selectable, that is, close enough to each other to be believed together, so a single bad source cannot steer it. With authselectmode left at its default (mix), chrony prefers the authenticated sources and uses the others only alongside them. makestep 1 3: a clock more than a second out is stepped, in the first three updates after chrony starts only; after that it is slewed, so a service started later never sees time jump backwards (one started earlier in the boot can).

The chicken and the egg. NTS starts with a TLS handshake, which checks a certificate, which needs the clock roughly right. nocerttimecheck 1 lets chrony skip the certificate's time check for its first handshake, while its clock is not yet set. The cost: for that one handshake, an expired certificate would be accepted. Here one machine, a small computer with no clock battery that lives behind a home router abroad, keeps systemd-timesyncd asking two servers by address instead, so its clock does not depend on DNS or a tunnel at boot. It is declared, it is the exception, and its offset is not judged by the rule below (only its sync flag is).

The judges read chrony itself, and the kernel's flag. The first version judged node_exporter's node_timex_offset_seconds, the kernel's own offset. Under chrony that is always 0: chrony steers the clock without the kernel's phase-locked loop, so the alert could never fire. A context-free review caught it; measured here, every chrony machine read exactly 0. Debian's collectors package ships chrony.py, which prints chrony's tracking as metrics, but no timer runs it; a one-minute timer here writes its output to node_exporter's textfile directory. Three alert rules (time.yml):

Alert Fires when
ClockNotSynchronised the kernel's clock is marked unsynchronised (STA_UNSYNC), for 15 minutes
ClockOffset chrony's own offset (chrony_tracking_system_offset_seconds) is more than 50 ms, ahead or behind, for 15 minutes
ChronyFactsStale chrony's facts are more than 10 minutes old: chronyd is down or not answering. The kernel's flag can stay set for hours after chrony dies, so it cannot catch this

A machine whose metrics stop arriving altogether is InstanceDown's, not these.

50 ms is far tighter than certificates need (they tolerate minutes); it is where log lines from different machines first stop lining up.

The configuration, with example values

/etc/chrony/chrony.conf:

server time.cloudflare.com iburst nts
server nts.netnod.se iburst nts
server ptbtime1.ptb.de iburst nts
pool 2.debian.pool.ntp.org iburst maxsources 2
minsources 2
makestep 1 3
nocerttimecheck 1
ntsdumpdir /var/lib/chrony
driftfile /var/lib/chrony/chrony.drift
rtcsync
leapseclist /usr/share/zoneinfo/leap-seconds.list
logdir /var/log/chrony

leapseclist, not leapsectz right/UTC: Debian 13 does not install the right/ zones without tzdata-legacy, and a leapsectz pointing at a missing file does nothing. NTS needs TCP 4460 out (the key exchange) as well as UDP 123.

The facts timer, /etc/systemd/system/chrony-facts.service and .timer (OnUnitActiveSec=1min):

ExecStart=/bin/sh -c 'python3 -I /usr/share/prometheus-node-exporter-collectors/chrony.py > /var/lib/prometheus/node_exporter/.chrony.prom.tmp && mv /var/lib/prometheus/node_exporter/.chrony.prom.tmp /var/lib/prometheus/node_exporter/chrony.prom'

Installing chrony on Debian removes systemd-timesyncd; the two must not both run.

The drill

drill-time.sh needs no credential.

This machine (machine). It asks chrony, read-only, how many sources it lists, how many are usable, which NTS sources hold a key with cookies in hand, whether a source is selected, and how far off the clock is. Needs chronyc, and root (or chrony's group), because chrony shows its NTS state only to them. Run without that, it exits 2 rather than pass a check it could not see.

$ sudo ./drill-time.sh machine
ok    5 sources listed
ok    4 sources usable and agreeing
ok    authenticated (NTS) sources with cookies: time.cloudflare.com nts.netnod.se ptbtime1.ptb.de
ok    a source is selected
ok    offset -0.000793876s, within 50 ms
all checks passed

The judges (rules). promtool's unit tests (time.test.yml) against the rules as published, then against three copies with a guard removed: without the threshold, a clock 1 ms off is reported; without abs, a clock running behind is never reported; without the age threshold, a chrony reporting every minute is called stale. The tests must pass on the first and fail on all three. That second copy failed to fail when the tests first held only a clock running ahead; a clock 200 ms behind was added.

$ ./drill-time.sh rules
ok    as-published: the tests pass, as they must
ok    no-threshold: the tests fail, as they must
ok    no-abs: the tests fail, as they must
ok    no-stale-age: the tests fail, as they must
all checks passed

What is still open