Server Monitoring14 min read

    Install & Configure NTP Chrony on CentOS 8

    By DanaServer Monitoring & Linux
    Share

    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 ntpd slews 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 the monlist command 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. iburst sends a burst of 4 packets at startup so chrony reaches sync in seconds rather than minutes. Use pool for round-robin DNS entries like *.pool.ntp.org, and server for 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 than N seconds during the first M updates 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 synchronised means 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.

    ntpdate is not available on CentOS 8 base repos and is deprecated upstream anyway. Use chronyd -q instead.


    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 pool directive usually resolves to 4 — verify with chronyc sources.
    • Mix sources. A pool entry resolves to multiple servers, but often on the same network. Mix pool.ntp.org with 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 peer directive between two or three internal chrony servers so they cross-check each other. Do not peer toward the public pool.
    • Don't run two NTP daemons. Only one process can bind UDP/123. ss -unlp | grep ':123' should show exactly chronyd.
    • 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_settime is blocked in the default seccomp profile. Sync the host, not the containers.
    • Keep tracking and sources under 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 tracking emits a comma-separated line that's easy to parse from a monitoring script. Alert on:
      • Leap status != Normal
      • Stratum > 4
      • |Last offset| > 100 ms
      • Reference ID == 7F7F0101 (the "unsynchronised" sentinel reference ID)
    • Source health: chronyc -c sources lists 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 tracking says Leap status: Not synchronised. No upstream is currently usable. Check chronyc sources -v — if every source shows ? or x, 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 10 and chronyd is serving from the local fallback because no upstream synced. Fix the upstream connectivity — local stratum is 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 simply nc -uvz <server> 123. Re-check firewall-cmd --list-all, SELinux denials (ausearch -m AVC -ts recent), and security groups / VPC ACLs in cloud environments. Also confirm the server's allow directive 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.
    • chronyd complains about RTC. rtcsync writes to /dev/rtc periodically. In some virtualised environments the RTC is read-only or absent — drop rtcsync from the config there. Look for unable to start real-time clock monitoring in journalctl -u chronyd.
    • High jitter on Wi-Fi or congested links. Wi-Fi adds 5–30 ms of variable latency. Increase minpoll/maxpoll if you are willing to trade responsiveness for stability, or pick upstreams that are network-closer.
    • dnf install ntp returns "No match for argument: ntp". This is expected on CentOS 8 — the ntp package was dropped. Install chrony instead.

    Summary

    For a fresh CentOS 8 NTP server, the minimum viable setup is:

    1. Install chrony — it's the only NTP daemon in the EL8 base repos.
    2. Edit /etc/chrony.conf — set pool (or several server) lines, allow your client networks, keep makestep 1.0 3 and rtcsync.
    3. systemctl enable --now chronyd.
    4. Open UDP/123 in firewalld for the client networks only.
    5. Verify with chronyc tracking (leap status Normal, stratum ≤ 4) and chronyc sources -v.
    6. 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.