Skip to content
Stribog

Operations

All writing

Trusted Time: Chrony, PTP, and Logs That Hold Up in Audit

CERT-In and EU MiFIR both mandate synchronised clocks. Run traceable time with chrony, PTP and a stratum-1 you own — then prove the error bound afterwards.

Stribog13 min read

Every incident review becomes an argument about ordering. Did the credential get used before the token was revoked, or after? That answer comes from comparing timestamps written by different machines, and is only as good as the agreement between those clocks — which almost nobody measures. The common posture is a default time client on a public pool, reporting a boolean; nobody retains the number saying how wrong it might be, because nobody has asked, until an auditor does and the window has closed.

A timestamp is not evidence

A log line with a time on it asserts something. A log line whose time carries a demonstrable upper bound on its error proves something. Most platforms have never measured that distance.

CERT-In is direct about why. Its FAQ says that "without an accurate time stamp it is extremely challenging to re-create accurate sequence of events," and that "unsynchronised clocks across systems could result in failure of security systems." The second clause is underrated: skew does not merely make forensics awkward, it makes correlation rules silently wrong. Kerberos rejects a message once skew exceeds a default of 300 seconds; certificate windows, token expiry and runtime rules correlating syscalls with network events inherit the same dependency.

What the regulators actually ask for

Two regimes are worth reading closely, and most readers are bound by the weaker one.

CERT-In's Directions of 28 April 2022, at Direction (i), bind service providers, intermediaries, data centres, body corporate and Government organisations to "connect to the Network Time Protocol (NTP) Server of National Informatics Centre (NIC) or National Physical Laboratory (NPL) or with NTP servers traceable to these NTP servers." The FAQ names them: samay1.nic.in, samay2.nic.in, time.nplindia.org. Multi-geography entities may use another standard source provided "their time source shall not deviate from NPL and NIC." There is no numeric divergence figure and no IST requirement, but "the time zone information shall also be recorded along-with time." That direction has been in force since April 2022; check for later amendment rather than assume. Our walkthrough of CERT-In's reporting and retention duties covers what these clocks timestamp.

The EU's business-clock rules are where numbers appear, and where most published guidance is out of date. Commission Delegated Regulation (EU) 2025/1155 repealed RTS 25 (Regulation (EU) 2017/574) with effect from 2 March 2026, and its Articles 11 to 16 apply from that date. Much guidance still describes 2017/574 as live law; it is not. And 2025/1155 supplements MiFIR, not MiFID II, so the familiar shorthand is wrong twice over. A correlation table maps the old numbering: Article 1 to 11, 2 to 12, 3 to 13, 4 to 16, the old Annex to Annex IV.

Scope decides everything. Article 11 binds operators of trading venues and their members, participants or users, systematic internalisers, DPEs, APAs and CTPs — nobody else. Article 12 with Annex IV Table 1 ties venue operators and systematic internalisers to gateway-to-gateway latency: above 1 ms, 1 millisecond divergence at 1 millisecond granularity; at or below it, 100 microseconds at 0,1 microseconds granularity. Article 12(2) derogates that population's voice, human-intervention RFQ and negotiated-transaction systems to 1 second. Article 13 with Table 2 covers members, participants or users in four classes: high frequency algorithmic trading technique at 100 microseconds and 0,1 microseconds granularity, and voice, human-intervention RFQ and concluding negotiated transactions at 1 second each. Articles 14 and 15 give DPEs, APAs and CTPs a flat 1 millisecond — a population RTS 25 did not address.

The clause worth stealing whatever your jurisdiction is Article 16. Operators of trading venues and their members, participants or users must "establish a system of traceability to UTC," demonstrate it "by documenting the system design, functioning and specifications," and "review the compliance of the system of traceability to UTC... at least once a year." If you are bound only by CERT-In, SOC 2 evidence expectations or DORA's ICT risk duties, it is still the right shape to build to.

The number that matters is root distance, not offset

chrony publishes the arithmetic: "An absolute bound on the computer's clock accuracy (assuming the stratum-1 computer is correct) is given by: clock_error <= |system_time_offset| + root_dispersion + (0.5 * root_delay)." Every term is a field chronyc tracking already prints, and almost nobody computes it.

One field choice decides whether the figure is right or merely plausible. The system_time_offset term is the System time line — "the current offset between the NTP clock and system clock." It is not Last offset, "the estimated local offset on the last clock update." Last offset is usually smaller, and substituting it yields an understated bound that looks credible. RMS offset is not a term either.

console
$ chronyc -n tracking
Reference ID    : 0A140B0B (10.20.11.11)
Stratum         : 2
Ref time (UTC)  : Sat Aug 08 05:41:12 2026
System time     : 0.000000234 seconds slow of NTP time   # system_time_offset
Last offset     : -0.000000019 seconds                   # NOT in the bound
RMS offset      : 0.000000041 seconds                    # NOT in the bound
Frequency       : 12.317 ppm slow
Residual freq   : +0.000 ppm
Skew            : 0.041 ppm
Root delay      : 0.000318442 seconds                    # root_delay
Root dispersion : 0.000094117 seconds                    # root_dispersion
Update interval : 64.3 seconds
Leap status     : Normal

# bound <= 0.000000234 + 0.000094117 + 0.000159221 = 0.000253572 s (254 us)
Annotated `chronyc tracking` from a node synchronised to an internal stratum-1 pair. Only three of these lines feed chrony's documented bound.

Read that against the tiers above: 254 microseconds satisfies a 1 millisecond duty and fails a 100 microsecond one. Note where the error lives — System time contributes 234 nanoseconds, half the root delay 159 microseconds. The network path dominates.

Two shipping defaults matter first. maxdistance caps the root distance a source may have to be usable — a distance that "includes the root dispersion and half of the root delay" — and "by default, the maximum root distance is 3 seconds." minsources, the count of sources that must agree "before the local clock is updated," defaults to 1. A stock chrony therefore accepts a source estimating three seconds of its own error, and moves your clock on one peer's word. Indefensible where logs are evidence.

bash
#!/usr/bin/env bash
# bound = |system_time_offset| + root_dispersion + (0.5 * root_delay)
# system_time_offset is the "System time" line — NOT "Last offset".
set -euo pipefail

BUDGET_SECONDS="${BUDGET_SECONDS:-0.001}"
tracking="$(chronyc -n tracking)"
field() { printf '%s\n' "$tracking" | sed -n "s/^$1[[:space:]]*: //p"; }

# "System time" prints a magnitude, so awk's abs() is belt and braces.
system_time="$(field 'System time'   | awk '{print $1}')"
root_delay="$(field 'Root delay'     | awk '{print $1}')"
root_disp="$(field 'Root dispersion' | awk '{print $1}')"
[ -n "$system_time" ] && [ -n "$root_delay" ] && [ -n "$root_disp" ] ||
  { echo "could not parse chronyc tracking" >&2; exit 2; }

bound="$(awk -v s="$system_time" -v rd="$root_delay" -v rp="$root_disp" \
  'BEGIN { if (s < 0) s = -s; printf "%.9f", s + rp + (rd / 2) }')"

printf 'bound=%s budget=%s\n' "$bound" "$BUDGET_SECONDS"
awk -v b="$bound" -v g="$BUDGET_SECONDS" 'BEGIN { exit !(b <= g) }' && exit 0
echo "clock error bound ${bound}s exceeds budget ${BUDGET_SECONDS}s" >&2
exit 1
Computes chrony's documented bound and fails when it exceeds the budget. Run from cron for continuous evidence, or as a pipeline gate.

Architecture: a stratum-1 you own

Traceability is a chain, and every link is either something you operate or something you depend on. The reference layer is a GNSS receiver feeding chronyd two things: a serial stream numbering the second, and a pulse-per-second signal marking its edge. They are not interchangeable — "as PPS refclocks do not supply full time, another time source... is needed to complete samples from the PPS refclock." PPS gives precision, NMEA correctness; one without the other does not work.

Build it as a pair, in separate racks, with separate antenna runs: a single stratum-1 is a single point of both failure and lying, and minsources 2 downstream is meaningless with one upstream to agree with. Holdover quality — how well each keeps time while its antenna is dark — is what buys a roof maintenance window without a compliance incident.

The distribution tier exists so nodes never talk to the reference directly and every hop is authenticated. Network Time Security, RFC 8915, is the mechanism: NTS-KE over TLS for key establishment, NTP extension fields protecting the time packets. chrony has shipped it since 4.0 in October 2020; 4.8, from August 2025, is current. An attacker "can drop or delay NTP packets... but they cannot modify the timestamps" — with a caveat that matters as much: "when authentication is enabled for an NTP source, it is important to disable unauthenticated NTP sources." One NTS server among three unauthenticated ones buys nothing.

Error accumulates down the chain; evidence is captured beside it. The chronyc bound at the node and the kernel's node_timex metrics are different quantities — keep both, and cite the chronyc figure.

Configuration that ships

The stratum-1 locks a PPS refclock to an NMEA source, serves NTS, and makes an unexpected step a loud failure rather than a silent rewrite of history.

conf
# NMEA numbers the second; noselect means it is used for locking only.
refclock SHM 0 refid NMEA offset 0.5 delay 0.2 noselect
# PPS supplies the edge, not full time, so it locks to NMEA.
refclock PPS /dev/pps0 refid PPS lock NMEA prefer

# External anchors: cross-check and traceability, never the master.
server time.nplindia.org iburst maxpoll 10
server samay1.nic.in iburst maxpoll 10

ntsservercert /etc/chrony/nts/fullchain.pem
ntsserverkey  /etc/chrony/nts/privkey.pem
allow 10.20.0.0/16
bindaddress 10.20.11.11

# Step only in the first three updates. After that a large offset is an
# incident: maxchange ignores it, logs, exits.
makestep 1.0 3
maxchange 0.1 3 1

logdir /var/log/chrony
log tracking measurements statistics
/etc/chrony/chrony.conf on an on-premise GNSS-disciplined stratum-1.

Node configuration corrects the two defaults: three authenticated sources, two required to agree, and a root distance far tighter than stock.

conf
server time1.internal.example.net iburst nts minpoll 4 maxpoll 6
server time2.internal.example.net iburst nts minpoll 4 maxpoll 6
server time.cloudflare.com iburst nts

# Defaults are minsources 1 and maxdistance 3 seconds. Require agreement,
# and reject a source estimating over 50 ms of its own error.
minsources 2
maxdistance 0.05

makestep 0.1 3
maxchange 0.1 3 1

# tracking.log is the retained evidence record.
logdir /var/log/chrony
log tracking measurements

hwtimestamp *
/etc/chrony/chrony.conf on cluster nodes. Every source is authenticated, which is what makes the NTS guarantee hold.

Talos Linux is the case most often missed. It does not run chrony: as of v1.11 it "implements SNTP protocol to sync time with the NTP server," and "by default, Talos Linux uses time.cloudflare.com as the NTP server." A good public server is still somebody else's, and a chain terminating outside the organisation cannot be documented as yours. Time is an availability dependency there too: "some components like kubelet and etcd wait for the time to be in sync before starting, as they don't support graceful certificate rotation."

yaml
# talosctl patch machineconfig --patch-file time-sync.yaml --nodes <node-ip>
machine:
  time:
    disabled: false
    servers:
      - time1.internal.example.net
      - time2.internal.example.net
Talos machine config patch replacing the default public server with the stratum-1 pair you operate.

Its native signal is talosctl get timestatus, a SYNCED column of true or false — fine as liveness, useless as evidence, which is why the bound must be computed elsewhere. The same applies to systemd-timesyncd, whose manual page states it "implements SNTP only" and that "complex use cases that require full NTP support... are not covered." It has no root distance to report because it does not track one.

When milliseconds are not enough: PTP on Kubernetes

PTP answers one question: the budget is tighter than a software-timestamped network path can defend. Reach for it when Annex IV Table 1's 100 microsecond tier binds you as a venue operator or systematic internaliser, or Table 2's HFT class binds you as a member, participant or user. Not because it sounds more rigorous: it adds hardware, a second daemon and switch requirements you own for a decade.

The linuxptp split matters before buying anything. ptp4l implements "the Precision Time Protocol (PTP) according to IEEE standard 1588 for Linux," covering Boundary, Ordinary and Transparent Clock roles, with hardware timestamping as its default and one constraint: all ports "must be attached to the same PTP hardware clock (PHC)." But ptp4l disciplines the PHC, not the system clock. phc2sys "synchronizes the system clock to a PTP hardware clock (PHC), which itself is synchronized by the ptp4l program." And ts2phc "synchronizes PTP Hardware Clocks (PHC) to external time stamp signals" — how a GNSS PPS becomes a PTP domain you own.

bash
#!/usr/bin/env bash
set -euo pipefail
IFACE="${IFACE:-eno1}"

# 1. Confirm a PTP hardware clock: hardware-transmit, hardware-receive,
#    and a PTP Hardware Clock index that is not "none".
ethtool -T "$IFACE"

# 2. Discipline the PHC. -H is hardware timestamping (the default),
#    -s slave-only, -m log to stdout.
ptp4l -i "$IFACE" -m -H -s &
ptp4l_pid=$!

# 3. ptp4l does not touch the system clock; phc2sys copies the PHC into
#    CLOCK_REALTIME, waiting (-w) for ptp4l first.
phc2sys -s "$IFACE" -w -m &
phc2sys_pid=$!

trap 'kill "$ptp4l_pid" "$phc2sys_pid" 2>/dev/null || true' EXIT
wait
PTP bring-up on a node. Step one is not optional: without hardware timestamping in the NIC, the reason for running PTP disappears.
Budgets are scoped to populations, not to everyone running Kubernetes. Most readers land in the left column and are served by chrony; PTP is forced by the microsecond tiers, not chosen for rigour.

Failure modes that silently destroy evidence

Each of these reports healthy. That is what makes it expensive.

  • Leap-smear mixing. AWS documents that its own two sources disagree during a leap event: "the NTP time source offers a leap smearing view of the UTC timescale, while the PHC does not smear time." Its guidance is scoped, not absolute — do not use "both smeared and non-smeared time sources in your time client configuration during a leap second event." Control: know which sources smear, and do not mix classes in one client.
  • Silent steps. A daemon that quietly steps the clock backwards rewrites the ordering of everything already written. maxchange makes it loud instead: a syslog message when an offset is ignored, and chronyd exiting after a repeat. Control: maxchange on every node, alert on chronyd restarts.
  • Single-source capture. With minsources 1, one compromised or misconfigured server moves the whole fleet consistently, so nothing looks anomalous. GNSS jamming and spoofing make this live for a stratum-1 you own. Control: minsources 2 or higher, an NTS-authenticated cross-check outside the GNSS path, maxchange as backstop.
  • Health signals that are not measurements. Kerberos tolerating 300 seconds of skew means authentication keeps working through drift that already made your logs uncorrelatable. Talos reports SYNCED true; systemd-timesyncd has no root distance at all; node_timex_sync_status is a 1 or a 0. Each is true right up to the moment it is useless. Control: monitor the bound, and retain a number rather than a state.

Proving it afterwards: the evidence pack

An evidence pack has three parts, and the common failure is shipping only the first.

The first is retained measurement: log tracking measurements on every node, kept as long as the logs it qualifies, so the bound at the moment of an incident is reconstructable, not re-measured months later. The second is a continuous monitor — node_exporter's timex collector already exports node_timex_sync_status and node_timex_maxerror_seconds into an existing observability stack, with no new agent.

yaml
apiVersion: monitoring.coreos.com/v1
kind: PrometheusRule
metadata:
  name: time-trustworthiness
  namespace: monitoring
spec:
  groups:
    - name: clock-evidence
      rules:
        - alert: NodeClockNotSynchronised
          expr: node_timex_sync_status == 0
          for: 10m
          labels:
            severity: critical
          annotations:
            description: >-
              Timestamps from this node carry no defensible bound until sync
              is restored.
        - alert: NodeClockMaxErrorOverBudget
          # Kernel adjtimex maxerror: the continuous monitor, NOT the chronyc
          # tracking bound retained in the evidence pack.
          expr: node_timex_maxerror_seconds > 0.001
          for: 15m
          labels:
            severity: warning
          annotations:
            description: >-
              Estimated maximum clock error exceeds the divergence budget.
PrometheusRule turning the clock into a monitored budget. Set the threshold to the divergence figure your population is actually bound by.

The third part is the document, and the one engineers skip. Article 16's language is a usable specification outside its scope too: document the design, identify the exact point at which a timestamp is applied, review it at least once a year. A design reviewed annually is a control; one never revisited is an assertion with a date on it.

Exit ramps and the long game

Time is one of the few subsystems where the sovereign option is also the cheap one. NTP, NTS and PTP are open protocols with several independent implementations; chrony and linuxptp are free software; a PPS output is an electrical pulse every implementation understands. Keep it that way: terminate traceability inside your own boundary, buy timing hardware on PPS and protocol support rather than on a management plane, and keep the distribution tier on plain NTP with NTS so repointing a node is a one-line change.

The long-game item is already scheduled. The CGPM decided in 2022 that "the maximum value for the difference (UT1-UTC) will be increased in, or before, 2035" — the change that ends leap seconds as we have handled them. Infrastructure specified now will still be running then, which argues for leap handling in software you can update rather than firmware you cannot, and for labelling smeared and non-smeared sources today.

None of this is exotic: a receiver, two servers, four corrected directives, a script computing one number, a document reviewed yearly. What it buys is the difference between logs that describe what happened and logs that prove it.

§FAQ/Common questions

Frequently asked

Does CERT-In require Indian systems to run their clocks in IST?

No. CERT-In's FAQ on the 2022 Directions states there is "no need to mandatorily set system clocks in Indian Standard Time (IST)," because NTP provides timestamps in UTC and conversion happens on the receiving host. What it does require is that "the time zone information shall also be recorded along-with time," so a timestamp can be converted accurately later. Direction (i) separately requires synchronisation to NIC or NPL NTP servers, or to servers traceable to them.

What replaced MiFID II RTS 25 for clock synchronisation?

Commission Delegated Regulation (EU) 2025/1155, which supplements MiFIR (Regulation (EU) No 600/2014) rather than MiFID II. It repealed RTS 25 (Regulation (EU) 2017/574) with effect from 2 March 2026, and its business-clock provisions, Articles 11 to 16, apply from that date. It carries a correlation table mapping the old article numbers onto the new ones. Scope widened to include systematic internalisers, DPEs, APAs and CTPs, and the tightest granularity moved from 1 microsecond to 0,1 microseconds.

Do I need PTP, or is NTP with chrony enough?

For almost everyone, chrony is enough. PTP earns its cost when a divergence budget in the microsecond range binds you — the 100 microsecond tiers in Annex IV of Regulation (EU) 2025/1155, which apply to venue operators and systematic internalisers under Table 1, and to members, participants or users employing a high frequency algorithmic trading technique under Table 2. PTP requires NICs with a PTP hardware clock, PTP-aware switching, and a second daemon (phc2sys) to steer the system clock from the PHC.

Can I fix clock skew inside a container or pod?

No. Linux time namespaces virtualize only CLOCK_MONOTONIC and CLOCK_BOOTTIME — CLOCK_REALTIME is not virtualized, so every pod reads the wall clock of the node it runs on. Clock correctness is a node-fleet problem, addressed on the host with chrony or linuxptp. Kubernetes audit timestamps inherit the same constraint: requestReceivedTimestamp and stageTimestamp are stamped by the API server, so their accuracy is bounded by the control-plane node's clock.

What single number should go into a clock evidence pack?

chrony's documented bound: clock_error is at most the absolute value of the System time offset, plus root dispersion, plus half the root delay — all read from chronyc tracking. Use the System time line, not Last offset or RMS offset, which are different quantities and produce an understated figure. Retain that number over time via chrony's tracking log rather than sampling it once at audit time, and alert continuously on node_timex_maxerror_seconds as a separate, related signal.

chronyptp precision time protocol kubernetestime source traceability audit logscert-in ntp synchronisation requirementgps stratum 1 ntp server on premisemifid ii clock synchronisation

Executive Briefing

Thirty minutes to clarify your infrastructure risk

Walk us through your vendor footprint and regulatory constraints. We will tell you honestly where sovereignty creates leverage — and where it does not. No pitch deck. No obligation.