Chrony Basics

Page 10 — Time synchronisation, why it matters, and how to check it.

What Chrony is

Chrony is a time synchronisation service. It keeps the system clock accurate by syncing with NTP servers. The daemon is called chronyd.

Why time matters

A surprising number of things break silently when system time is wrong:

Tip: Before debugging any Kerberos or TLS problem, always verify that time is correct first.

Main config file

/etc/chrony.conf

Basic config example

server time.example.com iburst
driftfile /var/lib/chrony/drift
makestep 1.0 3
rtcsync

iburst sends a burst of packets on first contact for faster initial sync. makestep 1.0 3 allows stepping the clock in the first 3 updates if offset exceeds 1 second. rtcsync syncs the hardware clock.

Useful commands

chronyc tracking

chronyc tracking

Shows current synchronisation status and time offset. Use it to verify the machine is actually synced to a time source.

chronyc sources -v

chronyc sources -v

Shows configured time sources and their quality/state. Look for a source with * (currently selected) and low stratum/offset values.

chronyc sourcestats

chronyc sourcestats

Shows statistics about each time source — useful for checking source stability over time.

timedatectl

timedatectl

Shows general system time settings including whether NTP is active. Quick first check for time-related issues.

Service checks

systemctl status chronyd
systemctl restart chronyd
journalctl -u chronyd -n 50

What to check

chronyc waitsync

chronyc waitsync blocks until chrony achieves time synchronization (within a specified offset). This is useful in scripts and Ansible playbooks that need the clock to be accurate before proceeding — for example, before enrolling in FreeIPA or generating Kerberos tickets.

# Wait until sync is achieved (blocks indefinitely)
chronyc waitsync

# Wait with max offset of 0.1 seconds and max 30 polls
# Format: chronyc waitsync [max-tries [max-correction [max-skew [interval]]]]
chronyc waitsync 30 0.1 0 1
#                 ^   ^    ^ ^
#                 |   |    | poll interval (seconds)
#                 |   |    max clock skew (0 = ignore)
#                 |   max offset to consider "synced" (seconds)
#                 max number of polls before giving up

Typical use in an Ansible playbook after system boot:

- name: Wait for NTP synchronisation
  ansible.builtin.command: chronyc waitsync 30 0.1 0 1
  changed_when: false
  when: ansible_service_mgr == "systemd"

Without this step, playbooks that run immediately after boot may encounter Kerberos authentication failures (KRB5KRB_AP_ERR_SKEW) because the clock skew exceeds the 5-minute Kerberos tolerance.

Serving NTP to a LAN

In isolated environments (air-gapped, on-prem without external internet access) you can configure one host to sync to an authoritative source and serve NTP to all other hosts on the LAN using chrony.

NTP server configuration

# /etc/chrony.conf on the NTP server

# Sync from an authoritative upstream source (or GPS/hardware clock)
pool ntp1.upstream.example.com iburst
pool ntp2.upstream.example.com iburst

# Allow LAN hosts to use this server as a time source
allow 10.0.0.0/8           # your internal network range
allow 192.168.0.0/24

# local stratum 10: if upstream becomes unreachable,
# continue serving time to clients using the local clock
# Stratum 10 indicates it is a local clock (not an internet source)
local stratum 10

driftfile /var/lib/chrony/drift
# Open the NTP port on the firewall
firewall-cmd --permanent --add-service=ntp
firewall-cmd --reload

# Restart chrony
systemctl restart chronyd

# Verify clients can reach the server
chronyc clients    # shows which hosts are querying this server

Client configuration

# /etc/chrony.conf on client hosts — point to the internal NTP server
server ntp-server.internal.example.com iburst
driftfile /var/lib/chrony/drift

systemd-timesyncd vs chrony

On Debian/Ubuntu systems, systemd-timesyncd is the default NTP client. It is lightweight but limited — it cannot serve NTP, cannot handle complex source configurations, and is not as accurate as chrony. Understanding which is active avoids confusion when chrony commands return no output.

Featuresystemd-timesyncdchrony
Default on Debian/UbuntuYesNo (must install)
Default on RHEL/CentOSNoYes
Can serve NTP to other hostsNoYes
Supports multiple sourcesLimitedYes
Suitable for serversAcceptableRecommended
AccuracyGoodBetter

Checking which service is active

# Check if timesyncd is running
systemctl status systemd-timesyncd

# Check if chrony is running
systemctl status chronyd

# Show current sync status (timesyncd)
timedatectl show-timesync

# Show current sync status (chrony)
chronyc tracking

Switching from timesyncd to chrony on Debian/Ubuntu

# Install chrony (automatically disables timesyncd via conflict)
apt install chrony

# Verify timesyncd is stopped
systemctl status systemd-timesyncd   # should be inactive

# Verify chrony is running
systemctl status chronyd
chronyc tracking

On RHEL/CentOS, chrony is the default and timesyncd is not used. On Debian/Ubuntu, installing chrony is the correct approach for any server that needs accurate time, serves NTP to other hosts, or must satisfy Kerberos time requirements.

NTS (Network Time Security)

Chrony 4.0+ supports NTS, an authenticated and encrypted flavour of NTP that prevents on-path attackers from tampering with time. NTS uses a separate key-establishment channel (NTS-KE) on 4460/tcp to hand out cookies, then the regular NTP exchange on UDP/123 carries authenticated packets.

# /etc/chrony.conf — use a public NTS-capable source
server time.cloudflare.com iburst nts
server nts.ntp.se iburst nts

Confirm NTS is actually negotiated — the Auth column in chronyc -N authdata should read NTS, and chronyc sources will not show a ? state once cookies are received.

KISS codes: If an upstream is overloaded it can return a KISS-o'-Death packet (DENY, RATE, RSTR). Chrony honours these and will back off automatically — if chronyc sources -v shows a source stuck in a bad state, check journalctl -u chronyd for a KISS code before blaming firewalls.

Make sure 4460/tcp is reachable outbound (it is separate from NTP's UDP/123):

# Outbound NTS-KE check
ss -tn state established '( dport = :4460 )'
# or explicit test
timeout 5 bash -c '

Leap seconds

UTC occasionally inserts a leap second. Unannounced, this can confuse long-running daemons (historically PostgreSQL, Java, and kernel hrtimers). Chrony can either step the clock at the leap or smear the correction over several hours.

# /etc/chrony.conf

# Use the system tz leap-second table so chrony knows when
# a leap is coming and can apply it correctly offline.
leapsectz right/UTC

# Smooth (smear) the leap over ~1000s instead of a hard step —
# safer for databases and apps that dislike time going backwards.
leapsecmode slew
maxslewrate 1000

leapsectz points chrony at the tzdata leap file (usually /usr/share/zoneinfo/right/UTC); this lets it apply leaps even without an upstream announcement. Use leapsecmode slew on servers running databases or JVMs; use the default (system/step) only on hosts where a brief backwards step is harmless.

Hardware timestamping

On NICs that support PTP-style hardware timestamping, chrony can read the packet timestamp directly from the card, removing kernel and userspace jitter and getting sub-microsecond accuracy on a LAN.

# /etc/chrony.conf — enable HW timestamping on every capable interface
hwtimestamp *
Driver support required: hwtimestamp * only works if the NIC driver exposes SOF_TIMESTAMPING_TX_HARDWARE/_RX_HARDWARE. Check with ethtool -T <iface> — if hardware-transmit and hardware-receive are not listed, leave this directive out or chrony will log errors and fall back to software timestamping anyway.
# Verify HW timestamping is actually in use
chronyc ntpdata          # look at Tx timestamping / Rx timestamping fields
ethtool -T eno1