Set up a production-grade NTP server using chrony with client access controls, firewall rules, and security hardening. Learn to configure upstream time sources, implement monitoring, and troubleshoot common synchronization issues.
Prerequisites
- Root or sudo access
- Network connectivity to upstream NTP servers
- Basic understanding of Linux networking
- Firewall management knowledge
What this solves
Precise time synchronization is critical for distributed systems, log correlation, and security protocols. This tutorial shows you how to configure chrony as an NTP server with proper security hardening, client access controls, and monitoring for production environments.
Step-by-step installation
Update system packages
Start by updating your package manager to ensure you get the latest versions of chrony and security tools.
sudo apt update && sudo apt upgrade -y
Install chrony NTP service
Install chrony, which provides better performance and security than traditional ntpd for most use cases.
sudo apt install -y chrony ntpstat
Stop and remove conflicting NTP services
Remove any existing NTP services that might conflict with chrony's operation.
sudo systemctl stop ntp ntpd systemd-timesyncd
sudo systemctl disable ntp ntpd systemd-timesyncd
sudo apt remove -y ntp ntpdate
Configure chrony main settings
Create a secure chrony configuration with reliable upstream servers and proper security settings.
# Use reliable NTP pool servers
pool 0.pool.ntp.org iburst maxsources 4
pool 1.pool.ntp.org iburst maxsources 3
pool 2.pool.ntp.org iburst maxsources 3
pool 3.pool.ntp.org iburst maxsources 3
Add public servers for redundancy
server time.cloudflare.com iburst
server time.google.com iburst
Security settings
port 0
cmdport 323
bindcmdaddress 127.0.0.1
bindcmdaddress ::1
Serve time to local network (adjust subnet as needed)
allow 192.168.1.0/24
allow 10.0.0.0/8
allow 172.16.0.0/12
Rate limiting
ratelimit interval 1 burst 16
ratelimit ntp interval 1 burst 8
Logging and monitoring
log tracking measurements statistics
logdir /var/log/chrony
Drift file and keys
driftfile /var/lib/chrony/chrony.drift
keyfile /etc/chrony/chrony.keys
System clock settings
makestep 1.0 3
rtcsync
Leap second handling
leapsectz right/UTC
Local stratum when disconnected (disable for production)
local stratum 10
Hardware timestamping if supported
hwtimestamp eth0
Create chrony keys file for security
Generate authentication keys for secure command access to chrony daemon.
sudo touch /etc/chrony/chrony.keys
sudo chmod 640 /etc/chrony/chrony.keys
sudo chown root:chrony /etc/chrony/chrony.keys
Add authentication keys to the file:
1 SHA1 HEX:$(openssl rand -hex 20)
2 SHA256 HEX:$(openssl rand -hex 32)
Create chrony log directory
Set up proper logging directory with correct permissions for chrony user.
sudo mkdir -p /var/log/chrony
sudo chown chrony:chrony /var/log/chrony
sudo chmod 755 /var/log/chrony
Configure firewall rules for NTP
Open the NTP port (123/UDP) with rate limiting to prevent abuse while allowing legitimate clients.
sudo ufw allow from 192.168.1.0/24 to any port 123 proto udp
sudo ufw allow from 10.0.0.0/8 to any port 123 proto udp
sudo ufw allow from 172.16.0.0/12 to any port 123 proto udp
sudo ufw limit 123/udp
Configure systemd service limits
Create systemd override to limit resource usage and improve security.
sudo systemctl edit chrony
[Service]
Security hardening
NoNewPrivileges=yes
PrivateTmp=yes
ProtectHome=yes
ProtectSystem=strict
ReadWritePaths=/var/lib/chrony /var/log/chrony /run/chrony
Resource limits
LimitNOFILE=1024
LimitNPROC=64
Network restrictions
RestrictAddressFamilies=AF_INET AF_INET6
Restart policy
Restart=always
RestartSec=10
Enable and start chrony service
Start chrony and enable it to start automatically on boot.
sudo systemctl daemon-reload
sudo systemctl enable chrony
sudo systemctl start chrony
sudo systemctl status chrony
Configure log rotation
Set up log rotation to prevent chrony logs from consuming excessive disk space.
/var/log/chrony/*.log {
daily
rotate 30
compress
delaycompress
missingok
notifempty
create 644 chrony chrony
postrotate
systemctl reload chrony > /dev/null 2>&1 || true
endscript
}
Set up monitoring and alerting
Create a monitoring script to check NTP synchronization status and alert on issues.
#!/bin/bash
NTP monitoring script
MAX_OFFSET=0.1 # Maximum acceptable time offset in seconds
LOG_FILE="/var/log/chrony/ntp-monitor.log"
Check chrony sync status
if ! systemctl is-active --quiet chrony; then
echo "$(date): CRITICAL - chrony service is not running" >> "$LOG_FILE"
exit 2
fi
Get time synchronization status
OFFSET=$(chronyc tracking | grep "System time" | awk '{print $4}' | tr -d ' seconds')
STRATUM=$(chronyc tracking | grep "Stratum" | awk '{print $3}')
SOURCES=$(chronyc sources | grep -c "^\^\*")
if [ "$SOURCES" -eq 0 ]; then
echo "$(date): WARNING - No synchronized time sources" >> "$LOG_FILE"
fi
if [ "$STRATUM" -gt 5 ]; then
echo "$(date): WARNING - High stratum level: $STRATUM" >> "$LOG_FILE"
fi
echo "$(date): OK - Offset: ${OFFSET}s, Stratum: $STRATUM, Sources: $SOURCES" >> "$LOG_FILE"
sudo chmod 755 /usr/local/bin/check-ntp-sync.sh
sudo chown root:root /usr/local/bin/check-ntp-sync.sh
Set up automated monitoring with cron
Schedule regular monitoring checks to detect synchronization issues early.
sudo crontab -e
# Check NTP synchronization every 5 minutes
/5 * /usr/local/bin/check-ntp-sync.sh
Daily NTP statistics report
0 6 * chronyc tracking >> /var/log/chrony/daily-stats.log
Configure client access controls
Set up subnet-based access control
Configure specific network access controls and rate limiting to prevent abuse.
# Allow specific subnets with different rate limits
allow 192.168.1.0/24
allow 10.0.0.0/8
allow 172.16.0.0/12
Deny all other access explicitly
deny all
Rate limiting per client
ratelimit interval 2 burst 16
ratelimit ntp interval 1 burst 8
Client logging
clientloglimit 100000
Configure NTS (Network Time Security)
Enable NTS for encrypted and authenticated time synchronization where supported.
# NTS server configuration
ntsservercert /etc/ssl/certs/ntp-server.crt
ntsserverkey /etc/ssl/private/ntp-server.key
ntsport 4460
ntsprocesses 4
NTS client configuration for upstream servers
server time.cloudflare.com nts
Performance tuning and optimization
Optimize chrony for high-load environments
Configure chrony for better performance under heavy client loads.
# Performance optimizations
schedrr 1
lock_all
noclientlog
Reduce poll intervals for better accuracy
minpoll 4
maxpoll 10
Hardware timestamping if network card supports it
hwtimestamp eth0
hwtimestamp *
Configure kernel-level time synchronization
Enable kernel PLL synchronization for better system clock stability.
# Kernel time synchronization parameters
kernel.time_adj=0
kernel.time_freq=0
kernel.time_maxerror=16000000
kernel.time_esterror=16000000
kernel.time_status=64
Network buffer sizes for NTP
net.core.rmem_max=134217728
net.core.wmem_max=134217728
sudo sysctl -p /etc/sysctl.d/99-ntp-tuning.conf
Verify your setup
Check that chrony is running correctly and serving time to clients.
sudo systemctl status chrony
chronyc tracking
chronyc sources -v
chronyc sourcestats
chronyc clients
ntpstat
Test time synchronization from a client machine:
chronyc -h 203.0.113.10 tracking
ntpdate -q 203.0.113.10
Check firewall rules and network connectivity:
sudo netstat -ulnp | grep :123
sudo ss -ulnp | grep :123
chronyc activity
Common issues
| Symptom | Cause | Fix |
|---|---|---|
| Clients can't connect | Firewall blocking port 123 | Check firewall rules: sudo ufw status or sudo firewall-cmd --list-all |
| Large time offset | System clock drift or bad upstream sources | Check sources: chronyc sources -v and verify connectivity |
| High stratum level | Upstream servers unreachable | Test connectivity: chronyc sourcestats and check network |
| Permission denied logs | Incorrect log directory ownership | Fix ownership: sudo chown -R chrony:chrony /var/log/chrony |
| Service fails to start | Configuration syntax error | Check config: sudo chronyd -Q for syntax validation |
| No client connections | Allow rules too restrictive | Review allow/deny rules in chrony.conf and adjust subnets |
| Rate limiting too aggressive | Burst limits too low | Increase burst values in ratelimit configuration |
| Time jumps frequently | Makestep threshold too low | Adjust makestep parameters or use smoothing |
Next steps
- Monitor Linux system resources with performance alerts and automated responses
- Configure Linux audit system with auditd for security compliance and file monitoring
- Set up Prometheus and Grafana monitoring stack with Docker compose
- Configure NTP monitoring with Grafana dashboards and alerting
- Implement Network Time Security (NTS) for encrypted time synchronization
Automated install script
Run this to automate the entire setup
#!/usr/bin/env bash
set -euo pipefail
# Colors for output
readonly RED='\033[0;31m'
readonly GREEN='\033[0;32m'
readonly YELLOW='\033[1;33m'
readonly NC='\033[0m' # No Color
# Global variables
NETWORK_ALLOW=""
CLEANUP_DONE=false
# Usage message
usage() {
echo "Usage: $0 [OPTIONS]"
echo "Options:"
echo " -n, --network CIDR Network to allow NTP access (default: 192.168.1.0/24)"
echo " -h, --help Show this help message"
echo ""
echo "Example: $0 --network 10.0.0.0/8"
}
# 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" >&2; }
# Cleanup function
cleanup() {
if [ "$CLEANUP_DONE" = false ]; then
log_warn "Installation failed, performing cleanup..."
systemctl stop chrony 2>/dev/null || true
systemctl disable chrony 2>/dev/null || true
rm -f /etc/chrony/chrony.conf.backup 2>/dev/null || true
CLEANUP_DONE=true
fi
}
# Set trap for cleanup on error
trap cleanup ERR
# Parse arguments
while [[ $# -gt 0 ]]; do
case $1 in
-n|--network)
NETWORK_ALLOW="$2"
shift 2
;;
-h|--help)
usage
exit 0
;;
*)
log_error "Unknown option: $1"
usage
exit 1
;;
esac
done
# Set default network if not provided
NETWORK_ALLOW=${NETWORK_ALLOW:-"192.168.1.0/24"}
# Check if running as root
if [[ $EUID -ne 0 ]]; then
log_error "This script must be run as root"
exit 1
fi
# Detect distribution and set package manager
log_info "[1/10] Detecting distribution..."
if [ ! -f /etc/os-release ]; then
log_error "Cannot detect distribution: /etc/os-release not found"
exit 1
fi
. /etc/os-release
case "$ID" in
ubuntu|debian)
PKG_MGR="apt"
PKG_UPDATE="apt update && apt upgrade -y"
PKG_INSTALL="apt install -y"
PKG_REMOVE="apt remove -y"
FIREWALL_CMD="ufw"
CHRONY_USER="chrony"
;;
almalinux|rocky|centos|rhel|ol)
PKG_MGR="dnf"
PKG_UPDATE="dnf update -y"
PKG_INSTALL="dnf install -y"
PKG_REMOVE="dnf remove -y"
FIREWALL_CMD="firewalld"
CHRONY_USER="chrony"
;;
fedora)
PKG_MGR="dnf"
PKG_UPDATE="dnf update -y"
PKG_INSTALL="dnf install -y"
PKG_REMOVE="dnf remove -y"
FIREWALL_CMD="firewalld"
CHRONY_USER="chrony"
;;
amzn)
PKG_MGR="yum"
PKG_UPDATE="yum update -y"
PKG_INSTALL="yum install -y"
PKG_REMOVE="yum remove -y"
FIREWALL_CMD="firewalld"
CHRONY_USER="chrony"
;;
*)
log_error "Unsupported distribution: $ID"
exit 1
;;
esac
log_info "Detected: $PRETTY_NAME"
# Update system packages
log_info "[2/10] Updating system packages..."
$PKG_UPDATE
# Install chrony and required tools
log_info "[3/10] Installing chrony and ntpstat..."
$PKG_INSTALL chrony ntpstat openssl
# Stop and disable conflicting services
log_info "[4/10] Stopping conflicting NTP services..."
for service in ntp ntpd systemd-timesyncd; do
systemctl stop $service 2>/dev/null || true
systemctl disable $service 2>/dev/null || true
done
# Remove conflicting packages
if [ "$PKG_MGR" = "apt" ]; then
$PKG_REMOVE ntp ntpdate 2>/dev/null || true
else
$PKG_REMOVE ntp ntpdate 2>/dev/null || true
fi
# Backup existing chrony configuration
if [ -f /etc/chrony/chrony.conf ]; then
cp /etc/chrony/chrony.conf /etc/chrony/chrony.conf.backup
fi
# Configure chrony main settings
log_info "[5/10] Configuring chrony..."
cat > /etc/chrony/chrony.conf << EOF
# Use reliable NTP pool servers
pool 0.pool.ntp.org iburst maxsources 4
pool 1.pool.ntp.org iburst maxsources 3
pool 2.pool.ntp.org iburst maxsources 3
pool 3.pool.ntp.org iburst maxsources 3
# Add public servers for redundancy
server time.cloudflare.com iburst
server time.google.com iburst
# Security settings
port 0
cmdport 323
bindcmdaddress 127.0.0.1
bindcmdaddress ::1
# Serve time to specified networks
allow $NETWORK_ALLOW
allow 10.0.0.0/8
allow 172.16.0.0/12
# Rate limiting
ratelimit interval 1 burst 16
ratelimit ntp interval 1 burst 8
# Logging and monitoring
log tracking measurements statistics
logdir /var/log/chrony
# Drift file and keys
driftfile /var/lib/chrony/chrony.drift
keyfile /etc/chrony/chrony.keys
# System clock settings
makestep 1.0 3
rtcsync
# Leap second handling
leapsectz right/UTC
# Local stratum when disconnected
local stratum 10
EOF
# Set proper permissions on config
chmod 644 /etc/chrony/chrony.conf
# Create chrony keys file
log_info "[6/10] Creating authentication keys..."
mkdir -p /etc/chrony
cat > /etc/chrony/chrony.keys << EOF
1 SHA1 HEX:$(openssl rand -hex 20)
2 SHA256 HEX:$(openssl rand -hex 32)
EOF
chmod 640 /etc/chrony/chrony.keys
chown root:$CHRONY_USER /etc/chrony/chrony.keys
# Create chrony log directory
log_info "[7/10] Setting up log directory..."
mkdir -p /var/log/chrony
chown $CHRONY_USER:$CHRONY_USER /var/log/chrony
chmod 755 /var/log/chrony
# Configure firewall
log_info "[8/10] Configuring firewall..."
if [ "$FIREWALL_CMD" = "ufw" ]; then
ufw --force enable
ufw allow from $NETWORK_ALLOW to any port 123 proto udp
ufw allow from 10.0.0.0/8 to any port 123 proto udp
ufw allow from 172.16.0.0/12 to any port 123 proto udp
ufw limit 123/udp
else
systemctl enable firewalld
systemctl start firewalld
firewall-cmd --permanent --add-rich-rule="rule family=\"ipv4\" source address=\"${NETWORK_ALLOW}\" port protocol=\"udp\" port=\"123\" accept"
firewall-cmd --permanent --add-rich-rule="rule family=\"ipv4\" source address=\"10.0.0.0/8\" port protocol=\"udp\" port=\"123\" accept"
firewall-cmd --permanent --add-rich-rule="rule family=\"ipv4\" source address=\"172.16.0.0/12\" port protocol=\"udp\" port=\"123\" accept"
firewall-cmd --reload
fi
# Configure systemd service limits
log_info "[9/10] Hardening systemd service..."
mkdir -p /etc/systemd/system/chrony.service.d
cat > /etc/systemd/system/chrony.service.d/security.conf << EOF
[Service]
# Security hardening
NoNewPrivileges=true
PrivateTmp=true
ProtectSystem=strict
ProtectHome=true
ReadWritePaths=/var/lib/chrony /var/log/chrony
ProtectKernelTunables=true
ProtectKernelModules=true
ProtectControlGroups=true
RestrictRealtime=true
RestrictNamespaces=true
SystemCallArchitectures=native
EOF
# Start and enable chrony service
systemctl daemon-reload
systemctl enable chrony
systemctl start chrony
# Verification
log_info "[10/10] Verifying installation..."
sleep 3
if systemctl is-active --quiet chrony; then
log_info "✓ Chrony service is running"
else
log_error "✗ Chrony service is not running"
exit 1
fi
if chrony sources &>/dev/null; then
log_info "✓ Chrony sources are accessible"
else
log_warn "! Chrony sources command failed (may need time to sync)"
fi
# Final status
log_info "NTP server installation completed successfully!"
log_info "Configuration file: /etc/chrony/chrony.conf"
log_info "Log directory: /var/log/chrony"
log_info "Allowed network: $NETWORK_ALLOW"
log_info ""
log_info "Check status with: chronyc sources -v"
log_info "Check tracking with: chronyc tracking"
CLEANUP_DONE=true
Review the script before running. Execute with: bash install.sh