Linux Time Synchronization Mastery: Chrony Best Practices for Production Systems
Comprehensive guide covering Linux time concepts, NTP protocol, chrony vs ntpd, clock source selection, leap second handling, configuration templates for cloud, containers, Kubernetes, and isolated networks, plus verification, monitoring, troubleshooting, compliance automation, and rollback strategies.
Core Time Concepts
Linux time synchronization involves multiple layers: system time (kernel-maintained wall clock via CLOCK_REALTIME), hardware clock (RTC) (battery-backed, accessed via /dev/rtc0 and hwclock), monotonic clock ( CLOCK_MONOTONIC, unaffected by NTP adjustments, critical for timeouts/heartbeats), and UTC/timezone management ( /etc/localtime, timedatectl). The article emphasizes UTC mode for RTC ( timedatectl set-local-rtc 0) and consistent timezone handling across clusters.
NTP Protocol Fundamentals
NTP (RFC 5905) uses UDP port 123. Key terms: stratum (distance from reference clock, 0=atomic/GPS, 1=directly connected, up to 15), offset (local vs upstream time difference), delay (round-trip network latency), jitter (offset variance), reach (8-bit shift register of recent poll success, 377 octal = all 8 successful), poll (interval as power of two seconds), refid (upstream identifier), leap (leap second status). The packet exchange uses four timestamps (T1–T4) to compute offset and delay; chrony employs Kalman filtering for better estimation.
Chrony vs Ntpd
Chrony advantages: faster convergence (seconds vs minutes), better for intermittent networks (laptops, cloud, containers), superior clock skew estimation in VMs, default slewing (gradual adjustment via adjtimex) to avoid time jumps, simpler config, built-in NTS (Network Time Security) support, richer chronyc CLI. Ntpd advantages: longer history, wider default availability on older distros, strict RFC 5905 compliance for legacy devices, more extensive documentation/auditing resources. Current status: RHEL 8/9, Ubuntu 18.04+, Debian 10+, SUSE 15+, and major cloud images default to chrony. Recommendation: new deployments → chrony; legacy ntpd clusters → migrate after test validation; embedded/BusyBox → choose by footprint; strict compliance → chrony + NTS.
Clock Source Selection
Cloud provider internal NTP (low latency, <5 ms): Alibaba Cloud ntp.aliyun.com / ntp.cloud.aliyuncs.com, Tencent Cloud time[1-5].cloud.tencent.com, Huawei Cloud ntp.myhuaweicloud.com, AWS 169.254.169.254 (Amazon Time Sync Service), Azure time.windows.com, GCP metadata.google.internal. Public pools: pool.ntp.org (regional subdomains), time.cloudflare.com (NTS), time.google.com, time.apple.com (5–50 ms, avoid for regulated environments). Self-hosted stratified architecture: GPS/BeiDou/atomic clock (stratum-0) → stratum-1 servers (1–2 HA nodes in core DC) → stratum-2 servers (2–4 per region) → clients. Stratum-1 options: Raspberry Pi + GPS/PPS, commercial receivers (Meinberg, Symmetricom), PTP-to-NTP gateways, or cloud provider internal NTP.
Leap Seconds, Stepping, Slewing
Leap seconds (inserted/deleted by IERS) handled via kernel leap flag, step ( chronyc makestep), or smear (distribute 1 second over 24h). Chrony supports leapsecmode slew, smeartime. Slewing (default) adjusts frequency via adjtimex (~0.5 ms/s), no jumps but slow convergence for large offsets. Stepping uses settimeofday for immediate correction; chrony steps only when offset > makestep threshold (default 1 sec) and during first 3 updates ( makestep 1.0 3). Applications caching time differences (token TTL, HTTP Expires) break on step; use monotonic clock for intervals.
Chrony Architecture & Configuration
chronyddaemon reads /etc/chrony.conf (RHEL) or /etc/chrony/chrony.conf (Debian), initializes sources, attempts initial step, then continuously polls, estimates offset/delay/jitter, slews system time, periodically syncs to RTC ( rtcsync), can serve NTP to clients ( allow + local), and maintains measurement/tracking statistics. Key chrony.conf directives grouped by function:
Upstream sources: pool pool.ntp.org iburst maxsources 4, server time1.aliyun.com iburst, peer ntp01.internal iburst, refclock SHM 0 offset 0.5 delay 0.2; options: iburst (8 quick packets at start), minpoll / maxpoll (poll interval 2^n seconds), prefer, trust, xleave / nts (interleaved/NTS modes).
Access control: allow 192.168.0.0/16, deny 0.0.0.0/0.
Sync behavior: makestep 1.0 3, rtcsync, leapsecmode slew, smeartime 86400.
Client/server: local stratum 10 (serve time even without upstream), manual (manual chronyc settime).
Logging: log tracking measurements statistics, logdir /var/log/chrony, mailonchange.
Chronyc Diagnostic Commands
Essential subcommands: chronyc tracking (overall status: Reference ID, Stratum, System time offset, Last offset, RMS offset, Frequency ppm, Root delay/dispersion, Leap status), chronyc sources -v (source list with state symbols: ^* selected, ^+ candidate, ^- backup, =? unconfirmed, ~ high jitter, M anomaly, x rejected, Reach octal), chronyc sourcestats -v (long-term stats), chronyc activity (recent poll results), chronyc ntpdata (raw NTP packets), chronyc waitsync 30 0.01 (block until offset < 10 ms), chronyc makestep (force step), chronyc clients / serverstats (server mode), chronyc add/delete (runtime source changes), chronyc accheck/certif/cacert (NTS).
Environment Preparation
Minimal hardware: 1 CPU, 512 MB RAM, 100 MB disk. Physical machines preferred for self-hosted NTP servers (better clock stability). Network: UDP 123 (NTP), TCP 4460 (NTS KE). OS support: RHEL 7–9, CentOS/Rocky/Alma, Ubuntu 18.04–24.04, Debian 10–12, SUSE 12/15, openEuler, UOS, Kylin. Kernel 3.10+ (2.6.32 works with chrony 3.x/4.x). Chrony versions: 1.x/2.x/3.x (legacy, no NTS), 4.x (mainstream, NTS from 4.2+). Verify with chronyd -v.
Practical Implementation Steps
Step 1: Baseline Assessment
Collect current state before changes: date, date -u, timedatectl, hwclock, systemctl status chronyd/ntpd/systemd-timesyncd, chronyc tracking, chronyc sources -v, chronyc sourcestats -v, chronyc activity, ss -ulnp | grep 123, tcpdump -i any -n udp port 123 -c 20. Record in a table (hostname, timezone, RTC mode, chrony version, upstream, offset, reach, notes). Health thresholds: |System time| < 100 ms (public) / < 1 ms (internal), |Last offset| < 1 s, RMS offset < 1 s, Frequency near 0 ± 50 ppm, Leap status = Normal, selected source ( ^* / ^+) with Reach=377.
Step 2: Timezone & RTC Configuration
Set timezone: timedatectl set-timezone Asia/Shanghai (IANA names from /usr/share/zoneinfo). Force UTC RTC: timedatectl set-local-rtc 0. Verify with timedatectl, date, date -u, hwclock --show --utc. Containers inherit host /etc/localtime; override via volumeMounts in Pod spec or Dockerfile ENV TZ=Asia/Shanghai + symlink. Application-level: JVM -Duser.timezone, Python os.environ['TZ'] + time.tzset(), Node.js TZ, MySQL time_zone. Recommend UTC for all servers/containers; convert at presentation layer.
Step 3: Chrony Installation
RHEL 9/8/7: dnf/yum install -y chrony; Ubuntu/Debian: apt update && apt install -y chrony; openEuler: dnf install -y chrony. Enable and start: systemctl enable --now chronyd. Verify: chronyc tracking.
Step 4: Upstream Source Configuration (Templates)
Seven ready-to-use /etc/chrony.conf templates:
Template 1 (Cloud + internal fallback): internal NTP servers preferred, cloud provider NTP as backup, pool.ntp.org as last resort; makestep 1.0 3, rtcsync, allow internal subnets, deny all else, logging.
Template 2 (Isolated network): only internal NTP servers, local stratum 10 for fallback service, no external access.
Template 3 (Self-hosted NTP server): syncs from upstream masters, allow client subnets, local stratum 10 fallback, no makestep on server.
Template 4 (NTS encryption): requires chrony 4.2+; server time.cloudflare.com iburst nts, deny all (client-only). Self-hosted NTS server adds ntsserverkey and ntstrustedcerts.
Template 5 (Kubernetes nodes): three internal NTP sources, makestep 1.0 3, rtcsync, deny all (nodes are clients only), minimal logging.
Template 6 (Edge/container/intermittent): pool.ntp.org + cloud NTP, makestep 1 1, maxpoll 12 / minpoll 6 for slower convergence, deny all.
Template 7 (Reflection attack hardening): allow specific subnets, deny all, noclientlog, ratelimit interval 3 burst 8.
Step 5: Restart & Verification
systemctl restart chronyd, then chronyc tracking, chronyc sources -v, chronyc sourcestats -v, chronyc activity. Success: System time offset within thresholds, ^* source present, Reach=377. Use chronyc waitsync 60 0.001 (exit 0 = success).
Step 6: Logging & Monitoring
Enable detailed logs: log tracking measurements statistics, logdir /var/log/chrony. Files: tracking.log (per-update offset/frequency), measurements.log (raw polls), statistics.log (long-term), temp.log (errors). Forward to rsyslog/ELK/Loki. Prometheus metrics via chrony_exporter ( chrony_offset_seconds, chrony_frequency_ppm, chrony_skew_ppm, chrony_root_delay_seconds, chrony_root_dispersion_seconds, chrony_leap_status 0=Normal,1=Insert,2=Delete,3=Not synced) or
node_exporter node_timex_*. Example alert rules: abs(chrony_offset_seconds) > 0.1 for 5m (warning), chrony_leap_status == 3 for 1m (critical).
Step 7: Kubernetes Cluster Time Sync
Critical for kube-apiserver certificates, metrics-server HPA, etcd leader election, scheduler timestamps, audit log ordering. Node config: three internal NTP sources, makestep 1.0 3, rtcsync, deny all, logging. Verify across nodes:
for n in $(kubectl get nodes -o name | cut -d/ -f2); do kubectl debug node/$n -it --image=busybox -- date -u; doneor use Node Problem Detector. Containers share host system time; monotonic clock is container-specific. Time namespace (K8s 1.27+ alpha) allows per-Pod time view — use cautiously. Cloud-managed K8s (ACK, TKE, EKS, GKE) use provider NTP; still add internal NTP for redundancy.
Step 8: Troubleshooting Guide
Structured diagnosis for eight common failures:
All sources ^? / ^x : firewall blocking UDP 123, upstream unreachable, DNS failure, restrict misconfig. Test: ping, nc -uv, nmap -sU -p 123, tcpdump, chronyc activity, chronyc ntpdata. Fix: open firewall, use IPs, verify upstream.
Offset oscillating (± seconds): unstable source (public), CPU saturation, hypervisor interference, frequent rtcsync writes. Check: top, vmstat, chronyc sourcestats -v (jitter), chronyc sources -v (reach). Fix: switch sources, increase maxpoll, reduce CPU contention, verify VM tools (KVM/VMware/Hyper-V).
Tracking shows ~0 offset but date wrong: settimeofday blocked by seccomp, or app manually changed time. Trace:
strace -f -e trace=clock_settime,settimeofday,adjtimex -p $(pidof chronyd).
Post-reboot time drift: RTC hardware drift, missing rtcsync, OS not syncing system time to RTC. Fix: hwclock --systohc --utc, check /etc/adjtime, ensure rtcsync in config.
Container time drift: missing CAP_SYS_TIME, no fakehwclock, runtime blocking time adjustments. Fix: add capability, bind-mount /dev/ptp, enable fakehwclock, K8s securityContext.capabilities.add: ["SYS_TIME"] (caution).
Multi-NIC node sync failure: chrony binds all interfaces, routing picks wrong source IP. Fix: bindaddress 192.168.1.10 in config.
Chronyd won't start: journalctl -u chronyd -n 50, debug with chronyd -f /etc/chrony.conf -d.
Reach stuck at 0: wrong server IP, upstream not listening on UDP 123, bidirectional firewall. Test: ntpdate -q ntp.aliyun.com (deprecated but handy).
Step 9: Risk Management & Rollback
Key risks and mitigations:
Config change breaks sync: backup /etc/chrony.conf, restore, restart, verify.
Upstream time wrong (cluster-wide drift): detect via chronyc tracking Reference ID mismatch; switch to trusted source, force chronyc makestep.
Forced step disrupts workloads: DB replication, distributed leases, certificate validation. Mitigate: makestep 1.0 3 limits, prefer slewing, use monotonic clock in apps.
Chronyd stop → slow drift: keep chronyd running; K8s nodes must not stop chronyd container/unit.
K8s cert-manager renewal fails due to time drift: emergency chronyc makestep + restart; long-term: multiple upstreams + Prometheus alerts.
UTC mode confuses admins: standardize on UTC for servers/logs; document "ops view UTC, business views local".
Self-hosted NTP hardware failure: minimum two stratum-1 servers, dual GPS/BeiDou receivers, monitor stratum-1 offset.
Step 10: Compliance & Audit (MLPS 2.0, PCI-DSS, ISO 27001)
Requirements: trusted time source for audit timestamps, at least two independent sources, centralized log collection with UTC timestamps, periodic evidence collection. Implementation: deploy internal NTP hierarchy tied to national time sources (BeiDou/GPS/NIM), enforce UTC logs, automate checks via Ansible playbook ( chrony-check.yml collecting chronyc tracking and sources -v) or Bash script ( chrony-audit.sh scanning for offset > 1s or "Not synchronized"). Batch config management via Ansible Jinja2 templates parameterized by environment (prod/dev).
Step 11: Advanced Directions
NTS (RFC 8915): encrypts NTP; chrony 4.2+ native support via server ... iburst nts; verify with chronyc accheck.
PTP (IEEE 1588): microsecond/nanosecond sync for finance, 5G, industrial; chrony 4.x supports refclock PHC to sync system time from PTP hardware clock.
GPS/BeiDou + Chrony stratum-1: Raspberry Pi 4/5 + u-blox NEO-M8N/UM220 + PPS on GPIO; config: refclock PPS /dev/pps0 lock GPS prefer, refclock SHM 0 offset 0.5 delay 0.2 refid GPS; HA with two nodes, monitor via
chrony_exporter chrony_stratum.
Full monitoring stack: Prometheus + chrony_exporter → Grafana dashboards (offset, reach, stratum, leap, frequency trends) → Alertmanager (offset > 100 ms, reach=0, leap != Normal).
Command Reference
Categorized cheat sheets for time inspection ( date, hwclock, timedatectl, zdump), chrony service management ( systemctl, chronyd -d, journalctl), chronyc subcommands, network/port checks ( ss, nc, nmap, tcpdump), debugging ( strace, gdb), legacy NTP tools ( ntpdate, sntp, busybox ntpd), and key file paths ( /etc/chrony.conf, /var/lib/chrony/drift, /var/log/chrony/, /usr/share/zoneinfo/, /etc/adjtime).
Configuration Examples Summary
Six complete, commented chrony.conf files for: internal NTP server (stratum-2/GPS), internal NTP client, K8s node client, edge/4G gateway, physical stratum-1 (GPS+PPS), NTS encrypted client. Each includes driftfile, makestep, rtcsync, source lines, access control, logging, and hardening directives.
Troubleshooting Matrix & Workflow
Table mapping symptoms (large offset, jumping offset, chronyc errors, container drift, node NotReady, log disorder) to checkpoints, commands, key indicators, and conclusions. 12-step workflow: observe phenomenon → check service status → check chrony state → check network → check CPU/IO → check timezone → check RTC → check container time → isolate root cause → fix → verify ( chronyc waitsync, cross-host date +%s%N) → postmortem.
Risk Summary
Forced chronyc makestep jumps system time → impacts DB, distributed systems, certs, cron.
Config changes require systemctl restart chronyd. allow 0.0.0.0/0 enables NTP reflection attacks → always restrict.
Single self-hosted NTP server = single point of failure.
RTC in local mode breaks cross-timezone clusters. pool.ntp.org quality varies; prefer cloud/self-hosted for production.
Ntpd historical vulnerabilities (monlist, reflection) → migrate to chrony.
K8s node drift causes NotReady, x509 errors, cert-manager failures.
Applications must use monotonic clock ( CLOCK_MONOTONIC) for intervals.
DB replication lag + offset jump → verify time sync first.
Verification Methods
Sync verification: chronyc tracking (System time ≈ 0), chronyc waitsync 60 0.001 (exit 0), chronyc sources -v ( ^*, Reach=377), chronyc sourcestats -v (stable offset, jitter < 1 ms), hwclock --show --utc (matches system time).
Cross-host comparison: simultaneous ssh host "date '+%s.%N'" on 5 nodes; difference < 10 ms (LAN) / < 100 ms (WAN).
Network partition test: block UDP 123 for 1 hour ( iptables -A OUTPUT -p udp --dport 123 -j DROP), measure drift; < 5 s indicates healthy RTC.
Load test: stress-ng --cpu 4 --vm 2 --vm-bytes 1G --timeout 600 while watching watch -n 1 'chronyc tracking'; offset should wobble then reconverge.
Business validation: MySQL Seconds_Behind_Master ≈ 0, kubectl get nodes all Ready, openssl x509 -dates matches date, adjacent host log timestamps < 1 s apart, RPC heartbeats healthy.
Rollback Procedures
Bad config: backup broken config, restore previous .bak, restart chronyd, verify.
System time chaos: stop chronyd, ntpdate ntp.aliyun.com or sntp -S time.aliyun.com, hwclock --systohc --utc, start chronyd, verify.
K8s node time corruption: kubectl cordon, drain, SSH to node, stop chronyd, ntpdate ntp01.internal, start chronyd, verify, uncordon.
Cluster-wide time corruption (rare): switch upstream in chrony.conf via sed, restart chronyd, chronyc makestep, monitor.
Key Takeaways
Clarify timezone/UTC/RTC relationships; unify cluster timezone.
Choose trusted upstreams: internal + cloud provider + public pool (three-layer fallback).
Limit large steps with makestep 1.0 3; rely on slewing otherwise.
Enable NTS to prevent NTP tampering.
Maintain ≥2 independent upstream sources; monitor reach, offset, stratum.
Integrate time metrics into Prometheus/Grafana/Alertmanager.
Provide dedicated templates for K8s nodes, self-hosted NTP servers, container hosts.
Automate compliance checks for MLPS/PCI-DSS/ISO 27001.
Application layer must use monotonic clock, not wall clock.
Advanced Paths
NTS encryption, PTP high-precision.
Self-hosted stratum-1 with GPS/BeiDou.
Chrony_exporter + Prometheus full monitoring.
Integration with CIS Benchmark, SLSA, Zero Trust architectures.
Appendix: Quick References
Key paths ( /etc/chrony.conf, /var/lib/chrony/drift, /var/log/chrony/, /var/run/chrony/chronyd.sock, /usr/bin/chronyc, /usr/sbin/chronyd, /usr/share/zoneinfo/, /etc/localtime, /etc/timezone), environment variables ( TZ, chronyc_TZ), ports (UDP 123 NTP, TCP 4460 NTS KE, TCP 3731 chrony cmdmonitor), files ( /etc/chrony.keys, /etc/adjtime, /etc/ntp.conf, /etc/systemd/timesyncd.conf), links (chrony.org, GitHub, NTP Pool, Cloudflare NTS, RFC 8915, RFC 5905), version compatibility (1.x–4.2+ features), common error codes (503 no suitable source, 500 too many sources, 504 source not found, 522 reachability not achieved), and ten practical tips for junior engineers (config first, understand directives, backup+test, chronyc tracking as first diagnostic, monitoring before incidents, prioritize K8s/self-hosted NTP/DB primary, embrace rollback).
Signed-in readers can open the original source through BestHub's protected redirect.
This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactand we will review it promptly.
Raymond Ops
Linux ops automation, cloud-native, Kubernetes, SRE, DevOps, Python, Golang and related tech discussions.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.
