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