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:
- 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.
- 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
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
Fedora / RHEL-family:
sudo dnf install chrony
sudo systemctl enable --now chronyd
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
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)
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
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
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
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
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
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
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
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
On Debian the unit is commonly chrony.service (aliasing chronyd). Check what you actually have:
systemctl status chrony.service chronyd.service --no-pager
Verify NTS is really in use
1. Sources
chronyc -n sources -v
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
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
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
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
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
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)
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"
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:
- Disable
systemd-timesyncd - Comment DHCP NTP sourcedirs
- Remove plain
pool/serverlines withoutntsorkey
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
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
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
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
- Install chrony 4.0+
- Disable
systemd-timesyncd/ competing NTP daemons - Configure ≥4
server … iburst ntslines from independent operators - Set
ntsdumpdir,authselectmode require, sensiblemakestep - Open outbound TCP/4460 and UDP/123 (plus vendor NTP ports if required)
-
systemctl restart chrony/chronyd - Verify with
chronyc sources -v,tracking, andauthdata - Confirm no plain DHCP/pool sources remain in the running config
References
- RFC 8915 — Network Time Security for the Network Time Protocol
- chrony.conf(5) — official documentation
- Cloudflare Time Services — NTS
- Netnod — How to use NTS
- ArchWiki — Chrony (NTS section)
- Community list of public NTS servers (vet before production use; no automatic endorsement)
- NTP Pool — upstream server quantity guidance
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)