chrony PTB/NTS alerts¶
Source rule: cluster/monitoring/chrony-ptb-alerts.yaml (PrometheusRule chrony-ptb-alerts, namespace monitoring, group chrony-ptb-conformance).
Scope and remediation path (read first)¶
Node clock policy is not an ArgoCD workload. It is node-level configuration
owned by the Ansible role ansible/roles/chrony_ptb and baked into the Packer
node image. Every alert in this group is fixed by changing that role in Git and
re-running ansible/playbooks/tune-cluster-ntp-ptb.yml — not by syncing an
Application and not by editing files on a node. See
docs/runbooks/chrony-ptb-nts.md for the rollout
procedure.
Desired state: exactly four hostname-based PTB sources (ptbtime1.ptb.de …
ptbtime4.ptb.de), NTS-only (authselectmode require), minsources 2, zero
Ubuntu/DHCP/fallback sources.
Relevant on-node paths:
| Thing | Path |
|---|---|
| Sources | /etc/chrony/sources.d/ptb.sources |
| Policy | /etc/chrony/conf.d/ptb-policy.conf |
| Main config | /etc/chrony/chrony.conf |
| Exporter script | /usr/local/sbin/chrony-ptb-textfile-exporter |
| Textfile output | /var/lib/node_exporter/textfile/chrony-ptb.prom |
| Units | chrony.service, chrony-ptb-textfile-exporter.{service,timer} |
All commands below are read-only. This repo is strictly GitOps for cluster state and Ansible-managed for node state: do not mutate either from a shell.
ChronyPTBExporterMissing¶
- Severity: warning · for: 10m
- Fires when: an instance present in
node_uname_info{job="node-exporter"}has nochrony_ptb_exporter_success{job="node-exporter"}series at all (unless on(instance)), for 10 minutes. This is series absence, not a value comparison.
Likely causes¶
chrony-ptb-textfile-exporter.timeris not enabled or its service failed, so no textfile is produced.- The node was built from an image predating the
chrony_ptbrole, or the playbook has not run against it. - node-exporter is not reading
/var/lib/node_exporter/textfile, or the file is unreadable.
Diagnose (read-only)¶
ssh <node> systemctl status chrony-ptb-textfile-exporter.timer chrony-ptb-textfile-exporter.service
ssh <node> ls -la /var/lib/node_exporter/textfile/
ssh <node> journalctl -u chrony-ptb-textfile-exporter.service --no-pager -n 50
node_uname_info{job="node-exporter"} unless on(instance) chrony_ptb_exporter_success{job="node-exporter"}
Resolution — Git change. Add the node to the inventory and/or re-run
tune-cluster-ntp-ptb.yml; if the unit template is wrong, fix
ansible/roles/chrony_ptb/templates/ and re-run the playbook. Do not hand-place
the unit on the node.
ChronyPTBExporterFailed¶
- Severity: warning · for: 5m
- Fires when:
chrony_ptb_exporter_success{job="node-exporter"} != 1for 5 minutes. Note this is!= 1, so a missing/garbled value also fires, not only a literal0.
Likely causes¶
chronyc -n -c sourcesfailed —chrony.serviceis down, wedged, or its socket is unreachable.- The exporter's systemd sandbox (
ProtectSystem=strict,PrivateDevices) is blocking something it needs. - Malformed chronyc output the parser could not read.
Diagnose (read-only)¶
ssh <node> systemctl status chrony.service
ssh <node> journalctl -u chrony-ptb-textfile-exporter.service --no-pager -n 50
ssh <node> chronyc -n -c sources
ssh <node> cat /var/lib/node_exporter/textfile/chrony-ptb.prom
Resolution — If chrony.service itself is unhealthy, that is the real
incident; investigate its logs first. Any durable fix (unit hardening, exporter
script logic) is a Git change to ansible/roles/chrony_ptb followed by a
playbook run.
ChronyPTBExporterStale¶
- Severity: warning · for: 5m
- Fires when:
abs(time() - chrony_ptb_exporter_timestamp_seconds{job="node-exporter"}) > 300for 5 minutes. Theabs()matters: this fires when the node timestamp is more than 300s behind or ahead of Prometheus — a node clock running fast trips it just as readily as a stalled timer.
Likely causes¶
- The exporter timer is not firing (interval default is 60s) or the oneshot service hangs.
- Severe node clock skew — which is itself the thing this group exists to catch. Cross-check
ChronyPTBNotSynchronised. - Clock skew on the Prometheus side, if many nodes go stale simultaneously.
Diagnose (read-only)¶
ssh <node> systemctl list-timers chrony-ptb-textfile-exporter.timer --all
ssh <node> stat /var/lib/node_exporter/textfile/chrony-ptb.prom
ssh <node> chronyc tracking
kubectl top nodes
Resolution — If only the timer stalled, resolving the underlying node issue
restores it. Interval changes (chrony_ptb_exporter_interval) are a Git change
in ansible/roles/chrony_ptb/defaults/main.yml. If the node clock is genuinely
skewed, treat ChronyPTBNotSynchronised as the primary alert.
ChronyPTBConfigurationDrift¶
- Severity: critical · for: 5m
- Fires when: any of the following holds for 5 minutes:
chrony_ptb_policy_configured{job="node-exporter"} != 1, orchrony_ptb_configured_sources{job="node-exporter"} != 4, orchrony_ptb_non_ptb_sources{job="node-exporter"} != 0
Note configured_sources must be exactly 4 — five configured sources
fires this just as three does.
Likely causes¶
- Node config edited outside Ansible, or a package upgrade re-added a distro default.
- A DHCP/cloud-init hook injected an NTP source, raising
non_ptb_sources. - The node booted an image predating the current
chrony_ptbrole.
Diagnose (read-only)¶
ssh <node> cat /etc/chrony/sources.d/ptb.sources
ssh <node> cat /etc/chrony/conf.d/ptb-policy.conf
ssh <node> grep -nE '^(server|pool|peer|sourcedir|include)' /etc/chrony/chrony.conf
ssh <node> chronyc -n sources
chrony_ptb_policy_configured != 1 or chrony_ptb_configured_sources != 4 or chrony_ptb_non_ptb_sources != 0
Resolution — Git change. Re-run tune-cluster-ntp-ptb.yml (use --check
--diff first to see the drift) to restore the managed files. If the drift came
from something outside the role — a distro package, cloud-init — fix the source
of the injection in the role or the Packer template, otherwise it returns on the
next boot.
ChronyPTBDesiredSourceMissing¶
- Severity: critical · for: 5m
- Fires when:
chrony_ptb_source_configured{job="node-exporter"} != 1for 5 minutes. This is per-hostname — the series carries asourcelabel (ptbtime1.ptb.de…ptbtime4.ptb.de); check{{ $labels.source }}to see which one is absent. The metric asserts the hostname appears exactly once with NTS.
Likely causes¶
- A hostname was removed, misspelled, or duplicated in
chrony_ptb_sources. - The source line lost its
ntsoption. - The playbook has not been re-run since the source list changed.
Diagnose (read-only)¶
Desired list lives in ansible/roles/chrony_ptb/defaults/main.yml (chrony_ptb_sources).
Resolution — Git change. Correct chrony_ptb_sources or the sources
template in the role, then re-run the playbook. There is no ConfigMap and no
Application for this.
ChronyPTBActiveSourceCountUnexpected¶
- Severity: warning · for: 30m
- Fires when:
chrony_ptb_observed_sources{job="node-exporter"} != 4for 30 minutes. Exactly four — this fires on five active sources as well as on three. The long window tolerates transient PTB maintenance.
Likely causes¶
- DNS failure for one PTB hostname, so chrony never instantiates that source.
- NTS-KE (TCP/4460) blocked or failing for one endpoint.
- An untracked runtime source appeared (count above four).
Diagnose (read-only)¶
ssh <node> chronyc -n sources
ssh <node> chronyc ntpdata
ssh <node> getent hosts ptbtime1.ptb.de ptbtime2.ptb.de ptbtime3.ptb.de ptbtime4.ptb.de
ssh <node> journalctl -u chrony.service --no-pager -n 100
Resolution — Transient PTB-side or network issues self-recover; the 30m
window already absorbs short blips. A count above four means drift — see
ChronyPTBConfigurationDrift and fix via the role in Git. Persistent DNS or
firewall problems are infrastructure fixes, not node edits.
ChronyPTBInsufficientAgreement¶
- Severity: critical · for: 5m
- Fires when:
chrony_ptb_selectable_sources{job="node-exporter"} < 2for 5 minutes.
This is the policy boundary: minsources 2 means chronyd deliberately stops
updating the clock below two agreeing sources. The node clock is now free-
running and will drift.
Likely causes¶
- Fewer than two sources reachable (NTP/UDP 123) or authenticated (NTS-KE/TCP 4460).
- Sources reachable but rejected by selection as falsetickers.
- Local offset already so large that sources are discarded.
Diagnose (read-only)¶
ssh <node> chronyc tracking
ssh <node> chronyc -n sources -v
ssh <node> chronyc selectdata
ssh <node> chronyc ntpdata
Resolution — Restore reachability/authentication for at least two PTB
sources; the alert clears on its own once selection recovers. Do not lower
minsources to silence this — that setting is the conformance guarantee, and
changing it is a Git change to ansible/roles/chrony_ptb/files/ptb-policy.conf
that needs an explicit decision, not an incident-time workaround.
ChronyPTBNTSAuthenticationInsufficient¶
- Severity: critical · for: 5m
- Fires when:
chrony_ptb_authenticated_sources{job="node-exporter"} < 2for 5 minutes.
Likely causes¶
- NTS-KE (TCP/4460) blocked by a firewall or upstream path.
- CA trust store on the node cannot validate the PTB certificate chain.
- DNS returns addresses whose certificate does not match the hostname (this is why sources are pinned by hostname, never by IP).
- Node clock skew large enough to fail TLS validity checks — a feedback loop with
ChronyPTBNotSynchronised.
Diagnose (read-only)¶
ssh <node> chronyc ntpdata
ssh <node> chronyc authdata
ssh <node> getent hosts ptbtime1.ptb.de
ssh <node> openssl s_client -connect ptbtime1.ptb.de:4460 -servername ptbtime1.ptb.de </dev/null
Resolution — Under authselectmode require, unauthenticated sources are
never eligible, so this alert overlaps ChronyPTBInsufficientAgreement. Fix the
path (firewall, DNS, CA bundle). CA-bundle or source-list changes are Git
changes to the role plus a playbook run.
ChronyPTBSourceUnreachable¶
- Severity: warning · for: 30m
- Fires when:
chrony_ptb_active_source_reachable{job="node-exporter"} == 0for 30 minutes. Per-address, labelledaddress— the metric tracks chrony's reach register for one of the four active NTS-assigned addresses.
Redundancy is intact as long as two other sources still agree; this is a warning, not a clock-loss condition.
Diagnose (read-only)¶
ssh <node> chronyc -n sources -v
ssh <node> chronyc ntpdata
ssh <node> ping -c 3 <address>
kubectl get node <node-name> -o wide
Resolution — A single PTB endpoint under maintenance recovers on its own; confirm the other three are healthy and let it clear. If all four are unreachable, escalate to egress/firewall — do not change node network config by hand.
ChronyPTBSourceAuthenticationFailed¶
- Severity: warning · for: 10m
- Fires when: for the same
(instance, address)pair, both:chrony_ptb_active_source_reachable{job="node-exporter"} == 1andchrony_ptb_active_source_authenticated{job="node-exporter"} == 0, for 10 minutes. Packets arrive but are not NTS-authenticated.
Likely causes¶
- NTS cookies exhausted or the NTS-KE re-handshake is failing while NTP/123 still answers.
- Certificate chain no longer validates for that address.
- Node clock skew breaking TLS validity windows.
Diagnose (read-only)¶
ssh <node> chronyc ntpdata
ssh <node> chronyc authdata
ssh <node> journalctl -u chrony.service --no-pager | grep -i -E 'nts|cert|auth'
Resolution — Under authselectmode require this address is not eligible for
selection, so it silently reduces redundancy. Fix the NTS-KE path or CA trust.
Trust-store and source changes are Git changes to the role. If the node clock is
badly off, resolve ChronyPTBNotSynchronised first — it can be the cause here,
not the effect.
ChronyPTBNotSynchronised¶
- Severity: critical · for: 5m
- Fires when:
chrony_ptb_synchronised{job="node-exporter"} != 1for 5 minutes — no PTB source is selected.
The node clock is free-running. Expect downstream damage: TLS validation failures, etcd lease churn, Kubernetes token/certificate rejections.
Diagnose (read-only)¶
ssh <node> chronyc tracking
ssh <node> chronyc -n sources -v
ssh <node> chronyc selectdata
ssh <node> chronyc ntpdata
kubectl get nodes
kubectl -n kube-system get pods --field-selector spec.nodeName=<node-name>
Resolution — Almost always a downstream symptom: work
ChronyPTBInsufficientAgreement / ChronyPTBNTSAuthenticationInsufficient
first, since chronyd will resynchronise by itself once two authenticated
sources agree. Do not force a step with chronyc makestep or a manual
date set — that bypasses the conformance policy this whole group enforces and
can step control-plane clocks under running etcd. If the node cannot recover,
escalate rather than stepping the clock. Config fixes go through the role in Git.
ChronyPTBClockOffsetHigh¶
- Severity: warning · for: 5m
- Fires when:
abs(chrony_ptb_last_offset_seconds{job="node-exporter"}) > 0.05for 5 minutes — last measured offset above 50 ms in either direction.
Likely causes¶
- Network latency or asymmetric routing to PTB, inflating measured offset.
- Source disagreement causing the selected root to oscillate.
- A recent source switch, after which chrony is still converging.
- Host-level clock instability (virtualised TSC, CPU frequency effects).
Diagnose (read-only)¶
abs(chrony_ptb_last_offset_seconds{job="node-exporter"}) > 0.05
chrony_ptb_rms_offset_seconds{job="node-exporter"}
chrony_ptb_rms_offset_seconds distinguishes a transient spike from sustained
error — it is exported but has no alert of its own.
Resolution — Short excursions after a source change are normal and clear
themselves. Sustained offset points at network path quality or host clock
instability (check the hypervisor for virtualised nodes). Chrony tuning changes
are Git changes to ansible/roles/chrony_ptb plus a playbook run.
Coverage¶
All 12 alerts in the source group are documented above: ChronyPTBExporterMissing,
ChronyPTBExporterFailed, ChronyPTBExporterStale, ChronyPTBConfigurationDrift,
ChronyPTBDesiredSourceMissing, ChronyPTBActiveSourceCountUnexpected,
ChronyPTBInsufficientAgreement, ChronyPTBNTSAuthenticationInsufficient,
ChronyPTBSourceUnreachable, ChronyPTBSourceAuthenticationFailed,
ChronyPTBNotSynchronised, ChronyPTBClockOffsetHigh.
Exported but unalerted: chrony_ptb_reachable_sources,
chrony_ptb_active_source_selectable, chrony_ptb_tracking_stratum,
chrony_ptb_rms_offset_seconds — useful as supporting signal during triage.