Set up reliable time synchronization with chrony NTP client, configure secure time servers, and implement monitoring to prevent time drift on production servers.
Prerequisites
- Root or sudo access
- Internet connectivity for NTP synchronization
- Basic familiarity with systemd services
What this solves
Accurate system time is critical for security protocols, log correlation, database transactions, and distributed systems. This tutorial configures chrony as your NTP client with hardened security settings, multiple time sources for redundancy, and monitoring to detect time drift before it impacts your applications.
Step-by-step installation
Update system packages
Start by updating your package manager to ensure you get the latest versions and security patches.
sudo apt update && sudo apt upgrade -y
Install chrony NTP client
Chrony is a modern NTP implementation that handles intermittent network connections better than traditional ntpd. It's the default time synchronization service on most modern Linux distributions.
sudo apt install -y chrony
Stop and disable conflicting time services
Remove systemd-timesyncd and ntp if they're running to prevent conflicts with chrony. Only one time synchronization service should be active.
sudo systemctl stop systemd-timesyncd ntp 2>/dev/null || true
sudo systemctl disable systemd-timesyncd ntp 2>/dev/null || true
sudo systemctl mask systemd-timesyncd
Configure chrony with secure NTP pools
Create a hardened chrony configuration with multiple time sources and security settings. This configuration uses geographically distributed NTP pools for better accuracy and redundancy.
# Use multiple NTP pool sources for redundancy
pool 0.pool.ntp.org iburst maxsources 4
pool 1.pool.ntp.org iburst maxsources 4
pool 2.pool.ntp.org iburst maxsources 4
pool 3.pool.ntp.org iburst maxsources 4
Add public time servers as backup
server time.cloudflare.com iburst
server time.google.com iburst
Record the rate at which the system clock gains/losses time
driftfile /var/lib/chrony/drift
Allow the system clock to be stepped in the first three updates
if its offset is larger than 1 second
makestep 1.0 3
Enable kernel synchronization of the real-time clock (RTC)
rtcsync
Enable hardware timestamping on all interfaces that support it
#hwtimestamp *
Increase the minimum number of selectable sources required to adjust
the system clock
minsources 2
Allow NTP client access from local network only
Uncomment and modify for your network if this server will serve time
#allow 192.168.1.0/24
Serve time even if not synchronized to a time source
#local stratum 10
Specify file containing keys for NTP authentication
#keyfile /etc/chrony/chrony.keys
Save NTP measurements and estimates
logdir /var/log/chrony
Select which information is logged
log measurements statistics tracking
Disable command port for security (uncomment for production)
cmdport 0
Step the system clock instead of slewing it if the adjustment is larger than
one second, but only in the first clockupdate
maxupdateskew 100.0
Ignore leap second for virtual machines
leapsectz right/UTC
Set correct file permissions
Secure the chrony configuration file to prevent unauthorized modifications. Only root should be able to modify time synchronization settings.
sudo chown root:root /etc/chrony/chrony.conf
sudo chmod 644 /etc/chrony/chrony.conf
sudo chown -R _chrony:_chrony /var/lib/chrony 2>/dev/null || sudo chown -R chrony:chrony /var/lib/chrony
Configure hardware clock synchronization
Ensure the hardware clock (RTC) stays synchronized with the system clock. This prevents time drift when the system reboots.
sudo hwclock --systohc
sudo timedatectl set-local-rtc 0
Enable and start chrony service
Enable chrony to start automatically at boot and start it immediately. The service will begin synchronizing time with the configured NTP servers.
sudo systemctl enable chrony
sudo systemctl start chrony
sudo systemctl status chrony
Configure timezone settings
Set your system timezone and verify that automatic time synchronization is enabled through timedatectl.
sudo timedatectl set-timezone UTC
sudo timedatectl set-ntp true
timedatectl status
timedatectl list-timezones to see available options.Create chrony monitoring script
Set up automated monitoring to detect time synchronization issues and excessive drift before they impact your applications.
#!/bin/bash
Chrony monitoring script
Checks time synchronization status and drift
LOG_FILE="/var/log/chrony-monitor.log"
MAX_OFFSET_MS=100
MAX_DRIFT_PPM=10
Function to log messages
log_message() {
echo "$(date '+%Y-%m-%d %H:%M:%S') - $1" | sudo tee -a "$LOG_FILE"
}
Check if chrony is running
if ! systemctl is-active --quiet chrony; then
log_message "ERROR: Chrony service is not running"
exit 1
fi
Get chrony tracking information
TRACKING=$(chronyc tracking 2>/dev/null)
if [ $? -ne 0 ]; then
log_message "ERROR: Unable to get chrony tracking information"
exit 1
fi
Extract values
SYSTEM_TIME=$(echo "$TRACKING" | grep "System time" | awk '{print $4}')
LAST_OFFSET=$(echo "$TRACKING" | grep "Last offset" | awk '{print $3}' | tr -d 's')
FREQ_OFFSET=$(echo "$TRACKING" | grep "Frequency" | awk '{print $2}' | tr -d 'ppm')
Convert offset to milliseconds for comparison
OFFSET_MS=$(echo "$LAST_OFFSET * 1000" | bc 2>/dev/null || echo "0")
OFFSET_ABS=$(echo "$OFFSET_MS" | sed 's/-//')
Check offset threshold
if (( $(echo "$OFFSET_ABS > $MAX_OFFSET_MS" | bc -l) )); then
log_message "WARNING: Time offset is ${OFFSET_MS}ms (threshold: ${MAX_OFFSET_MS}ms)"
fi
Check frequency drift threshold
DRIFT_ABS=$(echo "$FREQ_OFFSET" | sed 's/-//')
if (( $(echo "$DRIFT_ABS > $MAX_DRIFT_PPM" | bc -l) )); then
log_message "WARNING: Frequency drift is ${FREQ_OFFSET}ppm (threshold: ${MAX_DRIFT_PPM}ppm)"
fi
Check source availability
SOURCES=$(chronyc sources 2>/dev/null | grep -c "^\^\*")
if [ "$SOURCES" -eq 0 ]; then
log_message "WARNING: No active time sources available"
fi
log_message "INFO: Offset: ${OFFSET_MS}ms, Drift: ${FREQ_OFFSET}ppm, Active sources: $SOURCES"
Make monitoring script executable and create systemd timer
Set up the monitoring script to run automatically every 15 minutes using systemd timers.
sudo chmod 755 /usr/local/bin/chrony-monitor.sh
sudo chown root:root /usr/local/bin/chrony-monitor.sh
[Unit]
Description=Chrony Time Synchronization Monitor
After=chrony.service
Requires=chrony.service
[Service]
Type=oneshot
User=root
ExecStart=/usr/local/bin/chrony-monitor.sh
StandardOutput=journal
StandardError=journal
[Unit]
Description=Run Chrony Monitor every 15 minutes
Requires=chrony-monitor.service
[Timer]
OnBootSec=5min
OnUnitActiveSec=15min
Persistent=true
[Install]
WantedBy=timers.target
sudo systemctl daemon-reload
sudo systemctl enable chrony-monitor.timer
sudo systemctl start chrony-monitor.timer
Configure log rotation for chrony logs
Set up log rotation to prevent chrony logs from consuming excessive disk space over time.
/var/log/chrony/*.log {
daily
missingok
rotate 52
compress
delaycompress
sharedscripts
create 640 _chrony _chrony
postrotate
/bin/kill -HUP cat /var/run/chrony/chronyd.pid 2>/dev/null 2>/dev/null || true
endscript
}
/var/log/chrony-monitor.log {
weekly
missingok
rotate 12
compress
delaycompress
create 640 root root
}
Advanced security hardening
Disable chrony command port for production
For maximum security on production servers, disable the chrony command port to prevent unauthorized time adjustments.
sudo sed -i '/^#cmdport 0/s/^#//' /etc/chrony/chrony.conf
sudo systemctl restart chrony
Configure firewall rules for NTP
If your server needs to serve time to other systems, configure firewall rules to allow NTP traffic securely.
# Allow outgoing NTP traffic (client mode)
sudo ufw allow out 123/udp comment 'NTP client'
Allow incoming NTP from local network only (server mode)
Uncomment and modify for your network if serving time
sudo ufw allow from 192.168.1.0/24 to any port 123 comment 'NTP server local'
Verify your setup
Check that chrony is working correctly and synchronizing time with multiple sources.
sudo systemctl status chrony
chronyc tracking
chronyc sources -v
chronyc sourcestats
timedatectl status
sudo journalctl -u chrony --no-pager -n 20
Run the monitoring script manually to test it:
sudo /usr/local/bin/chrony-monitor.sh
sudo tail -f /var/log/chrony-monitor.log
The output should show your system is synchronized with multiple NTP sources. Look for an asterisk (*) next to the currently selected source in the sources output.
Monitor time synchronization health
Key metrics to monitor
Monitor these chrony metrics to ensure healthy time synchronization in production environments.
| Metric | Command | Good Value |
|---|---|---|
| Time offset | chronyc tracking | grep "Last offset" | < 100ms |
| Frequency drift | chronyc tracking | grep "Frequency" | < 10ppm |
| Active sources | chronyc sources | grep "^\^\*" | >= 1 |
| Stratum level | chronyc tracking | grep "Stratum" | 2-4 |
Set up email alerts for time issues
Configure the monitoring script to send email alerts when time synchronization problems are detected.
sudo apt install -y mailutils
Add email alerting to the monitoring script:
sudo sed -i '/log_message "ERROR:/a\n echo "$1" | mail -s "Chrony Alert on $(hostname)" admin@example.com' /usr/local/bin/chrony-monitor.sh
sudo sed -i '/log_message "WARNING:/a\n echo "$1" | mail -s "Chrony Warning on $(hostname)" admin@example.com' /usr/local/bin/chrony-monitor.sh
Common issues
| Symptom | Cause | Fix |
|---|---|---|
| chronyc commands fail | Command port disabled | Remove cmdport 0 from config or use systemctl status chrony |
| No sources available | Firewall blocking NTP | sudo ufw allow out 123/udp |
| Time not synchronizing | Large initial offset | sudo chrony -q then restart service |
| High frequency drift | Failing hardware clock | Check hardware, increase maxupdateskew |
| Service fails to start | Configuration syntax error | sudo chronyd -n -d to debug |
| Permission denied on logs | Wrong log directory ownership | sudo chown -R _chrony:_chrony /var/log/chrony |
Next steps
- Configure centralized logging with rsyslog and journald to collect chrony logs
- Set up Grafana and Prometheus monitoring for time synchronization metrics
- Configure your own NTP server with chrony for isolated networks
- Monitor time drift with Prometheus and Grafana for production environments
Automated install script
Run this to automate the entire setup
#!/usr/bin/env bash
set -euo pipefail
# Colors for output
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
NC='\033[0m' # No Color
# Default timezone
TIMEZONE="${1:-UTC}"
# Usage message
usage() {
echo "Usage: $0 [timezone]"
echo "Example: $0 America/New_York"
echo "Default timezone: UTC"
exit 1
}
# Logging functions
log_info() {
echo -e "${GREEN}[INFO]${NC} $1"
}
log_warn() {
echo -e "${YELLOW}[WARN]${NC} $1"
}
log_error() {
echo -e "${RED}[ERROR]${NC} $1"
}
# Cleanup function for rollback
cleanup() {
log_error "Installation failed. Rolling back changes..."
systemctl stop chrony 2>/dev/null || true
systemctl disable chrony 2>/dev/null || true
systemctl unmask systemd-timesyncd 2>/dev/null || true
systemctl enable systemd-timesyncd 2>/dev/null || true
}
# Set trap for cleanup on error
trap cleanup ERR
# Check if running as root or with sudo
if [[ $EUID -ne 0 ]]; then
log_error "This script must be run as root or with sudo"
exit 1
fi
# Validate timezone argument
if [[ $# -gt 1 ]]; then
usage
fi
if [[ -n "${1:-}" ]] && [[ ! -f "/usr/share/zoneinfo/$1" ]]; then
log_error "Invalid timezone: $1"
exit 1
fi
log_info "Starting chrony NTP synchronization setup with timezone: $TIMEZONE"
# Auto-detect distribution
if [ -f /etc/os-release ]; then
. /etc/os-release
case "$ID" in
ubuntu|debian)
PKG_MGR="apt"
PKG_INSTALL="apt install -y"
PKG_UPDATE="apt update && apt upgrade -y"
CHRONY_USER="_chrony"
;;
almalinux|rocky|centos|rhel|ol|fedora)
PKG_MGR="dnf"
PKG_INSTALL="dnf install -y"
PKG_UPDATE="dnf update -y"
CHRONY_USER="chrony"
;;
amzn)
PKG_MGR="yum"
PKG_INSTALL="yum install -y"
PKG_UPDATE="yum update -y"
CHRONY_USER="chrony"
;;
*)
log_error "Unsupported distribution: $ID"
exit 1
;;
esac
else
log_error "Cannot detect distribution - /etc/os-release not found"
exit 1
fi
log_info "Detected distribution: $ID using $PKG_MGR"
# Step 1: Update system packages
echo "[1/8] Updating system packages..."
eval $PKG_UPDATE
log_info "System packages updated successfully"
# Step 2: Install chrony
echo "[2/8] Installing chrony NTP client..."
eval "$PKG_INSTALL chrony"
log_info "Chrony installed successfully"
# Step 3: Stop and disable conflicting services
echo "[3/8] Stopping and disabling conflicting time services..."
systemctl stop systemd-timesyncd ntp 2>/dev/null || true
systemctl disable systemd-timesyncd ntp 2>/dev/null || true
systemctl mask systemd-timesyncd 2>/dev/null || true
log_info "Conflicting time services disabled"
# Step 4: Configure chrony with secure settings
echo "[4/8] Configuring chrony with hardened security settings..."
cat > /etc/chrony.conf << 'EOF'
# Use multiple NTP pool sources for redundancy
pool 0.pool.ntp.org iburst maxsources 4
pool 1.pool.ntp.org iburst maxsources 4
pool 2.pool.ntp.org iburst maxsources 4
pool 3.pool.ntp.org iburst maxsources 4
# Add public time servers as backup
server time.cloudflare.com iburst
server time.google.com iburst
# Record the rate at which the system clock gains/losses time
driftfile /var/lib/chrony/drift
# Allow the system clock to be stepped in the first three updates
# if its offset is larger than 1 second
makestep 1.0 3
# Enable kernel synchronization of the real-time clock (RTC)
rtcsync
# Increase the minimum number of selectable sources required to adjust
# the system clock
minsources 2
# Save NTP measurements and estimates
logdir /var/log/chrony
# Select which information is logged
log measurements statistics tracking
# Disable command port for security
cmdport 0
# Step the system clock instead of slewing it if the adjustment is larger than
# one second, but only in the first clockupdate
maxupdateskew 100.0
# Ignore leap second for virtual machines
leapsectz right/UTC
EOF
log_info "Chrony configuration created"
# Step 5: Set correct file permissions
echo "[5/8] Setting secure file permissions..."
chown root:root /etc/chrony.conf
chmod 644 /etc/chrony.conf
# Create chrony directories if they don't exist
mkdir -p /var/lib/chrony /var/log/chrony
# Set ownership based on distribution
if id "$CHRONY_USER" &>/dev/null; then
chown -R $CHRONY_USER:$CHRONY_USER /var/lib/chrony
chown -R $CHRONY_USER:$CHRONY_USER /var/log/chrony
else
log_warn "Chrony user ($CHRONY_USER) not found, using root ownership"
chown -R root:root /var/lib/chrony /var/log/chrony
fi
chmod 755 /var/lib/chrony /var/log/chrony
log_info "File permissions set securely"
# Step 6: Configure hardware clock synchronization
echo "[6/8] Configuring hardware clock synchronization..."
hwclock --systohc
timedatectl set-local-rtc 0
log_info "Hardware clock synchronization configured"
# Step 7: Set timezone and enable chrony service
echo "[7/8] Setting timezone and enabling chrony service..."
timedatectl set-timezone "$TIMEZONE"
systemctl enable chrony
systemctl start chrony
log_info "Chrony service enabled and started"
# Step 8: Verification
echo "[8/8] Verifying installation and time synchronization..."
# Wait a moment for chrony to initialize
sleep 3
# Check service status
if systemctl is-active --quiet chrony; then
log_info "Chrony service is running"
else
log_error "Chrony service failed to start"
exit 1
fi
# Check time synchronization status
if timedatectl status | grep -q "System clock synchronized: yes"; then
log_info "System clock is synchronized"
elif timedatectl status | grep -q "NTP service: active"; then
log_info "NTP service is active (synchronization in progress)"
else
log_warn "Time synchronization status unclear, checking chrony sources..."
fi
# Display chrony sources
if command -v chronyc >/dev/null; then
echo "Current NTP sources:"
chronyc sources -v || true
echo ""
echo "Tracking information:"
chronyc tracking || true
fi
# Display final system time status
echo ""
echo "System time configuration:"
timedatectl status
echo ""
log_info "Chrony NTP synchronization setup completed successfully!"
log_info "Timezone: $TIMEZONE"
log_info "Configuration file: /etc/chrony.conf"
log_info "Log directory: /var/log/chrony"
# Disable trap since we completed successfully
trap - ERR
Review the script before running. Execute with: bash install.sh