Introduction

Accurate time synchronization is critical for distributed systems — log correlation, TLS certificate validation, database replication, and Kerberos authentication all depend on it. Chrony is the default NTP implementation on modern Linux distributions (RHEL 8+, Ubuntu 20.04+), replacing the legacy ntpd. Ansible makes it easy to configure Chrony consistently across your entire fleet.

Basic Chrony Deployment

---
- name: Configure time synchronization with Chrony
  hosts: all
  become: true
  vars:
    ntp_servers:
      - 0.pool.ntp.org
      - 1.pool.ntp.org
      - 2.pool.ntp.org
      - 3.pool.ntp.org
    chrony_timezone: "UTC"
  tasks:
    - name: Install Chrony
      ansible.builtin.package:
        name: chrony
        state: present

    - name: Set timezone
      community.general.timezone:
        name: "{{ chrony_timezone }}"

    - name: Configure Chrony
      ansible.builtin.template:
        src: chrony.conf.j2
        dest: /etc/chrony.conf
        owner: root
        group: root
        mode: "0644"
        backup: true
      notify: restart chrony

    - name: Enable and start Chrony
      ansible.builtin.systemd:
        name: chronyd
        enabled: true
        state: started

    - name: Wait for time sync
      ansible.builtin.command:
        cmd: chronyc waitsync 10 0.1
      register: sync_result
      changed_when: false
      failed_when: false

    - name: Check synchronization status
      ansible.builtin.command:
        cmd: chronyc tracking
      register: tracking
      changed_when: false

    - name: Display sync status
      ansible.builtin.debug:
        msg: "{{ tracking.stdout_lines }}"

  handlers:
    - name: restart chrony
      ansible.builtin.systemd:
        name: chronyd
        state: restarted

Configuration Template

# templates/chrony.conf.j2
# Ansible managed — {{ template_host }}

# NTP servers
{% for server in ntp_servers %}
server {{ server }} iburst
{% endfor %}

{% if inventory_hostname in groups.get('ntp_servers', []) %}
# This host is an NTP server — allow clients
{% for network in chrony_allow_networks | default(['10.0.0.0/8', '172.16.0.0/12', '192.168.0.0/16']) %}
allow {{ network }}
{% endfor %}
local stratum 10
{% endif %}

# Record the rate at which the system clock gains/loses time
driftfile /var/lib/chrony/drift

# Allow the system clock to be stepped in the first three updates
makestep 1.0 3

# Enable kernel synchronization of the real-time clock (RTC)
rtcsync

# Specify directory for log files
logdir /var/log/chrony

# Select which information is logged
log measurements statistics tracking

NTP Server + Client Hierarchy

# group_vars/ntp_servers.yml
ntp_servers:
  - 0.pool.ntp.org
  - 1.pool.ntp.org
  - 2.pool.ntp.org
chrony_allow_networks:
  - "10.0.0.0/8"

# group_vars/all.yml (clients point to internal NTP servers)
ntp_servers: "{{ groups['ntp_servers'] | map('extract', hostvars, 'ansible_host') | list }}"
# inventory
[ntp_servers]
ntp-1 ansible_host=10.0.0.10
ntp-2 ansible_host=10.0.0.11

[webservers]
web-1 ansible_host=10.0.1.10
web-2 ansible_host=10.0.1.11

[databases]
db-1 ansible_host=10.0.2.10

Firewall Configuration

- name: Configure firewall for NTP
  hosts: ntp_servers
  become: true
  tasks:
    - name: Allow NTP traffic (firewalld)
      ansible.posix.firewalld:
        service: ntp
        permanent: true
        state: enabled
        immediate: true
      when: ansible_facts.services['firewalld.service'] is defined

    - name: Allow NTP traffic (ufw)
      community.general.ufw:
        rule: allow
        port: "123"
        proto: udp
      when: ansible_os_family == "Debian"

Monitoring and Validation

- name: Validate time synchronization
  hosts: all
  become: true
  tasks:
    - name: Check Chrony sources
      ansible.builtin.command:
        cmd: chronyc sources -v
      register: sources
      changed_when: false

    - name: Check tracking info
      ansible.builtin.command:
        cmd: chronyc tracking
      register: tracking
      changed_when: false

    - name: Extract system clock offset
      ansible.builtin.set_fact:
        clock_offset_ms: "{{ tracking.stdout | regex_search('System time\\s+:\\s+([\\d.]+)', '\\1') | first | float * 1000 }}"

    - name: Assert time is synchronized
      ansible.builtin.assert:
        that:
          - clock_offset_ms | float < 100  # Less than 100ms offset
        fail_msg: "Clock offset {{ clock_offset_ms }}ms exceeds threshold"
        success_msg: "Clock offset {{ clock_offset_ms }}ms — OK"

    - name: Report NTP source count
      ansible.builtin.debug:
        msg: "{{ sources.stdout_lines | select('match', '^\\^') | list | length }} NTP sources configured"

Troubleshooting

# Check sync status
chronyc tracking

# List NTP sources with status
chronyc sources -v

# Check source statistics
chronyc sourcestats

# Force immediate sync
chronyc makestep

# Check NTP activity
chronyc activity

# Verify NTP port is reachable
nc -zuv ntp-server 123

Conclusion

Chrony is the modern standard for NTP on Linux. With Ansible, you can deploy a consistent time synchronization hierarchy — dedicated NTP servers syncing to public pools, with all other hosts pointing to your internal servers. Add monitoring assertions to catch clock drift before it causes authentication failures or log correlation issues.