1{
2 config,
3 lib,
4 pkgs,
5 ...
6}:
7
8with lib;
9
10let
11 cfg = config.services.chrony;
12 chronyPkg = cfg.package;
13
14 stateDir = cfg.directory;
15 driftFile = "${stateDir}/chrony.drift";
16 keyFile = "${stateDir}/chrony.keys";
17 rtcFile = "${stateDir}/chrony.rtc";
18
19 configFile = pkgs.writeText "chrony.conf" ''
20 ${concatMapStringsSep "\n" (
21 server: "server " + server + " " + cfg.serverOption + optionalString (cfg.enableNTS) " nts"
22 ) cfg.servers}
23
24 ${optionalString (
25 cfg.initstepslew.enabled && (cfg.servers != [ ])
26 ) "initstepslew ${toString cfg.initstepslew.threshold} ${concatStringsSep " " cfg.servers}"}
27
28 driftfile ${driftFile}
29 keyfile ${keyFile}
30 ${optionalString (cfg.enableRTCTrimming) "rtcfile ${rtcFile}"}
31 ${optionalString (cfg.enableNTS) "ntsdumpdir ${stateDir}"}
32
33 ${optionalString (cfg.enableRTCTrimming) "rtcautotrim ${builtins.toString cfg.autotrimThreshold}"}
34 ${optionalString (!config.time.hardwareClockInLocalTime) "rtconutc"}
35
36 ${cfg.extraConfig}
37 '';
38
39 chronyFlags = [
40 "-n"
41 "-u"
42 "chrony"
43 "-f"
44 "${configFile}"
45 ]
46 ++ optional cfg.enableMemoryLocking "-m"
47 ++ cfg.extraFlags;
48in
49{
50 options = {
51 services.chrony = {
52 enable = mkOption {
53 type = types.bool;
54 default = false;
55 description = ''
56 Whether to synchronise your machine's time using chrony.
57 Make sure you disable NTP if you enable this service.
58 '';
59 };
60
61 package = mkPackageOption pkgs "chrony" { };
62
63 servers = mkOption {
64 default = config.networking.timeServers;
65 defaultText = literalExpression "config.networking.timeServers";
66 type = types.listOf types.str;
67 description = ''
68 The set of NTP servers from which to synchronise.
69 '';
70 };
71
72 serverOption = mkOption {
73 default = "iburst";
74 type = types.enum [
75 "iburst"
76 "offline"
77 ];
78 description = ''
79 Set option for server directives.
80
81 Use "iburst" to rapidly poll on startup. Recommended if your machine
82 is consistently online.
83
84 Use "offline" to prevent polling on startup. Recommended if your
85 machine boots offline or is otherwise frequently offline.
86 '';
87 };
88
89 enableMemoryLocking = mkOption {
90 type = types.bool;
91 default =
92 config.environment.memoryAllocator.provider != "graphene-hardened"
93 && config.environment.memoryAllocator.provider != "graphene-hardened-light";
94 defaultText = lib.literalExpression ''config.environment.memoryAllocator.provider != "graphene-hardened" && config.environment.memoryAllocator.provider != "graphene-hardened-light"'';
95 description = ''
96 Whether to add the `-m` flag to lock memory.
97 '';
98 };
99
100 enableRTCTrimming = mkOption {
101 type = types.bool;
102 default = true;
103 description = ''
104 Enable tracking of the RTC offset to the system clock and automatic trimming.
105 See also [](#opt-services.chrony.autotrimThreshold)
106
107 ::: {.note}
108 This is not compatible with the `rtcsync` directive, which naively syncs the RTC time every 11 minutes.
109
110 Tracking the RTC drift will allow more precise timekeeping,
111 especially on intermittently running devices, where the RTC is very relevant.
112 :::
113 '';
114 };
115
116 autotrimThreshold = mkOption {
117 type = types.ints.positive;
118 default = 30;
119 example = 10;
120 description = ''
121 Maximum estimated error threshold for the `rtcautotrim` command.
122 When reached, the RTC will be trimmed.
123 Only used when [](#opt-services.chrony.enableRTCTrimming) is enabled.
124 '';
125 };
126
127 enableNTS = mkOption {
128 type = types.bool;
129 default = false;
130 description = ''
131 Whether to enable Network Time Security authentication.
132 Make sure it is supported by your selected NTP server(s).
133 '';
134 };
135
136 initstepslew = {
137 enabled = mkOption {
138 type = types.bool;
139 default = true;
140 description = ''
141 Allow chronyd to make a rapid measurement of the system clock error
142 at boot time, and to correct the system clock by stepping before
143 normal operation begins.
144 '';
145 };
146
147 threshold = mkOption {
148 type = types.either types.float types.int;
149 default = 1000; # by default, same threshold as 'ntpd -g' (1000s)
150 description = ''
151 The threshold of system clock error (in seconds) above which the
152 clock will be stepped. If the correction required is less than the
153 threshold, a slew is used instead.
154 '';
155 };
156 };
157
158 directory = mkOption {
159 type = types.str;
160 default = "/var/lib/chrony";
161 description = "Directory where chrony state is stored.";
162 };
163
164 extraConfig = mkOption {
165 type = types.lines;
166 default = "";
167 description = ''
168 Extra configuration directives that should be added to
169 `chrony.conf`
170 '';
171 };
172
173 extraFlags = mkOption {
174 default = [ ];
175 example = [ "-s" ];
176 type = types.listOf types.str;
177 description = "Extra flags passed to the chronyd command.";
178 };
179 };
180 };
181
182 meta.maintainers = with lib.maintainers; [
183 thoughtpolice
184 vifino
185 ];
186
187 config = mkIf cfg.enable {
188 environment.systemPackages = [ chronyPkg ];
189
190 users.groups.chrony.gid = config.ids.gids.chrony;
191
192 users.users.chrony = {
193 uid = config.ids.uids.chrony;
194 group = "chrony";
195 description = "chrony daemon user";
196 home = stateDir;
197 };
198
199 services.timesyncd.enable = mkForce false;
200
201 # If chrony controls and tracks the RTC, writing it externally causes clock error.
202 systemd.services.save-hwclock = lib.mkIf cfg.enableRTCTrimming {
203 enable = lib.mkForce false;
204 };
205
206 systemd.services.systemd-timedated.environment = {
207 SYSTEMD_TIMEDATED_NTP_SERVICES = "chronyd.service";
208 };
209
210 systemd.tmpfiles.rules = [
211 "d ${stateDir} 0750 chrony chrony - -"
212 "f ${driftFile} 0640 chrony chrony - -"
213 "f ${keyFile} 0640 chrony chrony - -"
214 ]
215 ++ lib.optionals cfg.enableRTCTrimming [
216 "f ${rtcFile} 0640 chrony chrony - -"
217 ];
218
219 systemd.services.chronyd = {
220 description = "chrony NTP daemon";
221
222 wantedBy = [ "multi-user.target" ];
223 wants = [ "time-sync.target" ];
224 before = [ "time-sync.target" ];
225 after = [
226 "network.target"
227 "nss-lookup.target"
228 ];
229 conflicts = [
230 "ntpd.service"
231 "systemd-timesyncd.service"
232 ];
233
234 path = [ chronyPkg ];
235
236 unitConfig.ConditionCapability = "CAP_SYS_TIME";
237 serviceConfig = {
238 Type = "notify";
239 ExecStart = "${chronyPkg}/bin/chronyd ${builtins.toString chronyFlags}";
240
241 # Proc filesystem
242 ProcSubset = "pid";
243 ProtectProc = "invisible";
244 # Access write directories
245 ReadWritePaths = [ "${stateDir}" ];
246 UMask = "0027";
247 # Capabilities
248 CapabilityBoundingSet = [
249 "CAP_CHOWN"
250 "CAP_DAC_OVERRIDE"
251 "CAP_NET_BIND_SERVICE"
252 "CAP_SETGID"
253 "CAP_SETUID"
254 "CAP_SYS_RESOURCE"
255 "CAP_SYS_TIME"
256 ];
257 # Device Access
258 DeviceAllow = [
259 "char-pps rw"
260 "char-ptp rw"
261 "char-rtc rw"
262 ];
263 DevicePolicy = "closed";
264 # Security
265 NoNewPrivileges = true;
266 # Sandboxing
267 ProtectSystem = "full";
268 ProtectHome = true;
269 PrivateTmp = true;
270 PrivateDevices = false;
271 PrivateUsers = false;
272 ProtectHostname = true;
273 ProtectClock = false;
274 ProtectKernelTunables = true;
275 ProtectKernelModules = true;
276 ProtectKernelLogs = true;
277 ProtectControlGroups = true;
278 RestrictAddressFamilies = [
279 "AF_UNIX"
280 "AF_INET"
281 "AF_INET6"
282 ];
283 RestrictNamespaces = true;
284 LockPersonality = true;
285 MemoryDenyWriteExecute = true;
286 RestrictRealtime = true;
287 RestrictSUIDSGID = true;
288 RemoveIPC = true;
289 PrivateMounts = true;
290 # System Call Filtering
291 SystemCallArchitectures = "native";
292 SystemCallFilter = [
293 "~@cpu-emulation @debug @keyring @mount @obsolete @privileged @resources"
294 "@clock"
295 "@setuid"
296 "capset"
297 "@chown"
298 ];
299 };
300 };
301
302 assertions = [
303 {
304 assertion =
305 !(
306 cfg.enableRTCTrimming
307 && builtins.any (line: (builtins.match "^ *rtcsync" line) != null) (
308 lib.strings.splitString "\n" cfg.extraConfig
309 )
310 );
311 message = ''
312 The chrony module now configures `rtcfile` and `rtcautotrim` for you.
313 These options conflict with `rtcsync` and cause chrony to crash.
314 Unless you are very sure the former isn't what you want, please remove
315 `rtcsync` from `services.chrony.extraConfig`.
316 Alternatively, disable this behaviour by `services.chrony.enableRTCTrimming = false;`
317 '';
318 }
319 ];
320 };
321}