How to Use Shell Scripting to Synchronize the Time on Linux

Time synchronization is a cornerstone of modern IT infrastructure. From accurate logging and secure communications to distributed systems coordination, consistent system time across servers and endpoints is essential. For IT professionals and Managed Service Providers (MSPs), having a reliable and automated method to synchronize time on Linux systems is critical. This post delves into a robust shell script designed for synchronizing time using chronyd or systemd-timesyncd, two widely supported network time protocol (NTP) services in Linux.

Background

Many IT environments, especially those relying on centralized management and automation platforms like NinjaOne, require strict time synchronization. Log correlations, file versioning, authentication tokens, and audit trails all depend on precise system clocks. Although NTP services like chronyd and systemd-timesyncd often handle this quietly in the background, misconfiguration, service failures, or system image inconsistencies can throw off synchronization.

This shell script addresses those issues head-on by automatically detecting available time sync services, ensuring they’re properly configured, temporarily starting them if needed, synchronizing the time, and reporting detailed diagnostics. It also gracefully manages services, returning systems to their previous state if time synchronization services were initially disabled.

The Script:

#!/usr/bin/env bash
#
# Description: Synchronize the time on Linux using the system's network time server. Expects chrony or systemd-timesyncd to be installed and running.
# By using this script, you indicate your acceptance of the following legal terms as well as our Terms of Use at https://www.ninjaone.com/terms-of-use.
# Ownership Rights: NinjaOne owns and will continue to own all right, title, and interest in and to the script (including the copyright). NinjaOne is giving you a limited license to use the script in accordance with these legal terms. 
# Use Limitation: You may only use the script for your legitimate personal or internal business purposes, and you may not share the script with another party. 
# Republication Prohibition: Under no circumstances are you permitted to re-publish the script in any script library or website belonging to or under the control of any other software provider. 
# Warranty Disclaimer: The script is provided “as is” and “as available”, without warranty of any kind. NinjaOne makes no promise or guarantee that the script will be free from defects or that it will meet your specific needs or expectations. 
# Assumption of Risk: Your use of the script is at your own risk. You acknowledge that there are certain inherent risks in using the script, and you understand and assume each of those risks. 
# Waiver and Release: You will not hold NinjaOne responsible for any adverse or unintended consequences resulting from your use of the script, and you waive any legal or equitable rights or remedies you may have against NinjaOne relating to your use of the script. 
# EULA: If you are a NinjaOne customer, your use of the script is subject to the End User License Agreement applicable to you (EULA).
#
# Minimum OS Architecture Supported: Debian 11 (Bullseye)+, Red Hat Enterprise Linux (RHEL) 8+
#
# Release Notes: Initial Release
#

# When run directly without testing, the "__()" function does nothing.
test || __() { :; }

die() {
    local _ret="${2:-1}"
    echo "$1" >&2
    exit "${_ret}"
}

function GetChronyConfigFile() {
    local _configFile
    # Check if there is a chrony config folder
    if [[ -d "/etc/chrony" ]]; then
        # Check if the config file exists
        if [[ -f "/etc/chrony/chrony.conf" ]]; then
            _configFile="/etc/chrony/chrony.conf"
        fi
    fi
    # Check if primary default config file exists, this takes precedence over the chrony folder
    if [[ -f "/etc/chrony.conf" ]]; then
        _configFile="/etc/chrony.conf"
    fi
    # Get the system's default config file for chronyd
    _chronySystemDefaultConfigFile=$(
        # Get man page for chronyd
        # Looking for a line containing this "The compiled-in default value is /etc/chrony/chrony.conf."
        man chronyd |
            # Get the default config file location
            grep /chrony.conf | grep default |
            # Get the last item in the line
            awk '{print $NF}' |
            # Remove the trailing period
            sed 's/\.$//'
    )
    # Check if the config file exists
    if [[ -f "$_chronySystemDefaultConfigFile" ]]; then
        _configFile="$_chronySystemDefaultConfigFile"
    fi

    # Validate that the config file exists
    if [[ -n "${_configFile}" && -f "$_configFile" ]]; then
        echo "$_configFile"
    else
        echo "[Error] No chrony config file found"
        exit 1
    fi
}

function GetNtpPoolServers() {
    local -a _configFiles
    local -a _pools
    local -a _servers
    local -a _timedatectlConfigFiles
    _timedatectlConfigFiles=(
        # Last take precedence over previous ones
        "/usr/lib/systemd/timesyncd.conf.d"
        "/usr/local/lib/systemd/timesyncd.conf.d"
        "/run/systemd/timesyncd.conf.d"
        "/etc/systemd/timesyncd.conf.d"
        "/usr/lib/systemd/timesyncd.conf"
        "/usr/local/lib/systemd/timesyncd.conf"
        "/run/systemd/timesyncd.conf"
        "/etc/systemd/timesyncd.conf"
    )
    # Get the NTP pool servers for the specified region

    echo "[Info] Getting NTP pools and servers"
    if command -v chronyc &>/dev/null; then
        # Get the NTP pool servers for chrony
        if [[ -f "${_chronyConfigFile}" ]]; then
            _pools+=$(grep -E "^pool" "${_chronyConfigFile}" | awk '{print $2}' | sort -u)
            _servers+=$(grep -E "^server" "${_chronyConfigFile}" | awk '{print $2}' | sort -u)
        fi
    elif command -v timedatectl &>/dev/null; then
        # Get the NTP pool servers for systemd-timesyncd
        for configFile in "${_timedatectlConfigFiles[@]}"; do
            if [[ -d "$configFile" ]]; then
                # If the config file is a directory, check for .conf files inside it
                for confFile in "$configFile"/*.conf; do
                    if [[ -f "$confFile" ]]; then
                        # Append the contents of the .conf file to the list of config files
                        _pools+=$(grep -E "^NTP=" "$configFile" | awk -F '=' '{print $2}' | sort -u)
                        _pools+=$(grep -E "^FallbackNTP=" "$configFile" | awk -F '=' '{print $2}' | sort -u)
                    fi
                done
            elif [[ -f "$configFile" ]]; then
                _pools+=$(grep -E "^NTP=" "$configFile" | awk -F '=' '{print $2}' | sort -u)
                _pools+=$(grep -E "^FallbackNTP=" "$configFile" | awk -F '=' '{print $2}' | sort -u)
            fi
        done
    else
        echo "[Error] chronyc or timedatectl not found"
        exit 1
    fi

    # Return the list of NTP pools and servers
    if [[ -n "${_pools}" ]]; then
        echo "[Info] NTP pools:"
        echo "$_pools"
    fi
    if [[ -n "${_servers}" ]]; then
        echo "[Info] NTP servers:"
        echo "$_servers"
    fi
    if [[ -z "${_pools}" && -z "${_servers}" ]]; then
        # If no pools or servers were found
        echo "[Info] Distribution's default NTP servers are being used."
    fi
}

function GetNtpConfiguration() {

    # Get the NTP configuration for the specified region

    if command -v chronyc &>/dev/null; then
        echo "[Info] NTP configuration:"
        if ! [[ -f "$_chronyConfigFile" ]]; then
            _chronyConfigFile="$(GetChronyConfigFile)"
        fi

        # Get the NTP configuration for chrony
        grep -E "^server|^pool" "${_chronyConfigFile}" 2>/dev/null
        if [[ -f "${_chronyConfigFile}" ]]; then
            grep -E "^sourcedir" "${_chronyConfigFile}" 2>/dev/null | awk '{print $2}' | while read -r _dir; do
                if [[ -d "$_dir" ]]; then
                    for confFile in "$_dir"/*.conf; do
                        # Check if the file exists before grepping
                        if [[ -f "$confFile" ]]; then
                            grep -E "^server|^pool" <"$confFile" 2>/dev/null
                        fi
                    done
                fi
            done
            grep -E "^confdir" "${_chronyConfigFile}" 2>/dev/null | awk '{print $2}' | while read -r _dir; do
                if [[ -d "$_dir" ]]; then
                    for confFile in "$_dir"/*.conf; do
                        # Check if the file exists before grepping
                        if [[ -f "$confFile" ]]; then
                            grep -E "^server|^pool" <"$confFile" 2>/dev/null
                        fi
                    done
                fi
            done
        fi
    elif command -v timedatectl &>/dev/null; then
        # Get the NTP configuration for systemd-timesyncd
        if (("$(timedatectl --version | grep systemd | awk '{print $2}')" >= 239)); then # Try to get the NTP configuration
            echo "[Info] NTP configuration:"
            timedatectl show-timesync 2>/dev/null
        else
            # If it fails then we are running and older version of systemd, then get the status
            echo "[Warn] systemd version is older than 239, show-timesync is not available."
        fi
    else
        echo "[Error] No NTP configuration found."
        exit 1
    fi
}

function IsChronySecurityConfigured() {
    # Check if the keyfile is set in chrony.conf
    if grep -q "^keyfile" "${_chronyConfigFile}" 2>/dev/null; then
        # Check if commandkey is set in chrony.conf
        if grep -q "^commandkey" "${_chronyConfigFile}" 2>/dev/null; then
            echo "[Info] Keyfile and commandkey are set in ${_chronyConfigFile}"
            _id=$(grep "^keyfile" "${_chronyConfigFile}" 2>/dev/null | awk '{print $2}')
            _keyfile=$(grep "^commandkey" "${_chronyConfigFile}" 2>/dev/null | awk '{print $2}')
            # Check if the keyfile exists
            if [[ -f "$_keyfile" ]]; then
                # Check if commandkey exists in keyfile
                if grep -q "^$_id" "$_keyfile"; then
                    # Keyfile exists and commandkey is set
                    return 0
                else
                    # Keyfile exists but commandkey is not set
                    echo "[Error] Keyfile $_keyfile exists but commandkey $_id is not set in it"
                    return 1
                fi
            else
                # Keyfile does not exist
                echo "[Error] Keyfile $_keyfile does not exist"
                return 1
            fi
        else
            # Keyfile is set but commandkey is not set
            return 1
        fi
    else
        # Keyfile is not set
        return 1
    fi
}

__ begin __

# Check if the script is being run as root. If not, exit with an error message.
if [[ $(id -u) -ne 0 ]]; then
    die "[Error] This script must be run with root permissions. Try running it with sudo or as the system/root user." 1
fi

_is_enabled=0
_is_active=0

# Check if time synchronization services are installed
if systemctl list-unit-files | grep -q -E "(systemd-timesyncd|chronyd).service"; then
    echo "[Info] Time synchronization service(s) found:"
    # Check if systemd-timesyncd or chronyd is running
    while read -r service; do                                          # Read each service name
        if systemctl is-enabled --quiet "${service}" 2>/dev/null; then # Check if the service is enabled
            echo "[Info] ${service} is enabled"
            _is_enabled=1
        else
            echo "[Info] ${service} is not enabled"
        fi
        if systemctl is-active --quiet "${service}" 2>/dev/null; then # Check if the service is active/running
            echo "[Info] ${service} is running"
            _is_active=1
        else
            echo "[Info] ${service} is not running"
        fi
    done < <(
        systemctl list-unit-files |                         # List all unit files
            grep -E "(systemd-timesyncd|chronyd).service" | # Filter for systemd-timesyncd or chronyd
            awk '{print $1}'                                # Get the first column (service name)
    )
    _service_name=$(systemctl list-unit-files | grep -E "(systemd-timesyncd|chronyd).service" | awk '{print $1}')
    if [[ -z "${_service_name}" ]] && [[ $_is_enabled -eq 0 ]] && [[ "${_is_active}" -eq 0 ]]; then
        die "[Error] No time synchronization service found. Please install systemd-timesyncd, or chrony." 1
    fi
    if [[ "${_is_active}" -eq 0 ]]; then
        echo "[Info] Time synchronization service is not running. Will start it now, sync it, and stop it after."
        # Start the service
        if [[ "${_service_name}" == *"chronyd"* ]]; then
            # If chronyd is available, start it as it takes precedence over systemd-timesyncd
            # Start chronyd
            echo "[Info] Starting time synchronization service: chronyd"
            systemctl start chronyd.service 2>/dev/null || die "[Error] Failed to start chronyd service" 1
            sleep 2
        elif [[ "${_service_name}" == *"systemd-timesyncd"* ]]; then
            # chronyd is not available, so we will use systemd-timesyncd
            # Start systemd-timesyncd
            echo "[Info] Starting time synchronization service: systemd-timesyncd"
            systemctl start systemd-timesyncd.service 2>/dev/null || die "[Error] Failed to start systemd-timesyncd service" 1
            sleep 2
        else
            # If neither chronyd nor systemd-timesyncd is available, exit with an error
            die "[Error] No time synchronization service found. Please install systemd-timesyncd, or chrony." 1
        fi
    fi
else
    die "[Error] No time synchronization service found. Please install systemd-timesyncd, or chrony." 1
fi

_shouldError=0

# Sync time now
echo "[Info] Syncing time now"
if command -v chronyc &>/dev/null; then
    # Check if keyfile is set in chrony.conf
    if IsChronySecurityConfigured; then
        # Use chronyc to sync time
        echo "[Info] Syncing time using chronyc with keyfile in ${_chronyConfigFile}"
        if [[ "$(chronyc -a makestep 2>/dev/null)" == *"OK"* ]]; then
            echo "[Info] Time syncing"
        else
            echo "[Error] Failed to sync time using chronyc"
            _shouldError=1
        fi
    else
        # Sync time using chronyc
        echo "[Info] Syncing time using chronyc"
        if [[ "$(chronyc makestep 2>/dev/null)" == *"OK"* ]]; then
            echo "[Info] Time syncing"
        else
            echo "[Error] Failed to sync time using chronyc"
            _shouldError=1
        fi
    fi

    # Verify that time is synced if we had no errors
    if ((_shouldError == 0)); then
        # Sleep for 5 seconds to allow some time to sync after restarting chronyd
        sleep 5

        # Check if time is synced with chronyc tracking
        case $(chronyc tracking 2>/dev/null | grep "Leap status") in
        *"Leap Second"* | *"Normal"*)
            # If the leap status is "Leap Second" or "Normal", then time is synced
            echo "[Info] Time synced successfully"
            ;;
        *"Insert second"*)
            # If the leap status is "Insert second", then time is being synced
            echo "[Info] Chrony is in the process of inserting a second and will be synced over time to prevent time jumps."
            ;;
        *"Delete second"*)
            # If the leap status is "Delete second", then time is being synced
            echo "[Info] Chrony is in the process of deleting a second and will be synced over time to prevent time jumps."
            ;;
        *"Not synchronized"*)
            # If the leap status is "Not synchronized", then time is not synced
            echo "[Error] Chrony is not synchronized."
            _shouldError=1
            ;;
        *"Unknown"*)
            # If the leap status is "Unknown", then time is not synced
            echo "[Error] Unknown status from chronyc tracking."
            _shouldError=1
            ;;
        *)
            # If the leap status is not recognized, then time is not synced
            echo "[Error] Failed to get sync status from chronyc"
            _shouldError=1
            ;;
        esac

        # Stop chronyd if it was not running before
        if ((_is_active == 0)); then
            sleep 5
            # Get the service name
            _service_name=$(systemctl list-unit-files | grep -E "chronyd.service" | awk '{print $1}')
            echo "[Info] Stopping time synchronization service: ${_service_name}"
            # Stop the service
            systemctl stop "$_service_name" 2>/dev/null
        fi
    fi
elif command -v timedatectl &>/dev/null; then
    # Sync time using timedatectl
    echo "[Info] Syncing time using timedatectl"
    # Restart systemd-timesyncd to force a sync
    if systemctl restart systemd-timesyncd.service 2>/dev/null; then
        if timedatectl status 2>/dev/null | grep "synchronized" | grep -q "yes"; then
            echo "[Info] Time synced successfully"
        else
            echo "[Error] Failed to sync time using timedatectl"
            _shouldError=1
        fi
    else
        echo "[Error] Failed to sync time using timedatectl"
        _shouldError=1
    fi

    # Stop systemd-timesyncd if it was not running before
    if ((_is_active == 0)); then
        sleep 5
        # Get the service name
        _service_name=$(systemctl list-unit-files | grep -E "systemd-timesyncd.service" | awk '{print $1}')
        echo "[Info] Stopping time synchronization service: ${_service_name}"
        # Stop the service
        systemctl stop "$_service_name" 2>/dev/null
    fi
else
    echo "[Error] No time synchronization service found. Please install systemd-timesyncd, or chrony."
    exit 1
fi

echo ""

GetNtpConfiguration
GetNtpPoolServers

if ((_shouldError == 1)); then
    exit 1
fi

 

Detailed Breakdown

This script is structured into multiple functions and sections for modular control and clarity. Here’s a breakdown of its main components:

1. Pre-flight Checks

  • Root Permission Check
    Ensures the script is run as root since time and service control require elevated privileges.
  • Service Discovery
    Searches for either chronyd or systemd-timesyncd. If neither is installed, the script exits with a clear error message.

2. Service Control and Sync

  • Service State Evaluation
    Determines whether each service is enabled and/or running. If not running, it starts the appropriate one temporarily.
  • Time Synchronization
    • If using chronyc: Issues the makestep command to immediately synchronize time.
    • If using timedatectl: Restarts systemd-timesyncd and checks sync status.
  • Service Restoration
    If a service was initially inactive, the script stops it again after syncing.

3. Configuration Inspection

  • GetChronyConfigFile(): Locates the chrony configuration file based on system conventions or man page defaults.
  • GetNtpPoolServers(): Collects NTP pools and server configurations from chronyd or timesyncd config files.
  • GetNtpConfiguration(): Outputs all server, pool, and possibly sourced configurations to help validate NTP settings.
  • IsChronySecurityConfigured(): Verifies that chronyd has security parameters like keyfile and commandkey set correctly.

Potential Use Cases

Case Study: Remote Patch Management

An MSP is managing Linux systems across various time zones. A recent patch deployed via NinjaOne reports inconsistent installation timestamps. The administrator rolls out this script as a pre-check step in a NinjaOne policy. The script ensures all systems sync to the same NTP pool before the patch process begins. As a result, logs align properly, and reporting is accurate.

Comparisons

MethodAdvantagesLimitations
ntpdate (legacy)Quick, standaloneDeprecated, unreliable
chronyd/systemd-timesyncdModern, secure, persistentRequires proper config
This ScriptAutomated detection, validation, security checksInitial root access needed

Unlike ad-hoc commands or legacy tools, this script provides a structured, error-aware, and service-agnostic method suitable for automated deployment.

Implications

Poor time synchronization can lead to a host of issues — failed logins due to token mismatch, corrupted logs, and broken replication. By validating configuration files, checking service security, and providing audit-friendly logs, this script enhances infrastructure reliability and security. It’s particularly important for systems involved in identity management, secure communications, and compliance-heavy environments.

Recommendations

  • Pre-Deployment Test: Always run the script in a staging environment first.
  • Pair With Monitoring: Use NinjaOne or similar tools to monitor service status after deployment.
  • Include in Base Image: Add this script as part of post-install configuration to prevent future drift.
  • Log Output: Redirect script output to a log file for auditability (./sync-time.sh >> /var/log/time-sync.log).

Final Thoughts

Time synchronization is foundational to operational integrity across all layers of IT. This script offers a resilient, portable, and standards-compliant way to synchronize time on Linux using shell scripting. Integrated with platforms like NinjaOne, this script empowers IT professionals and MSPs to maintain uniformity across their fleet effortlessly — improving audit readiness, data accuracy, and system reliability.

For those seeking to synchronize the time on Linux using shell scripting, this tool is both robust and automation-friendly. By embedding it into lifecycle processes, you can ensure consistent and secure timekeeping across your infrastructure.

FAQs

The script halts and advises the user to install one of the two services.

Yes, it’s fully compatible with automation platforms and cron jobs.

No. If a time sync service was inactive before the script ran, it will be stopped again after syncing.

Yes, assuming service restarts don’t affect dependent applications. Testing in staging is recommended.

Next Steps

Building an efficient and effective IT team requires a centralized solution that acts as your core service delivery tool. NinjaOne enables IT teams to monitor, manage, secure, and support all their devices, wherever they are, without the need for complex on-premises infrastructure.

Learn more about NinjaOne Remote Script Deployment, check out a live tour, or start your free trial of the NinjaOne platform.

Categories:

You might also like