Server Monitoring13 min read

    Set Up an NTP Server on CentOS 7

    By DanaServer Monitoring & Linux
    Share

    Accurate, consistent time is a hard requirement for log correlation, TLS certificate validation, Kerberos authentication, database replication, distributed locks, and almost every protocol that signs or sequences events. A drift of a few seconds is enough to break Kerberos tickets, void TOTP codes, or make MongoDB and Elasticsearch refuse to elect a primary. The only sane fix is to put every host on a known good time source — and on a Linux fleet, that source is NTP.

    This guide walks through installing, configuring, and operating an NTP server on CentOS 7 using the two daemons available in the base repos: chronyd (the default) and ntpd (the classic reference implementation). It also covers firewall rules, verification, and how to wire NTP health into ongoing monitoring.

    Heads up — CentOS 7 EOL. CentOS 7 reached end-of-life on June 30, 2024. If you are deploying a new time server, do it on RHEL 8/9, Rocky Linux 8/9, AlmaLinux 8/9, or Oracle Linux. The configuration shown here is identical on those distributions; only the package commands differ (dnf instead of yum). This article remains useful for legacy CentOS 7 fleets you still need to keep on time while you migrate.


    chronyd or ntpd — pick one

    CentOS 7 ships two NTP implementations and both are in the base repos:

    chronyd ntpd
    Package chrony ntp
    Default on CentOS 7 Yes No
    Recovers from large offsets Quickly (slew or step) Slowly (slew only by default)
    Works well on intermittently connected / mobile hosts Yes Less so
    Can serve clients Yes (allow directive) Yes (default)
    Hardware reference clocks (GPS/PPS) Yes Yes (more mature)
    Replaced upstream by — (active) chronyd on RHEL/CentOS 7+

    For 95% of fleets — physical servers, VMs, containers — use chronyd. It is the default for a reason: faster sync, better behavior on hosts that suspend or move networks, and a simpler config surface. Reach for ntpd only if you have a hard requirement (a tooling integration, a hardware reference clock workflow, or compliance language that names ntpd).

    You should run only one at a time. Both daemons want UDP/123 and will fight over the system clock if they run concurrently.


    Prerequisites

    • A CentOS 7 host with network access (either to the public NTP pool or to your upstream internal NTP servers).
    • sudo / root access.
    • The system clock close enough to reality that a sync is feasible. If the clock is years off (common after a CMOS battery failure on bare metal), force a one-shot correction first — see Bootstrap a wildly wrong clock below.

    1. Install

    sudo yum install -y chrony
    

    chrony is usually already installed on CentOS 7. If ntpd is also installed, stop and disable it before continuing:

    sudo systemctl stop ntpd
    sudo systemctl disable ntpd
    

    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.
    server 0.centos.pool.ntp.org iburst
    server 1.centos.pool.ntp.org iburst
    server 2.centos.pool.ntp.org iburst
    server 3.centos.pool.ntp.org iburst
    
    # Or, if you have multiple chronyd hosts that should share knowledge:
    #peer ntp-b.example.com
    
    # Record the rate at which the system clock gains/loses time.
    driftfile /var/lib/chrony/drift
    
    # Allow large initial step if the clock is more than 1s off
    # during the first three updates after start.
    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 am not great,
    # but I'm better than nothing."
    local stratum 10
    
    # Log files for diagnostics.
    logdir /var/log/chrony
    log measurements statistics tracking
    

    Key directives explained:

    • server — an upstream NTP source. iburst sends a burst of 4 packets at startup so chrony reaches sync in seconds instead of minutes.
    • peer — a symmetric peer of equal stratum; both sides exchange time. Use this between your own chronyd servers, not toward the public pool.
    • allow / deny — ACL for serving time. Default is to not serve any client.
    • makestep — step the clock if the offset is larger than the threshold during the first N updates. After that, chronyd slews (gradual adjustment) to avoid jumping the clock under live workloads.
    • local stratum — broadcast a stratum even when no upstream is reachable, so internal clients on an air-gapped LAN keep agreeing with each other.

    3. Enable and start

    sudo systemctl enable --now chronyd
    

    Confirm it is running and sourcing time:

    sudo systemctl status chronyd
    chronyc tracking
    chronyc sources -v
    

    chronyc tracking is the one-liner status:

    Reference ID    : 8B26F6C0 (time.cloudflare.com)
    Stratum         : 3
    Ref time (UTC)  : Sat May 09 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 server (1 = atomic clock / GPS, 2 = peers of stratum 1, …). Anything higher means you are too far 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.

    4. Open the firewall

    NTP runs on UDP/123. CentOS 7 uses firewalld:

    sudo firewall-cmd --permanent --add-service=ntp
    sudo firewall-cmd --reload
    

    Or by port:

    sudo firewall-cmd --permanent --add-port=123/udp
    sudo firewall-cmd --reload
    

    Restrict the source if appropriate. Public-facing NTP servers attract abuse (monlist reflection attacks were a long-running problem with old ntpd; chronyd does not implement monlist, but you should still scope the ACL).


    Option B — ntpd (classic)

    Use this only if you specifically need ntpd. Otherwise skip to verification.

    1. Install

    sudo systemctl stop chronyd
    sudo systemctl disable chronyd
    sudo yum install -y ntp
    

    2. Configure /etc/ntp.conf

    driftfile /var/lib/ntp/drift
    
    # Restrict default access; permit query and notrap, deny configure/modify.
    restrict default nomodify notrap nopeer noquery
    restrict 127.0.0.1
    restrict ::1
    
    # Permit clients on these networks to query this server.
    restrict 10.0.0.0 mask 255.0.0.0 nomodify notrap
    restrict 192.168.0.0 mask 255.255.0.0 nomodify notrap
    
    # Upstream sources.
    server 0.centos.pool.ntp.org iburst
    server 1.centos.pool.ntp.org iburst
    server 2.centos.pool.ntp.org iburst
    server 3.centos.pool.ntp.org iburst
    
    # Optional: serve as orphan stratum 10 if upstreams are unreachable.
    tos orphan 10
    
    # Log file.
    logfile /var/log/ntp.log
    

    Key bits:

    • restrict default ... noquery denies time queries from everyone unless explicitly allowed below. This is what shields you from monlist-style reflection abuse.
    • The two restrict <network> lines allow internal clients to query without granting modify or peer rights.
    • tos orphan 10 is the ntpd equivalent of chrony's local stratum.

    3. Enable and start

    sudo systemctl enable --now ntpd
    sudo systemctl status ntpd
    

    4. Verify

    ntpq -pn
    
         remote           refid      st t when poll reach   delay   offset  jitter
    ==============================================================================
    *162.159.200.1   10.27.57.53      3 u   23   64  377    8.412   -0.144   0.522
    +162.159.200.123 10.27.57.53      3 u   18   64  377    7.918    0.082   0.611
    +5.2.79.49       193.79.237.14    2 u   42   64  377   33.211   -0.017   1.044
    -216.197.156.83  136.243.135.7    2 u   28   64  377   78.502    1.220   2.811
    

    Read the leading column:

    • * — the system peer (the source ntpd is currently using).
    • + — a candidate good enough to be selected as a peer.
    • - — rejected by the clustering algorithm.
    • x — falseticker (the source disagrees with the rest).
    • (blank) — discarded outright.

    You want at least one * and a couple of + candidates. Stratum (st) 1 or 2 is ideal for upstreams. reach should reach 377 (octal for "all 8 of the last 8 polls succeeded") within a few minutes.

    ntpstat gives a one-line summary:

    sudo yum install -y ntpstat
    ntpstat
    # synchronised to NTP server (162.159.200.1) at stratum 3
    #    time correct to within 38 ms
    #    polling server every 64 s
    

    5. Firewall

    Same as chronyd:

    sudo firewall-cmd --permanent --add-service=ntp
    sudo firewall-cmd --reload
    

    Pointing clients at your new server

    On every CentOS 7/8/9, RHEL, Rocky, or Alma client, point chrony or ntpd at your internal server(s):

    chronyd

    # /etc/chrony.conf
    server ntp-a.example.com iburst
    server ntp-b.example.com iburst
    

    ntpd

    # /etc/ntp.conf
    server ntp-a.example.com iburst
    server ntp-b.example.com iburst
    

    Restart the daemon and confirm with chronyc sources -v or ntpq -pn. Two or three internal NTP servers are typical — clients run their own clustering algorithm and benefit from having more than one upstream so a single failed server does not desynchronise the fleet.


    Time zone

    NTP synchronises UTC. The wall-clock time you see in date is UTC + your time zone. On CentOS 7:

    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 that humans look at, not in the kernel.


    Hardware clock (RTC)

    The real-time clock (battery-backed clock on the motherboard or hypervisor) drifts independently. On boot, the kernel reads it; while running, ntpd/chronyd writes back to it periodically.

    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, with ntpd:

    sudo systemctl stop ntpd
    sudo ntpdate pool.ntp.org
    sudo systemctl start ntpd
    

    (ntpdate is deprecated upstream and not installed by default on newer distros — chronyd -q or chronyc -a makestep is preferred.)


    Operational tips

    • Use at least four upstream sources. NTP's clustering algorithm needs ≥3 to detect and exclude a falseticker; four gives you fault tolerance.
    • Mix sources. A pool entry resolves to multiple servers, but 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. With chrony, use peer between two or three internal servers so they cross-check; with ntpd, list each other as peer (or use pool and restrict).
    • Don't run both daemons. They both bind UDP/123 and will fight. systemctl is-active chronyd ntpd should show exactly one as active.
    • Watch out for VMs and containers. VMware, Hyper-V, and KVM can inject time into the guest from the host (VMware Tools' "Synchronize guest time with host", Hyper-V time provider, KVM PTP/kvm_ptp). Decide on one authority — either the hypervisor or NTP — and disable the other. Two competing time sources causes erratic ~1-second corrections.
    • Containers inherit the host clock and cannot run their own NTP daemon. Sync the host, not the containers.
    • Keep tracking and sources under monitoring. Stratum, offset, and Last offset (chrony) or offset/jitter (ntpd) 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 — show up far away from the cause. Wire NTP state into your monitoring rather than relying on incident-time discovery.

    Useful checks:

    • chrony: parse chronyc -c tracking (CSV-friendly output) and alert on Leap status != Normal, Stratum > 4, or |Last offset| > 100 ms.
    • ntpd: parse ntpq -c rv 0 'stratum,offset,sys_jitter,leap' and alert on the same thresholds.
    • Synthetic check: from another host, run chronyd -Q 'server <yourserver> iburst' (one-shot, no daemon) and assert the offset is small.

    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 or ntpd automatically.


    Troubleshooting

    • chronyc tracking says Leap status: Not synchronised. No upstream is currently usable. Check chronyc sources -v — if every source has ? or x, you have a network/firewall issue or every upstream is temporarily down.
    • ntpq -pn shows no * peer. ntpd has not selected a system peer yet. Wait 5–10 minutes after start; if it still does not select, the upstreams are unreachable, the offset is too large for ntpd to step (use ntpdate or restart with tinker panic 0 set), or all sources are flagged falsetickers.
    • Clients cannot reach the server. Verify with chronyc -h <server> tracking from a client (requires cmdallow on the server) 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.
    • Stratum 16 forever. Stratum 16 means "no synchronisation". Common causes: outbound UDP/123 blocked, every server line points to the same network and that network is down, or the system clock is so far off that the daemon refuses to step.
    • Time jumps 1 second backwards every minute. A second NTP source is fighting chronyd/ntpd — 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.
    • 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.

    Summary

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

    1. Install chrony (skip ntp unless you need it).
    2. Edit /etc/chrony.conf — list 4 upstream 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 are starting fresh today, do this on RHEL 9 / Rocky 9 / Alma 9 instead — the steps are identical apart from dnf install chrony. Either way, the goal is the same: every host on your network agrees on what time it is, within tens of milliseconds, all the time.