Bits, Bytes, & Radio Waves

A quiet journey through discovery and understanding.

Building a Production-Ready Dual-Stack NTP Infrastructure with Chrony

Overview

In this post, I walk through how I configured NTP using Chrony. As I continue experimenting with dual-stack environments, these servers are designed to provide time over both IPv4 and IPv6.


Infrastructure Details

The first step is confirming that both IPv4 and IPv6 addresses are available on the system.

ip address
2: ens34: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc mq state UP group default qlen 1000
    link/ether 00:0c:29:a1:c8:b5 brd ff:ff:ff:ff:ff:ff
    altname enp2s2
    altname enx000c29a1c8b5
    inet 172.16.3.100/24 brd 172.16.3.255 scope global noprefixroute ens34
       valid_lft forever preferred_lft forever
    inet6 2603:7080:7407:dd34:20c:29ff:fea1:c8b5/64 scope global dynamic noprefixroute 
       valid_lft 86301sec preferred_lft 86301sec
    inet6 fe80::20c:29ff:fea1:c8b5/64 scope link noprefixroute 
       valid_lft forever preferred_lft forever

I have two virtual machines dedicated to providing NTP services:

  • dnsntp1-v163-100
    • 172.16.3.100
    • 2603:7080:7407:dd34:20c:29ff:fea1:c8b5
  • dnsntp2-v163-101
    • 172.16.3.101
    • 2603:7080:7407:dd34:20c:29ff:fe87:ff78

Configuring

The main Chrony configuration file is located at /etc/chrony.conf. This single file controls the behavior of the chronyd.service.

The configuration itself is straightforward. The following sections outline the core settings that should be applied to each NTP server.


Server Pools

Server pools provide hostname-based time sources and represent a one-to-many relationship. This avoids maintaining individual IPv4 and IPv6 addresses while still allowing Chrony to resolve multiple upstream servers dynamically.

pool 2.rocky.pool.ntp.org iburst maxsources 2
pool time.cloudflare.com iburst maxsources 2
pool time.google.com iburst maxsources 2

Peer

Because I am deploying two NTP servers, I want to avoid a split-brain scenario. Each server is configured to peer with the other using IP addresses rather than hostnames. This removes any dependency on DNS availability.

Be sure to swap the peer IP address on the opposite server.

# Peer
peer 172.16.3.101

Server Hardening

These settings help keep the configuration tight and predictable:

  • Provide a safe local fallback if all external sources fail
  • Allow limited stepping on startup
  • Keep the hardware clock synchronized
  • Require multiple valid sources
makestep 1.0 3
rtcsync
minsources 2
local stratum 10

Allowed Clients

Finally, I define which subnets are allowed to receive time from the NTP servers. This limits exposure and keeps the service scoped to the intended infrastructure.

allow 127.0.0.1
allow 172.16.0.0/16
allow 2603:7080:7407:dd36::/64

When all the settings are configured for your environment, save and close the file. Restart the service.

sudo systemctl restart chronyd.service

Monitoring the Service

Chrony provides several built-in tools to monitor synchronization status and source health:

  • chronyc tracking
  • chronyc sources -v
  • chronyc sourcestats

The output from these commands makes it easy to confirm peer health, upstream reliability, and overall clock stability.


Tracking

chronyc tracking
Reference ID    : DA498B23 (time2.google.com)
Stratum         : 3
Ref time (UTC)  : Tue Feb 03 15:21:43 2026
System time     : 0.000085068 seconds fast of NTP time
Last offset     : +0.000005815 seconds
RMS offset      : 0.000083750 seconds
Frequency       : 0.396 ppm slow
Residual freq   : -0.002 ppm
Skew            : 0.073 ppm
Root delay      : 0.036965232 seconds
Root dispersion : 0.015774220 seconds
Update interval : 64.9 seconds
Leap status     : Normal

Sources

This tool is very helpful. From it’s output, we can determine the list of servers (^) and peers (=). We can see what Stratum the servers we are connected to as a reference reliability, the lower, the better.

chronyc sources -v

  .-- Source mode  '^' = server, '=' = peer, '#' = local clock.
 / .- Source state '*' = current best, '+' = combined, '-' = not combined,
| /             'x' = may be in error, '~' = too variable, '?' = unusable.
||                                                 .- xxxx [ yyyy ] +/- zzzz
||      Reachability register (octal) -.           |  xxxx = adjusted offset,
||      Log2(Polling interval) --.      |          |  yyyy = measured offset,
||                                \     |          |  zzzz = estimated error.
||                                 |    |           \
MS Name/IP address         Stratum Poll Reach LastRx Last sample               
===============================================================================
^+ time.cloudflare.com           2   7   377    57   +228us[ +234us] +/-   45ms
^+ time.cloudflare.com           2   8   377   254   +123us[ +263us] +/-   42ms
^+ 2600:4040:e0eb:ea00::cbb>     2   7   377   118   +205us[ +211us] +/-   44ms
^+ 2602:f9f3:1:2f::123:123       2   7   377   125   +213us[ +240us] +/-   44ms
^* time2.google.com              2   6   377    59   +453us[ +459us] +/-   45ms
^+ time1.google.com              2   8   377   256    +71us[ +212us] +/-   42ms
=+ dnsntp2-163-101.lab.aaro>     3   8   377   126    +55us[  +55us] +/-   35ms

Sourcestats

chronyc sourcestats
Name/IP Address            NP  NR  Span  Frequency  Freq Skew  Offset  Std Dev
==============================================================================
time.cloudflare.com         7   5  1035     +0.134      0.255    +84us    28us
time.cloudflare.com         6   3  1161     +0.114      0.450    +51us    60us
2600:4040:e0eb:ea00::cbb>   6   5   775     +0.257      0.316   +105us    21us
2602:f9f3:1:2f::123:123     7   5  1162     +0.118      0.180    +69us    28us
time2.google.com           11   7  1165     +0.130      0.178    +86us    42us
time1.google.com            9   7   25m     +0.017      0.337    +47us   105us
dnsntp2-163-101.lab.aaro>  26  15   40m     -0.356      0.159   -408us   125us

Conclusion

This configuration provides a stable, production-ready NTP service capable of serving both IPv4 and IPv6 clients. By combining multiple upstream sources, explicit peer relationships, and basic hardening, Chrony delivers accurate time while remaining resilient to upstream or DNS-related failures.


Leave a Reply

Your email address will not be published. Required fields are marked *