Skip to content

Commit cce5201

Browse files
committed
feat: Support key and keyfile for authentication
Feature: Allow users to specify an authentication key for a server using the new `key` parameter for items in the `timesync_ntp_servers` list. The `key` value is as described at https://chrony-project.org/doc/latest/chrony.conf.html#keyfile except that the ID number is auto-generated by the role. Reason: Users need to be able to specify authentication keys for secure time synchronization. Result: Users can configure secure time synchronization. Assisted-by: Cursor using models Cursor Grok 4.5, Composer 2.5, Sonnet 5. Signed-off-by: Rich Megginson <rmeggins@redhat.com>
1 parent b48f052 commit cce5201

18 files changed

Lines changed: 311 additions & 23 deletions

README.md

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,26 @@ timesync_ntp_servers:
5959
xleave: false # Flag enabling interleaved mode (default false)
6060
filter: 1 # Number of NTP measurements per clock update
6161
# (default 1)
62+
key: !vault ... # Optional NTP authentication key for the
63+
# server when using chrony. The value is the
64+
# optional key type followed by the key
65+
# contents, as described in the chrony
66+
# keyfile documentation, except for the
67+
# numeric ID. Multiple servers may share the
68+
# same key. IMPORTANT: you are strongly
69+
# recommended to vault encrypt the value,
70+
# see
71+
# https://docs.ansible.com/ansible/latest/user_guide/vault.html
72+
# for details.
73+
74+
# Flag enabling secure logging for tasks that handle sensitive data
75+
# (default true). Set to false for debugging.
76+
timesync_secure_logging: true
77+
78+
# When using chrony, remove the key file if no NTP servers have a key
79+
# configured (default false). If false, an existing key file is left in
80+
# place when keys are removed from the configuration.
81+
timesync_remove_keyfile_if_no_keys: false
6282

6383
# List of PTP domains
6484
timesync_ptp_domains:

defaults/main.yml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,3 +11,5 @@ timesync_transactional_update_reboot_ok: null
1111
# options are all, IPv4, IPv6
1212
# default is none which is platform default
1313
timesync_ntp_ip_family: ""
14+
timesync_secure_logging: true
15+
timesync_remove_keyfile_if_no_keys: false

tasks/main.yml

Lines changed: 69 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -153,37 +153,86 @@
153153
timesync_mode2_hwts: "{{ timesync_phc_ctl_output.rc == 0 }}"
154154
when: timesync_mode == 2
155155

156-
- name: Generate chrony.conf file
157-
template:
158-
src: chrony.conf.j2
159-
dest: "{{ timesync_chrony_conf_path }}"
160-
backup: true
161-
mode: "0644"
162-
# wokeignore:rule=master
163-
notify: Restart {{ 'chronyd' if timesync_mode == 1 else 'timemaster' }}
156+
- name: Manage config and key files
164157
when:
165158
- timesync_mode != 2
166159
- timesync_ntp_provider == 'chrony'
167160
- "'chrony' in ansible_facts.packages"
168-
169-
- name: Generate chronyd sysconfig file
170-
template:
171-
src: chronyd.sysconfig.j2
172-
dest: "{{ timesync_chrony_sysconfig_path }}"
173-
backup: true
174-
mode: "0644"
175-
notify: Restart chronyd
176-
when:
177-
- timesync_mode == 1
178-
- timesync_ntp_provider == 'chrony'
179-
- "'chrony' in ansible_facts.packages"
161+
block:
162+
- name: Build list of unique NTP keys
163+
set_fact:
164+
__timesync_ntp_unique_keys: "{{ timesync_ntp_servers |
165+
selectattr('key', 'defined') |
166+
selectattr('key') |
167+
map(attribute='key') | unique | list }}"
168+
no_log: "{{ timesync_secure_logging }}"
169+
170+
- name: Build NTP key ID mapping
171+
set_fact:
172+
__timesync_ntp_key_ids: "{{ __timesync_ntp_key_ids | default({}) |
173+
combine({item: 10 + (idx * 10)}) }}"
174+
loop: "{{ __timesync_ntp_unique_keys }}"
175+
loop_control:
176+
index_var: idx
177+
no_log: "{{ timesync_secure_logging }}"
178+
when: __timesync_ntp_unique_keys | length > 0
179+
180+
- name: Generate chrony key file
181+
template:
182+
src: chrony.keys.j2
183+
dest: "{{ timesync_chrony_keyfile_path }}"
184+
backup: true
185+
owner: root
186+
group: chrony
187+
mode: "0640"
188+
no_log: "{{ timesync_secure_logging }}"
189+
# wokeignore:rule=master
190+
notify: Restart {{ 'chronyd' if timesync_mode == 1 else 'timemaster' }}
191+
when: __timesync_ntp_unique_keys | default([]) | length > 0
192+
193+
- name: Remove chrony key file if no keys are configured and flag is set
194+
file:
195+
path: "{{ timesync_chrony_keyfile_path }}"
196+
state: absent
197+
# wokeignore:rule=master
198+
notify: Restart {{ 'chronyd' if timesync_mode == 1 else 'timemaster' }}
199+
when:
200+
- __timesync_ntp_unique_keys | default([]) | length == 0
201+
- timesync_remove_keyfile_if_no_keys | bool
202+
203+
- name: Generate chrony.conf file
204+
template:
205+
src: chrony.conf.j2
206+
dest: "{{ timesync_chrony_conf_path }}"
207+
backup: true
208+
mode: "0644"
209+
no_log: "{{ timesync_secure_logging }}"
210+
# wokeignore:rule=master
211+
notify: Restart {{ 'chronyd' if timesync_mode == 1 else 'timemaster' }}
212+
213+
- name: Generate chronyd sysconfig file
214+
template:
215+
src: chronyd.sysconfig.j2
216+
dest: "{{ timesync_chrony_sysconfig_path }}"
217+
backup: true
218+
mode: "0644"
219+
notify: Restart chronyd
220+
when: timesync_mode == 1
221+
222+
always:
223+
# ensure that we don't leak these to the global scope
224+
- name: Reset key variables
225+
set_fact:
226+
__timesync_ntp_key_ids: {}
227+
__timesync_ntp_unique_keys: []
180228

181229
- name: Generate ntp.conf file
182230
template:
183231
src: ntp.conf.j2
184232
dest: /etc/ntp.conf
185233
backup: true
186234
mode: "0644"
235+
no_log: "{{ timesync_secure_logging }}"
187236
# wokeignore:rule=master
188237
notify: Restart {{ 'ntpd' if timesync_mode == 1 else 'timemaster' }}
189238
when:

templates/chrony.conf.j2

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@ value['hostname'] }}{{
1010
' iburst' if 'iburst' in value and value['iburst'] else '' }}{{
1111
' prefer' if 'prefer' in value and value['prefer'] else '' }}{{
1212
' trust' if 'trust' in value and value['trust'] else '' }}{{
13+
' key {0}'.format(__timesync_ntp_key_ids[value['key']])
14+
if 'key' in value and value['key'] else '' }}{{
1315
' xleave' if __timesync_chrony_version is version('3.0', '>=') and
1416
'xleave' in value and value['xleave'] else '' }}{{
1517
' filter {0}'.format(value['filter'])
@@ -59,10 +61,9 @@ bindcmdaddress 127.0.0.1
5961
bindcmdaddress ::1
6062

6163
{% endif %}
62-
{% if __timesync_chrony_version is version('2.2', '<') %}
6364
# Specify file containing keys for NTP and command authentication.
64-
keyfile /etc/chrony.keys
65-
65+
keyfile {{ timesync_chrony_keyfile_path }}
66+
{% if __timesync_chrony_version is version('2.2', '<') %}
6667
# Specify ID of command key.
6768
commandkey 1
6869

templates/chrony.keys.j2

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
{{ ansible_managed | comment }}
2+
{{ "system_role:timesync" | comment(prefix="", postfix="") }}
3+
{% for key in __timesync_ntp_unique_keys %}
4+
{{ __timesync_ntp_key_ids[key] }} {{ key }}
5+
{% endfor %}

tests/tasks/cleanup.yml

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,3 +5,12 @@
55
timesync_ntp_provider: "{{ timesync_ntp_provider_os_default }}"
66
timesync_ptp_domains: []
77
timesync_ntp_servers: []
8+
9+
- name: Reset settings and provider to os default for other provider
10+
include_tasks: tasks/run_role_with_clear_facts.yml
11+
vars:
12+
other_provider: "{{ 'ntp' if timesync_ntp_provider_os_default == 'chrony' else 'chrony' }}"
13+
timesync_ntp_provider: "{{ other_provider }}"
14+
timesync_ptp_domains: []
15+
timesync_ntp_servers: []
16+
when: other_provider in ansible_facts.packages

tests/tests_keys.yml

Lines changed: 165 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,165 @@
1+
# Assisted-by: Cursor using models Cursor Grok 4.5, Composer 2.5, Sonnet 5.
2+
---
3+
- name: Test NTP server authentication keys
4+
hosts: all
5+
vars:
6+
timesync_secure_logging: false
7+
timesync_ntp_provider: chrony
8+
timesync_ntp_servers:
9+
- hostname: 172.16.124.1
10+
key: "{{ __timesync_test_key_simple }}"
11+
- hostname: 172.16.124.2
12+
key: "{{ __timesync_test_key_simple }}"
13+
- hostname: 172.16.124.3
14+
key: "{{ __timesync_test_key_md5 }}"
15+
- hostname: 172.16.124.4
16+
key: "{{ __timesync_test_key_md5 }}"
17+
- hostname: 172.16.124.5
18+
key: "{{ __timesync_test_key_sha1 }}"
19+
- hostname: 172.16.124.6
20+
key: "{{ __timesync_test_key_sha1 }}"
21+
22+
tasks:
23+
- name: Run key tests
24+
tags: tests::verify
25+
block:
26+
- name: Run the role
27+
include_tasks: tasks/run_role_with_clear_facts.yml
28+
vars:
29+
__sr_public: true
30+
31+
- name: Flush handlers
32+
meta: flush_handlers
33+
34+
- name: Fetch chrony.conf file
35+
slurp:
36+
src: "{{ timesync_chrony_conf_path }}"
37+
register: __keys_chrony_conf_encoded
38+
39+
- name: Decode chrony.conf file
40+
set_fact:
41+
__keys_chrony_conf: "{{ __keys_chrony_conf_encoded.content | b64decode }}"
42+
43+
- name: Fetch chrony key file
44+
slurp:
45+
src: "{{ timesync_chrony_keyfile_path }}"
46+
register: __keys_chrony_keys_encoded
47+
48+
- name: Decode chrony key file
49+
set_fact:
50+
__keys_chrony_keys: "{{ __keys_chrony_keys_encoded.content | b64decode }}"
51+
52+
- name: Check chrony.conf key settings on ansible 2.10 and later
53+
assert:
54+
that:
55+
- __keys_chrony_conf is search('keyfile ' ~ timesync_chrony_keyfile_path)
56+
- __keys_chrony_conf is search('172\.16\.124\.1.* key 10')
57+
- __keys_chrony_conf is search('172\.16\.124\.2.* key 10')
58+
- __keys_chrony_conf is search('172\.16\.124\.3.* key 20')
59+
- __keys_chrony_conf is search('172\.16\.124\.4.* key 20')
60+
- __keys_chrony_conf is search('172\.16\.124\.5.* key 30')
61+
- __keys_chrony_conf is search('172\.16\.124\.6.* key 30')
62+
when: ansible_version.full is version('2.10', '>=')
63+
64+
# the unique filter does not preserve order on ansible 2.9
65+
- name: Check chrony.conf key settings on ansible 2.9
66+
assert:
67+
that:
68+
- __keys_chrony_conf is search('keyfile ' ~ timesync_chrony_keyfile_path)
69+
- __keys_chrony_conf is search('172\.16\.124\.1.* key [1-3]0')
70+
- __keys_chrony_conf is search('172\.16\.124\.2.* key [1-3]0')
71+
- __keys_chrony_conf is search('172\.16\.124\.3.* key [1-3]0')
72+
- __keys_chrony_conf is search('172\.16\.124\.4.* key [1-3]0')
73+
- __keys_chrony_conf is search('172\.16\.124\.5.* key [1-3]0')
74+
- __keys_chrony_conf is search('172\.16\.124\.6.* key [1-3]0')
75+
when: ansible_version.full is version('2.10', '<')
76+
77+
- name: Check chrony key file contents on ansible 2.10 and later
78+
assert:
79+
that:
80+
- __keys_chrony_keys is search('10 timesync-test-simple-key-32chars')
81+
- __keys_chrony_keys is search('20 MD5 ASCII:abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789')
82+
- __keys_chrony_keys is search('30 SHA1 HEX:933F62BE1D604E68A81B557F18CFA200483F5B70')
83+
when: ansible_version.full is version('2.10', '>=')
84+
85+
- name: Check chrony key file contents on ansible 2.9
86+
assert:
87+
that:
88+
- __keys_chrony_keys is search('[1-3]0 timesync-test-simple-key-32chars')
89+
- __keys_chrony_keys is search('[1-3]0 MD5 ASCII:abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789')
90+
- __keys_chrony_keys is search('[1-3]0 SHA1 HEX:933F62BE1D604E68A81B557F18CFA200483F5B70')
91+
when: ansible_version.full is version('2.10', '<')
92+
93+
- name: Check chrony key file permissions
94+
stat:
95+
path: "{{ timesync_chrony_keyfile_path }}"
96+
register: __keys_chrony_keys_stat
97+
98+
- name: Verify chrony key file permissions
99+
assert:
100+
that:
101+
- __keys_chrony_keys_stat.stat.pw_name == 'root'
102+
- __keys_chrony_keys_stat.stat.gr_name == 'chrony'
103+
- __keys_chrony_keys_stat.stat.mode == '0640'
104+
105+
- name: Check chrony conf for ansible_managed, fingerprint
106+
include_tasks: tasks/check_header.yml
107+
vars:
108+
__file_content: "{{ __keys_chrony_conf_encoded }}"
109+
__fingerprint: "system_role:timesync"
110+
111+
- name: Grab the journal for chronyd
112+
command: journalctl -u chronyd
113+
register: __keys_chrony_journal
114+
changed_when: false
115+
116+
- name: Check for chrony entries in the journal
117+
assert:
118+
that:
119+
- __keys_chrony_journal.stdout is not search("Key .* is too short")
120+
- __keys_chrony_journal.stdout is not search("Key .* is missing")
121+
122+
- name: Test timesync_remove_keyfile_if_no_keys
123+
block:
124+
- name: Run the role without keys and default remove keyfile setting
125+
include_tasks: tasks/run_role_with_clear_facts.yml
126+
vars:
127+
__sr_public: true
128+
timesync_ntp_provider: chrony
129+
timesync_ntp_servers:
130+
- hostname: 172.16.124.1
131+
132+
- name: Check chrony key file is retained by default
133+
stat:
134+
path: "{{ timesync_chrony_keyfile_path }}"
135+
register: __keys_chrony_keys_retained_stat
136+
137+
- name: Verify chrony key file is retained by default
138+
assert:
139+
that: __keys_chrony_keys_retained_stat.stat.exists
140+
141+
- name: Run the role without keys and remove keyfile enabled
142+
include_tasks: tasks/run_role_with_clear_facts.yml
143+
vars:
144+
__sr_public: true
145+
timesync_ntp_provider: chrony
146+
timesync_ntp_servers:
147+
- hostname: 172.16.124.1
148+
timesync_remove_keyfile_if_no_keys: true
149+
150+
- name: Flush handlers
151+
meta: flush_handlers
152+
153+
- name: Check chrony key file was removed
154+
stat:
155+
path: "{{ timesync_chrony_keyfile_path }}"
156+
register: __keys_chrony_keys_removed_stat
157+
158+
- name: Verify chrony key file was removed
159+
assert:
160+
that: not __keys_chrony_keys_removed_stat.stat.exists
161+
162+
always:
163+
- name: Cleanup after tests
164+
tags: tests::cleanup
165+
include_tasks: tasks/cleanup.yml

tests/vars/vault-variables.yml

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
__timesync_test_key_simple: !vault |
2+
$ANSIBLE_VAULT;1.1;AES256
3+
64653964313363666635646539343164366230626136656261346132343935316638623536323833
4+
6535376635313739613163333165303831623133613139650a623164613131326339663538633633
5+
36383335313131393833323133393837323531616162613536323534356264343163346639613563
6+
6437616163373864300a616361633637633262653434316232343136306538623731353134633965
7+
35663764363931616139646566663935376636333966393033633633343362313661663937326362
8+
3935363066363061326366626466313235343639376362643337
9+
__timesync_test_key_md5: !vault |
10+
$ANSIBLE_VAULT;1.1;AES256
11+
35363731373462323736636462323839633235303332363233396661343231383561616438636439
12+
6534313136303464656364646638383561353335656363360a396232623038383930316665343362
13+
39633333613037666363313363343135666636636335653664613934303732666563353765356636
14+
6465333364653361620a383932353463343934666433343263666137323838363137343532333061
15+
35616536396532333964656363613162393364363464623836346633663738313134383737343161
16+
37356533306638633731666366383863653330616339373665313237313534396435633432313935
17+
62326439663332316136323333323331383763326162323564643931623637626234343061333435
18+
61336239663935623337
19+
__timesync_test_key_sha1: !vault |
20+
$ANSIBLE_VAULT;1.1;AES256
21+
62396534633266326532633730633533653439623263336662323262646436623035353832613663
22+
6231633366316532336230393838383738383935396433330a646333386634323430326463626533
23+
35663666646436643438623432663766383834616136333031653733636263636262643563303831
24+
3265616435336336660a656562353264636162373361306563383231373065383264663136653431
25+
63333061633331356334313463383131653464326239653639373032623866343935636261323435
26+
61643234343062646331343661613130393061616330346136376530323535373961353461663461
27+
376435333838633236396333386361366265

tests/vault_pwd

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
password

vars/Debian.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ timesync_chrony_dhcp_sourcedir: "/run/chrony-dhcp"
44
timesync_chrony_sysconfig_path: "/etc/default/chrony"
55
timesync_chrony_sysconfig_options: ""
66
timesync_chrony_conf_path: "/etc/chrony/chrony.conf"
7+
timesync_chrony_keyfile_path: /etc/chrony/chrony.keys
78
timesync_ntp_sysconfig_path: "/etc/default/ntp"
89
timesync_ptp4l_sysconfig_path: ""
910
timesync_phc2sys_sysconfig_path: ""

0 commit comments

Comments
 (0)