Posted on June 13, 2026
(bash) systemctl list-units --type=service --state=running | grep -iE 'chrony|ntp|timesyncd'
chrony.service loaded active running chrony, an NTP client/server
So, it appears that Ubuntu 26.04 defaults to using chrony as the default NTP client. Now we can use chronyc to verify what NTP servers are actually being used.
(bash) chronyc sources
MS Name/IP address Stratum Poll Reach LastRx Last sample
================================================================================
^+ ntp-nts-2.ps5.canonical.> 2 10 377 863 +733us[ +683us] +/- 71ms
^+ ntp-nts-3.ps5.canonical.> 2 10 377 495 +900us[ +913us] +/- 70ms
^+ ntp-nts-2.ps6.canonical.> 2 9 377 379 -674us[ -622us] +/- 58ms
^+ ntp-nts-3.ps6.canonical.> 2 9 377 113 -219us[ -208us] +/- 67ms
^+ ntp-nts-1.ps5.canonical.> 2 10 377 62 +615us[ +615us] +/- 71ms
As you can see from the output, the NTP servers that are being called are the canonical NTP servers, which are not the same as the NTP server that is set in the DHCP scope (10.190.64.1).
(bash) cat /etc/chrony/chrony.conf
# Welcome to the chrony configuration file. See chrony.conf(5) for more
# information about usable directives.
# The Ubuntu NTP pool servers configuration was moved to /etc/chrony/sources.d/ubuntu-ntp-pools.sources
# Use time sources from DHCP.
# Those could be non-NTS sources. If you want to avoid unauthenticated NTP
# sources comment the following line. (LP: #2115565)
sourcedir /run/chrony-dhcp
# Use NTP sources found in /etc/chrony/sources.d.
sourcedir /etc/chrony/sources.d
# This directive specifies the location of the file containing ID/key pairs for
# NTP authentication
keyfile /etc/chrony/chrony.keys
# This directive specifies the file into which chronyd will store the rate
# information
driftfile /var/lib/chrony/chrony.drift
# Save NTS keys and cookies.
ntsdumpdir /var/lib/chrony
# Uncomment the following line to turn logging on.
#log tracking measurements statistics
# Log files location
logdir /var/log/chrony
# Stop bad estimates upsetting machine clock.
maxupdateskew 100.0
# This directive enables kernel synchronisation (every 11 minutes) of the
# real-time clock. Note that it can't be used along with the 'rtcfile' directive.
rtcsync
# Step the system clock instead of slewing it if the adjustment is larger than
# one second, but only in the first three clock updates
makestep 1 3
# Get TAI-UTC offset and leap seconds from the system tz database.
# This directive must be commented out when using time sources serving
# leap-smeared time.
leapseclist /usr/share/zoneinfo/leap-seconds.list
# Include configuration files found in /etc/chrony/conf.d.
confdir /etc/chrony/conf.d
Reading through this config file, there is one section that sticks out Use time sources from DHCP. This section shows that time sources will be pulled from sourcedir /run/chrony-dhcp. This is great news, all we need to do now is to verify that the directory exists and is being populated by DHCP. A simple ls command should give us the info we need.
(bash) ls /run/chr*
chronyd.pid chronyd.sock
Hmmm, well that didn't work, it does not look like there is a /run/chrony-dhcp directory. The next step is to check and see if the DHCP lease that is on the server actually has NTP information. It is possible all of this was for nothing, but let's verify first. To check the DHCP lease information, we will just cat a file / directory.
(bash) cat /run/systemd/netif/leases/*
# This is private data. Do not parse.
ADDRESS=10.190.64.24
NETMASK=255.255.255.0
ROUTER=10.190.64.1
SERVER_ADDRESS=10.190.64.1
T1=3d 12h
T2=6d 3h
LIFETIME=1w
DNS=10.190.64.1
NTP=10.190.64.1
DOMAINNAME=combobulate.test
DOMAIN_SEARCH_LIST=combobulate.test
CLIENTID=ffb6220feb00020000ab118179be1a8cd8138f
OPTION_224=4647313045305442323239303031363600
Based on this information, it does look like the NTP information is being sent by the DHCP server. We have now verified the NTP server is NTP=10.190.64.1. So now we just need to figure out why that information isn't being copied to /run/chrony-dhcp.
systemctl list-units --full -all | grep "dhclient"
systemctl list-units --full -all | grep "NetworkManager"
And we have a winner with systemd-networkd:
(bash) systemctl list-units --full -all | grep "systemd-networkd.service"
systemd-networkd.service loaded active running Network Management
In this case, it appears that we are running systemd-networkd as the network manager.
(bash) ls /etc/network/if-up.d/
chrony ethtool
We already discovered that we are using chrony so let's examine the contents of the chrony script to see what wonders it may contain.
(bash) cat /etc/network/if-up.d/chrony
#!/bin/sh
set -e
[ -x /usr/sbin/chronyd ] || exit 0
if [ -e /run/chrony/chronyd.pid ]; then
chronyc onoffline > /dev/null 2>&1
fi
exit 0
Hmmm, interesting, this script appears to check to see if chronyd exists ( [ -x /usr/sbin/chronyd ]), if not it exits. Then it checks to see if there is a chronyd.pid that is running. Basically it looks to see if chronyd is installed and running and if so it runs chronyc onoffline. While all of this is well and good, this does not appear to actually try to create the elusive /run/chrony-dhcp. Let's move on.
ls /etc/networkd-dispatcher/routable.d/
And based on the no output from our list command, there aren't any networkd-dispatcher scripts that are being executed.
(bash) ls /usr/lib/NetworkManager/dispatcher.d/
20-chrony-dhcp 20-chrony-onoffline
Eureka, we have found something. Maybe not what we are looking for, because we aren't using NetworkManager, but we did find something. So let's peek into this 20-chrony-dhcp treasure and see what we can learn.
(bash) cat /usr/lib/NetworkManager/dispatcher.d/20-chrony-dhcp
#!/bin/sh
# This is a NetworkManager dispatcher script for chronyd to update
# its NTP sources with servers from DHCP options passed by NetworkManager
# in the DHCP4_NTP_SERVERS and DHCP6_DHCP6_NTP_SERVERS environment variables.
export LC_ALL=C
interface=$1
action=$2
chronyc=/usr/bin/chronyc
server_options=iburst
server_dir=/run/chrony-dhcp
dhcp_server_file=$server_dir/$interface.sources
dhcp_ntp_servers="$DHCP4_NTP_SERVERS $DHCP6_DHCP6_NTP_SERVERS"
add_servers_from_dhcp() {
rm -f "$dhcp_server_file"
for server in $dhcp_ntp_servers; do
# Check for invalid characters (from the DHCPv6 NTP FQDN suboption)
len1=$(printf '%s' "$server" | wc -c)
len2=$(printf '%s' "$server" | tr -d -c 'A-Za-z0-9:.-' | wc -c)
if [ "$len1" -ne "$len2" ] || [ "$len2" -lt 1 ] || [ "$len2" -gt 255 ]; then
continue
fi
printf 'server %s %s\n' "$server" "$server_options" >> "$dhcp_server_file"
done
$chronyc reload sources > /dev/null 2>&1 || :
}
clear_servers_from_dhcp() {
if [ -f "$dhcp_server_file" ]; then
rm -f "$dhcp_server_file"
$chronyc reload sources > /dev/null 2>&1 || :
fi
}
mkdir -p $server_dir
case "$action" in
up|dhcp4-change|dhcp6-change)
add_servers_from_dhcp;;
down)
clear_servers_from_dhcp;;
esac
exit 0
Wow, this looks exactly like what we are looking for. Reading through this script, it appears that this script will create the /run/chrony-dhcp and load the file with all of the NTP servers from DHCP using either $DHCP4_NTP_SERVERS or $DHCP6_DHCP6_NTP_SERVERS. But wait, if we read the top of the script, it says that those two variables are assigned by NetworkManager. Just to double check, let's see if those variables exist in the OS.
echo $DHCP4_NTP_SERVERS
echo $DHCP6_DHCP6_NTP_SERVERS
Yup, it does look like those variables are assigned by NetworkManager before the script is executed, as both of them are empty. So at this point we have hit a bit of a dead end with what is configured by the OS and the different packages.
(bash) vim /etc/networkd-dispatcher/routable.d/20-chrony-dhcp
#!/bin/sh
# This is a systemd-networkd dispatcher script for chronyd to update
# its NTP sources with servers from DHCP options pulled from
# /run/systemd/netif/leases/{interface index}
# $IFACE is pulled from networkd-dispatcher
export LC_ALL=C
network_class_index="/sys/class/net/$IFACE/ifindex"
lease_dir="/run/systemd/netif/leases"
dhcp_run_dir="/run/chrony-dhcp"
full_script_path="$(realpath "$0")"
script_name="$(basename "$0")"
dir_path="$(dirname "$full_script_path")"
# Create the chrony-dhcp directory if it doesn't already exist
if [ ! -d $dhcp_run_dir ]
then
mkdir -p $dhcp_run_dir
fi
# Remove any existing entires if they exist
if [ -f "$dhcp_run_dir/$IFACE.sources" ]
then
rm -f "$dhcp_run_dir/$IFACE.sources"
fi
# Pull the ifindex to find the lease file
if [ -f "$network_class_index" ]
then
ifindex=$(cat "$network_class_index")
else
exit 0
fi
# Parse the lease for NTP servers
if [ -f "$lease_dir/$ifindex" ]
then
ntp_servers=$(grep '^NTP=' "$lease_dir/$ifindex" | awk -F= '{print $2}')
else
exit 0
fi
# Add NTP server info to chrony-dhcp
if [ -n "$ntp_servers" ]
then
echo "# This list is automatically updated by $dir_path/$script_name" >> "$dhcp_run_dir/$IFACE.sources"
for ntp_server in $ntp_servers
do
echo "server $ntp_server iburst" >> "$dhcp_run_dir/$IFACE.sources"
done
chronyc reload sources >/dev/null 2>&1
else
exit 0
fi
A quick run down of this script, first it checks to see if the /run/chrony-dhcp directory exists, and if not, it creates it. Then it pulls the ifindex from /sys/class/net/$IFACE/ifindex. $IFACE is submitted by networkd-dispatcher when the script is executed. Then it pulls the lease information for that ifindex /run/systemd/netif/leases/$ifindex and parses the NTP information. Finally, it copies the NTP server info into /run/chrony-dhcp and reloads the sources for chrony.
cp 20-chrony-dhcp /etc/networkd-dispatcher/routable.d/
chown root:root /etc/networkd-dispatcher/routable.d/20-chrony-dhcp
chmod 755 /etc/networkd-dispatcher/routable.d/20-chrony-dhcp
To verify that everything is running correctly, just check chronyc again.
(bash) chronyc sources
MS Name/IP address Stratum Poll Reach LastRx Last sample
================================================================================
^+ ntp-nts-2.ps5.canonical.> 2 10 377 863 +733us[ +683us] +/- 71ms
^+ ntp-nts-3.ps5.canonical.> 2 10 377 495 +900us[ +913us] +/- 70ms
^+ ntp-nts-2.ps6.canonical.> 2 9 377 379 -674us[ -622us] +/- 58ms
^+ ntp-nts-3.ps6.canonical.> 2 9 377 113 -219us[ -208us] +/- 67ms
^+ ntp-nts-1.ps5.canonical.> 2 10 377 62 +615us[ +615us] +/- 71ms
^+ _gateway 3 6 377 41 -2765us[-2740us] +/- 63ms
If you notice we now have _gateway added to the list. In our case here 10.190.64.1 is the gateway, so this is correct. But you will notice that all of the other default NTP servers still exist. *sigh* this is because chrony is reading from all sources (/run/chrony-dhcp, /etc/chrony/sources.d). A non-destructive quick fix is to rename the default source in /etc/chrony/sources.d
mv /etc/chrony/sources.d/ubuntu-ntp-pools.sources /etc/chrony/sources.d/ubuntu-ntp-pools.sources.old
chronyc reload sources
Chrony will only read files with a .sources extension. By just renaming the file to .old it will stop using it as a default source. We then just reload the sources really quick, and voila, we now only have one entry in our NTP sources list.
(bash) chronyc sources
MS Name/IP address Stratum Poll Reach LastRx Last sample
================================================================================
^+ _gateway 3 6 377 8 +1545us[ +706us] +/- 64ms