DEV Community

Cover image for Stop Trusting Plain NTP: Practical Network Time Security with chrony on Linux
Lyra
Lyra

Posted on

Stop Trusting Plain NTP: Practical Network Time Security with chrony on Linux

Plain NTP is still the default on a shocking number of Linux hosts. It works — until someone on the path spoofs replies, delays packets on purpose, or your TLS cert checks start failing because the clock drifted into the past.

Certificates, Kerberos, log correlation, 2FA codes, and container image signatures all assume the system clock is roughly right and not controlled by an attacker. Network Time Security (NTS), standardized in RFC 8915, gives NTP the cryptographic authenticity it never had out of the box. On modern Debian, Ubuntu, Fedora, and RHEL-family systems, the practical client is chrony 4.0+.

This guide gets you from "whatever DHCP handed us" to authenticated NTS sources, with verification you can trust.

Why plain NTP is a weak link

Classic NTP client/server mode has no built-in server identity check. A network attacker can:

  • Forge time responses
  • Replay old packets
  • Delay traffic asymmetrically (NTS does not fully solve pure delay attacks — see RFC 8915 §8.6 — but it stops forgery and tampering)

Shared-key NTP MACs exist, but they need pre-shared secrets on every host. That does not scale for public or multi-site fleets.

NTS splits the problem into two phases:

  1. NTS-KE (Key Establishment) — TLS (1.3+) on TCP port 4460 by default. The client authenticates the server certificate the same way a browser trusts HTTPS, negotiates AEAD keys, and receives opaque cookies.
  2. Authenticated NTP — normal NTP (usually UDP/123, sometimes a server-specific port such as Netnod's 4123) with NTS extension fields. Cookies carry server-side state so the NTP server stays mostly stateless.

After the first successful KE, chrony can dump cookies to disk and avoid repeating the TLS handshake on every restart.

What you need

Piece Notes
chrony ≥ 4.0 Debian 11+, Ubuntu 22.04+, Fedora 35+, RHEL 9+
Outbound TCP 4460 NTS-KE
Outbound UDP 123 (and sometimes vendor-specific NTP ports) Authenticated time packets
System CA store Or explicit ntstrustedcerts for private PKI
Root or sudo Install packages, edit config, manage systemd units

Disable competing time daemons first. On systems using systemd-timesyncd:

sudo systemctl disable --now systemd-timesyncd.service
Enter fullscreen mode Exit fullscreen mode

If ntpsec or classic ntp is installed, pick one stack. On Debian/Ubuntu, installing chrony will typically remove conflicting NTP packages.

Install chrony

Debian / Ubuntu:

sudo apt update
sudo apt install chrony
Enter fullscreen mode Exit fullscreen mode

Fedora / RHEL-family:

sudo dnf install chrony
sudo systemctl enable --now chronyd
Enter fullscreen mode Exit fullscreen mode

Config paths differ slightly:

  • Debian/Ubuntu: /etc/chrony/chrony.conf (plus /etc/chrony/sources.d/ and sometimes DHCP drop-ins)
  • Fedora/RHEL/Arch-style: /etc/chrony.conf

Confirm the binary is new enough:

chronyd --version
# Need 4.0+ for NTS
Enter fullscreen mode Exit fullscreen mode

A clean NTS client config

The important part is the nts option on each server line. Use at least four independent sources when you can (NTP best practice). Prefer geographic diversity so one anycast or one lab outage does not strand you.

Debian / Ubuntu pattern

Edit /etc/chrony/chrony.conf (or drop a dedicated file and keep the main file minimal). Comment out stock pools and DHCP-sourced NTP so unauthenticated servers cannot win source selection:

sudo cp /etc/chrony/chrony.conf /etc/chrony/chrony.conf.bak.$(date +%F)
Enter fullscreen mode Exit fullscreen mode

Create a dedicated sources file if your package already uses sourcedir:

# /etc/chrony/sources.d/nts.sources
# Authenticated public NTS sources — adjust to your region.

server time.cloudflare.com iburst nts
server nts.netnod.se iburst nts
server ptbtime1.ptb.de iburst nts
server ptbtime2.ptb.de iburst nts
Enter fullscreen mode Exit fullscreen mode

Then make sure the main config does not reintroduce plain pools. Typical lines to disable:

# pool pool.ntp.org iburst maxsources 4
# sourcedir /run/chrony-dhcp
sourcedir /etc/chrony/sources.d
Enter fullscreen mode Exit fullscreen mode

Keep (or add) the operational basics:

# Persist NTS cookies so restarts skip a full KE when possible
ntsdumpdir /var/lib/chrony

# Prefer authenticated sources strictly
authselectmode require

# Step the clock only early after start (boot / service restart)
makestep 0.1 3

# Keep the hardware RTC roughly in sync on many servers
rtcsync

driftfile /var/lib/chrony/drift
Enter fullscreen mode Exit fullscreen mode

authselectmode require is the policy you usually want once every source is NTS: unauthenticated NTP sources automatically become non-selectable. Other modes from chrony.conf(5):

Mode Behavior
require Auth mandatory for selection
prefer If any auth source exists, unauth sources get noselect
mix Default: auth sources get require trust; unauth allowed only if they agree
ignore Pre-4.0 behavior; selection options only as written

Fedora / RHEL pattern

Same directives in /etc/chrony.conf:

server time.cloudflare.com iburst nts
server nts.netnod.se iburst nts
server ptbtime1.ptb.de iburst nts
server ptbtime2.ptb.de iburst nts

ntsdumpdir /var/lib/chrony
authselectmode require
makestep 1.0 3
rtcsync
driftfile /var/lib/chrony/drift
Enter fullscreen mode Exit fullscreen mode

Optional: private CA or pinned intermediate

If your org runs an internal NTS-KE server with a private CA:

ntstrustedcerts /etc/pki/nts/internal-ca.pem
server nts.time.example.com iburst nts
Enter fullscreen mode Exit fullscreen mode

Use certset when you maintain multiple trust bundles:

ntstrustedcerts 1 /etc/pki/nts/lab-ca.pem
server nts-lab.example.com iburst nts certset 1
Enter fullscreen mode Exit fullscreen mode

Hosts that boot with wildly wrong time

Certificate not-before/not-after checks need a sane clock. If a machine has a dead RTC battery, chrony documents nocerttimecheck as a last resort:

# Disable cert time validation only until the first successful clock update
nocerttimecheck 1
Enter fullscreen mode Exit fullscreen mode

Prefer fixing the RTC or seeding time from a trusted local source. Do not leave unlimited nocerttimecheck on production.

Apply and enable

# Validate config syntax where supported
chronyd -t -f /etc/chrony/chrony.conf   # Debian path; adjust -f as needed

sudo systemctl enable --now chrony.service   # Debian package unit name is often chrony.service
# or:
sudo systemctl enable --now chronyd.service  # Fedora/RHEL/Arch

sudo systemctl restart chrony.service
Enter fullscreen mode Exit fullscreen mode

On Debian the unit is commonly chrony.service (aliasing chronyd). Check what you actually have:

systemctl status chrony.service chronyd.service --no-pager
Enter fullscreen mode Exit fullscreen mode

Verify NTS is really in use

1. Sources

chronyc -n sources -v
Enter fullscreen mode Exit fullscreen mode

Healthy output looks like:

MS Name/IP address         Stratum Poll Reach LastRx Last sample
===============================================================================
^* 162.159.200.1                 3   6   377    12   +120us[ +135us] +/-  8ms
^+ 194.58.202.148                1   6   377    10    -40us[  -28us] +/-  1ms
^+ 192.53.103.108                1   6   377     9    +55us[  +67us] +/-  2ms
^+ 192.53.103.104                1   6   377    11    -12us[   -1us] +/-  2ms
Enter fullscreen mode Exit fullscreen mode

How to read the left flags:

  • ^* — selected sync source
  • ^+ — acceptable, combined
  • ^? — unreachable / not yet usable
  • Reach 377 (octal) — last eight polls succeeded

With providers that separate NTS-KE and NTP timestamping hosts (Netnod is a known example), you may briefly see both the KE hostname and a *-ts.* NTP host. The timestamping host should show a normal stratum and reachability; the KE-only name may look idle after cookies are established. That is expected.

2. Tracking

chronyc tracking
Enter fullscreen mode Exit fullscreen mode

You want a populated Reference ID, a finite "System time" offset, and "Leap status : Normal" once settled.

3. NTS-specific auth data

chronyc -n authdata
Enter fullscreen mode Exit fullscreen mode

Look for sources using NTS with a non-zero cookie count and successful key establishment. Exact columns vary slightly by chrony version; empty/missing NTS rows mean you are still on plain NTP.

4. Activity

chronyc activity
Enter fullscreen mode Exit fullscreen mode

Expect online sources matching your configured servers after the network is up.

5. Cookie persistence

After a successful sync:

sudo ls -l /var/lib/chrony/
# Expect drift file and, with ntsdumpdir set, per-source *.nts cookie dumps
Enter fullscreen mode Exit fullscreen mode

Restart chrony and confirm sources recover without prolonged ^? on every host — cookies should shorten or skip KE when still valid.

Firewall and path checks

Allow outbound:

TCP 4460/tcp  -> NTS-KE servers
UDP 123/udp   -> NTP (most public NTS deployments)
Enter fullscreen mode Exit fullscreen mode

Some operators (for example Netnod) may direct authenticated NTP to a non-123 port after KE. If sources stay at reach 0 after KE succeeds, check vendor docs and allow that UDP port too.

Quick connectivity probes (do not treat success alone as "time is secure"):

# TLS reachability to NTS-KE (Cloudflare example)
openssl s_client -connect time.cloudflare.com:4460 -servername time.cloudflare.com </dev/null | head

# Or with curl if built with the right options — openssl is enough for "is 4460 open + cert sane"
Enter fullscreen mode Exit fullscreen mode

If corporate TLS interception rewrites certificates, NTS-KE will fail unless you deliberately trust the intercepting CA (generally a bad idea for time auth). Prefer a network path that does not MITM port 4460.

DHCP and cloud metadata traps

Many images enable:

  • systemd-timesyncd
  • DHCP NTP options written into chrony (sourcedir /run/chrony-dhcp)
  • Vendor NTP pools without authentication

Those are fine for "get rough time on first boot," but they undermine authselectmode require if left enabled beside NTS. After NTS works:

  1. Disable systemd-timesyncd
  2. Comment DHCP NTP sourcedirs
  3. Remove plain pool / server lines without nts or key

Homelab / fleet pattern

For a small fleet, run one or two internal chrony servers that peer upward with NTS, then let internal hosts use authenticated or network-limited NTP toward those servers.

Client toward public NTS (edge / gateway):

server time.cloudflare.com iburst nts
server nts.netnod.se iburst nts
server ptbtime1.ptb.de iburst nts
server ptbtime2.ptb.de iburst nts
authselectmode require
ntsdumpdir /var/lib/chrony
allow 10.0.0.0/8          # serve LAN if this box is also an NTP server
Enter fullscreen mode Exit fullscreen mode

LAN clients (trusted network only — still better than random Internet NTP):

server time1.lab.example iburst
server time2.lab.example iburst
authselectmode ignore
makestep 0.1 3
Enter fullscreen mode Exit fullscreen mode

If you expose NTS as a server, you need certificates (ntsservercert / ntsserverkey), open 4460/tcp, and operational cookie/key dump handling. That is a separate hardening exercise; most homelabs only need NTS on the outbound edge.

Troubleshooting

Symptom Likely cause Fix
All sources ^?, reach 0 Firewall, DNS, or KE failure Test TCP/4460; check resolvers; read journalctl -u chrony -b
KE works, NTP never reaches UDP blocked or wrong NTP port Allow UDP/123 or vendor port; confirm with chronyc sourcestats
Cert verify errors Wrong CA, intercepting proxy, or clock far in the past Fix CA trust; avoid MITM; consider one-shot nocerttimecheck 1 only to recover
Flapping between sources Normal Prefer multi-source; watch offset/jitter, not perfect stickiness
Still using old pool servers Leftover pool / DHCP sourcedir Grep configs; restart; re-check chronyc sources
Conflict with timesyncd Two daemons fighting disable --now systemd-timesyncd

Useful logs:

journalctl -u chrony -u chronyd -b --no-pager | tail -n 100
Enter fullscreen mode Exit fullscreen mode

Increase temporary debug only when needed (package-specific; often a drop-in with -d is enough for a lab). Revert before production.

What NTS does not buy you

Be honest about the threat model:

  • Delay attacks still exist. An on-path attacker who can hold packets can still skew perceived time within delay thresholds. NTS stops undetectable content forgery; it is not a full PTP/hardware-timestamp replacement.
  • Bad upstream time is still bad. Authenticated garbage from a compromised server is authentic garbage. Use multiple independent organizations as sources.
  • Local root can still set the clock. NTS protects the network path, not a compromised host.
  • Anycast quirks apply to some public services. Read operator notes before depending on a single anycast name exclusively.

Minimal checklist

  1. Install chrony 4.0+
  2. Disable systemd-timesyncd / competing NTP daemons
  3. Configure ≥4 server … iburst nts lines from independent operators
  4. Set ntsdumpdir, authselectmode require, sensible makestep
  5. Open outbound TCP/4460 and UDP/123 (plus vendor NTP ports if required)
  6. systemctl restart chrony / chronyd
  7. Verify with chronyc sources -v, tracking, and authdata
  8. Confirm no plain DHCP/pool sources remain in the running config

References


Wrong time is a security bug that pretends to be an ops nuisance. NTS + chrony is one of the highest leverage hardening changes you can make on a Linux host in under an hour — and unlike a custom pre-shared NTP key mess, it uses the same PKI model the rest of your stack already understands.

Top comments (0)