Accurate time is non-negotiable for log correlation, TLS certificate validation, Kerberos, TOTP-based 2FA, distributed databases, and most cluster coordination protocols. A drift of a few seconds is enough to invalidate Kerberos tickets, break MFA codes, or stop a MongoDB or Elasticsearch cluster from electing a primary. On a Linux fleet, the fix is to put every host on a known good time source — and on CentOS 8 (or any EL8 distribution), the daemon for that is chrony.
This guide walks through installing, configuring, and operating an NTP server on CentOS 8 using chrony — the only NTP daemon shipped in the EL8 base repos. The same steps apply to CentOS Stream 8, RHEL 8, Rocky Linux 8, and AlmaLinux 8.
Heads up — CentOS 8 EOL. CentOS Linux 8 reached end-of-life on December 31, 2021. If you are deploying a new time server, do it on RHEL 9, Rocky Linux 9, AlmaLinux 9, or CentOS Stream 9 — the configuration shown here is identical. This article remains useful for legacy CentOS 8 hosts you still need to keep on time, and the same commands work on any EL8 derivative.
Why chrony, not ntpd
On CentOS 7 you had a choice between chronyd and ntpd. On CentOS 8 you do not — Red Hat dropped the ntp package in EL8. The base repos only ship chrony. If a guide tells you to dnf install ntp on CentOS 8, it's wrong: that package does not exist in the standard channels.
This is the right call for almost every workload. chronyd:
- Recovers from large clock offsets quickly (slew or step), where
ntpdslews very slowly by default. - Behaves well on hosts that suspend, change networks, or run in containers/VMs.
- Has a much smaller attack surface than legacy
ntpd— it never implemented themonlistcommand that drove a decade of amplification attacks. - Is what the daemon you'd install anyway if you had the choice.
If you have a hard requirement for ntpd (compliance language, a tooling integration, a hardware reference clock workflow that only ntpd supports), you'll need to enable EPEL and source it there — out of scope for this guide.
Prerequisites
- A CentOS 8 / RHEL 8 / Rocky 8 / Alma 8 host with network access — either outbound to the public NTP pool, or to your upstream internal NTP servers.
sudo/ root access.- The system clock close enough to reality that a one-shot correction is feasible. If the clock is years off (CMOS battery failure on bare metal, or a fresh VM clone with a stale virtual RTC), see Bootstrap a wildly wrong clock below.
1. Install chrony
sudo dnf install -y chrony
chrony is usually already installed on a minimal CentOS 8 image. Confirm with:
rpm -q chrony
If anything else is bound to UDP/123, stop it first — only one NTP daemon can run at a time:
sudo ss -unlp | grep ':123'
2. Configure /etc/chrony.conf
The default file is well annotated. The minimum changes to turn this host into an NTP server for your network are:
# Use the public NTP pool as upstream sources. Replace with internal
# stratum-1/2 servers if you have them. `pool` resolves to multiple A/AAAA
# records; chronyd manages each as an independent source.
pool 2.centos.pool.ntp.org iburst
# Or, for finer control, list named anchors so a single pool outage
# does not blind you:
# server time.cloudflare.com iburst
# server time.google.com iburst
# Record the rate at which the system clock gains/loses time so chronyd
# can compensate after a restart.
driftfile /var/lib/chrony/drift
# Allow large initial step if the clock is more than 1s off during the
# first three updates after start. After that, chronyd slews (gradual
# adjustment) to avoid jumping the clock under live workloads.
makestep 1.0 3
# Enable kernel synchronization of the real-time clock (RTC).
rtcsync
# ---- Server mode ----
# Allow clients on these networks to query this server.
allow 10.0.0.0/8
allow 192.168.0.0/16
# allow 0.0.0.0/0 # ONLY if you intend to be a *public* NTP server
# Optional: serve time even when this host has no upstream sync
# (useful on isolated networks). Local stratum 10 = "I know I'm not
# great, but I'm better than nothing — agree with me."
local stratum 10
# Log files for diagnostics.
logdir /var/log/chrony
log measurements statistics tracking
Key directives explained:
server/pool— upstream NTP sources.iburstsends a burst of 4 packets at startup so chrony reaches sync in seconds rather than minutes. Usepoolfor round-robin DNS entries like*.pool.ntp.org, andserverfor named anchors.allow/deny— ACL for serving time. The default is to not serve any client. Restrict this to the networks that should actually use you as an NTP server.makestep N M— step the clock if the offset is larger thanNseconds during the firstMupdates after start. After that, chronyd slews instead.local stratum— broadcast a stratum even when no upstream is reachable, so internal clients on an air-gapped LAN keep agreeing with each other.rtcsync— write the system time back to the hardware RTC every 11 minutes (the kernel actually does this, chronyd just enables it).
Save the file. There is no need to validate manually — chronyd will refuse to start if the config is malformed.
3. Enable and start chronyd
sudo systemctl enable --now chronyd
sudo systemctl status chronyd
Confirm it is running and sourcing time:
chronyc tracking
chronyc sources -v
chronyc tracking is the one-liner status:
Reference ID : 8B26F6C0 (time.cloudflare.com)
Stratum : 3
Ref time (UTC) : Thu May 14 09:14:08 2026
System time : 0.000018221 seconds slow of NTP time
Last offset : -0.000031542 seconds
RMS offset : 0.000284901 seconds
Frequency : 12.503 ppm slow
Residual freq : -0.012 ppm
Skew : 0.052 ppm
Root delay : 0.024812 seconds
Root dispersion : 0.000412 seconds
Update interval : 1027.6 seconds
Leap status : Normal
What to look for:
- Stratum ≤ 4 for a healthy server (1 = atomic clock / GPS, 2 = peers of stratum 1, …). Anything higher means you are several hops from a reference clock.
- System time offset under ~50 ms on a wired network, under a few hundred ms on Wi-Fi or constrained VMs.
- Leap status =
Normal.Not synchronisedmeans chronyd has no usable upstream.
chronyc sources -v shows each upstream and its state. The ^* marker is the source chrony has selected; ^+ are accepted candidates; ^- are rejected by the clustering algorithm; ^? are unreachable.
4. Open the firewall
NTP runs on UDP/123. CentOS 8 uses firewalld:
sudo firewall-cmd --permanent --add-service=ntp
sudo firewall-cmd --reload
Or by port (equivalent):
sudo firewall-cmd --permanent --add-port=123/udp
sudo firewall-cmd --reload
For tighter control, scope the rule to the source networks that should be allowed to query this server using a firewalld rich rule:
sudo firewall-cmd --permanent --add-rich-rule='rule family="ipv4" source address="10.0.0.0/8" port port="123" protocol="udp" accept'
sudo firewall-cmd --reload
Restrict the source if you are exposed to the internet. Public-facing NTP servers attract abuse — chrony does not implement monlist (the old ntpd reflection vector), but you should still scope the ACL with allow in chrony.conf and the firewall.
5. SELinux
CentOS 8 ships with SELinux in enforcing mode by default. chrony is fully supported out of the box — there is no extra step required for the standard config. The chronyd_t domain already has the policy it needs to bind UDP/123, read its config, and write its drift file.
The only time SELinux gets in the way is if you move the chrony state directory or its log files outside the default paths. If you do, restore the labels:
sudo restorecon -Rv /var/lib/chrony /var/log/chrony /etc/chrony.conf
If you suspect an SELinux denial after a change, check the audit log:
sudo ausearch -m AVC,USER_AVC -ts recent -c chronyd
If audit2allow suggests a new policy, prefer fixing the file label or moving the file back rather than installing a custom policy module.
Pointing clients at your new server
On every CentOS 7/8/9, RHEL, Rocky, or Alma client, point chrony at your internal server(s) by editing /etc/chrony.conf:
server ntp-a.example.com iburst
server ntp-b.example.com iburst
Remove (or comment out) any pool *.pool.ntp.org lines on internal clients so they only talk to your servers. Restart and verify:
sudo systemctl restart chronyd
chronyc sources -v
Two or three internal NTP servers are typical — chronyd runs its own clustering algorithm and benefits from having more than one upstream so a single failed server does not desynchronise the fleet.
For Ubuntu/Debian clients, the config file lives at /etc/chrony/chrony.conf instead, but the directives are identical.
Time zone
NTP synchronises UTC. The wall-clock time you see in date is UTC + your time zone. On CentOS 8:
timedatectl # show current state
timedatectl list-timezones # browse zones
sudo timedatectl set-timezone UTC # set the system zone
For server fleets, set every host to UTC. Local-time servers create off-by-one bugs in cron, log correlation, and audit trails that surface only at DST transitions. Render local time in the dashboards humans look at, not in the kernel.
Hardware clock (RTC)
The real-time clock (the battery-backed clock on the motherboard, or the virtual RTC presented by your hypervisor) drifts independently. On boot the kernel reads it; while running, chronyd writes back to it periodically thanks to rtcsync.
sudo hwclock --show # show the RTC
sudo hwclock --systohc # write system time to RTC (one-shot)
Decide whether the RTC stores UTC or local time:
sudo timedatectl set-local-rtc 0 # 0 = UTC (recommended on Linux-only hosts)
sudo timedatectl set-local-rtc 1 # 1 = local (only for dual-boot with Windows)
UTC in the RTC is the right answer for a server. Only flip to local if you dual-boot Windows on the same hardware and want the clock to read correctly under both OSes.
Bootstrap a wildly wrong clock
If the host's clock is hours, days, or years off (commonly after a dead CMOS battery on bare metal, or a fresh VM clone), chronyd may refuse to step it during normal operation. Two options:
One-shot, before starting the daemon:
sudo systemctl stop chronyd
sudo chronyd -q 'server pool.ntp.org iburst'
sudo systemctl start chronyd
chronyd -q queries the server, sets the clock, and exits. Once the clock is in the right ballpark, the daemon takes over.
Or, while chronyd is already running (requires cmdallow or running as root locally):
sudo chronyc -a makestep
This asks the running daemon to step the clock immediately on its next update, bypassing the makestep threshold.
ntpdateis not available on CentOS 8 base repos and is deprecated upstream anyway. Usechronyd -qinstead.
Operational tips
- Use at least four upstream sources. NTP's clustering algorithm needs ≥3 sources to detect and exclude a falseticker; four gives you fault tolerance. A single
pooldirective usually resolves to 4 — verify withchronyc sources. - Mix sources. A
poolentry resolves to multiple servers, but often on the same network. Mixpool.ntp.orgwith one or two named anchors (time.cloudflare.com,time.google.com) so a single network event cannot blind you. - Internal NTP servers should peer. Use the
peerdirective between two or three internal chrony servers so they cross-check each other. Do notpeertoward the public pool. - Don't run two NTP daemons. Only one process can bind UDP/123.
ss -unlp | grep ':123'should show exactlychronyd. - Watch out for VMs. VMware, Hyper-V, and KVM can inject time into the guest from the host (VMware Tools' "Synchronize guest time with host", the Hyper-V time provider, KVM PTP /
kvm_ptp). Decide on one authority — either the hypervisor or chronyd — and disable the other. Two competing time sources cause erratic ~1-second corrections that look exactly like a flaky NTP setup. - Containers inherit the host clock and cannot meaningfully run their own NTP daemon —
clock_settimeis blocked in the default seccomp profile. Sync the host, not the containers. - Keep
trackingandsourcesunder monitoring. Stratum, last offset, and leap status are the three numbers that tell you whether the server is healthy. A jump in stratum is your earliest signal that upstream connectivity is broken.
Monitoring NTP health
A silent NTP failure is one of the worst kinds because the symptoms — auth failures, log timestamp gaps, replication lag, expired TLS handshakes — show up far away from the cause. Wire NTP state into your monitoring rather than relying on incident-time discovery.
Useful checks:
- CSV-friendly status:
chronyc -c trackingemits a comma-separated line that's easy to parse from a monitoring script. Alert on:Leap status != NormalStratum > 4|Last offset| > 100 msReference ID == 7F7F0101(the "unsynchronised" sentinel reference ID)
- Source health:
chronyc -c sourceslists every upstream and its state. Alert when fewer than 2 sources are in the^*or^+state. - Synthetic check from another host:
chronyd -Q 'server <yourserver> iburst'is a one-shot, no-daemon query that prints the offset. Useful as an external probe.
Xitoring's server monitoring collects time-sync state alongside CPU, memory, disk, and network so you see drift on the same dashboard as the workload it impacts. The agent runs on CentOS 7/8/9, RHEL, Rocky, Alma, Ubuntu, Debian, and Windows — it picks up chronyd automatically.
Troubleshooting
chronyc trackingsaysLeap status: Not synchronised. No upstream is currently usable. Checkchronyc sources -v— if every source shows?orx, you have a network/firewall issue, or every upstream is temporarily down. Confirm outbound UDP/123 is allowed:nc -uvz pool.ntp.org 123.Reference ID : 7F7F0101 ()— chrony's placeholder for "I have no sync source". Same root cause as above.- Stratum 10 forever. You set
local stratum 10and chronyd is serving from the local fallback because no upstream synced. Fix the upstream connectivity —local stratumis a safety net, not a steady state. - Clients cannot reach the server. Verify from a client with
chronyd -Q 'server <yourserver> iburst'(one-shot, no daemon), or simplync -uvz <server> 123. Re-checkfirewall-cmd --list-all, SELinux denials (ausearch -m AVC -ts recent), and security groups / VPC ACLs in cloud environments. Also confirm the server'sallowdirective covers the client's subnet. - Time jumps 1 second backwards every minute. A second time source is fighting chronyd — usually VMware Tools' or Hyper-V's host-time-sync. Disable one side.
chronydcomplains aboutRTC.rtcsyncwrites to/dev/rtcperiodically. In some virtualised environments the RTC is read-only or absent — droprtcsyncfrom the config there. Look forunable to start real-time clock monitoringinjournalctl -u chronyd.- High
jitteron Wi-Fi or congested links. Wi-Fi adds 5–30 ms of variable latency. Increaseminpoll/maxpollif you are willing to trade responsiveness for stability, or pick upstreams that are network-closer. dnf install ntpreturns "No match for argument: ntp". This is expected on CentOS 8 — thentppackage was dropped. Installchronyinstead.
Summary
For a fresh CentOS 8 NTP server, the minimum viable setup is:
- Install
chrony— it's the only NTP daemon in the EL8 base repos. - Edit
/etc/chrony.conf— setpool(or severalserver) lines,allowyour client networks, keepmakestep 1.0 3andrtcsync. systemctl enable --now chronyd.- Open UDP/123 in firewalld for the client networks only.
- Verify with
chronyc tracking(leap statusNormal, stratum ≤ 4) andchronyc sources -v. - Monitor stratum, offset, and leap status continuously — silent NTP failures are expensive.
If you're standing up a brand-new time server today, deploy this on RHEL 9 / Rocky 9 / Alma 9 / CentOS Stream 9 instead — the steps are identical apart from minor package versions, and you get a supported OS. Either way, the goal is the same: every host on your network agrees on what time it is, within tens of milliseconds, all of the time.