1{
2 config,
3 lib,
4 pkgs,
5 utils,
6 ...
7}:
8let
9 cfg = config.services.logrotate;
10
11 generateLine =
12 n: v:
13 if
14 builtins.elem n [
15 "files"
16 "priority"
17 "enable"
18 "global"
19 ]
20 || v == null
21 then
22 null
23 else if builtins.elem n [ "frequency" ] then
24 "${v}\n"
25 else if
26 builtins.elem n [
27 "firstaction"
28 "lastaction"
29 "prerotate"
30 "postrotate"
31 "preremove"
32 ]
33 then
34 "${n}\n ${v}\n endscript\n"
35 else if lib.isInt v then
36 "${n} ${toString v}\n"
37 else if v == true then
38 "${n}\n"
39 else if v == false then
40 "no${n}\n"
41 else
42 "${n} ${v}\n";
43 generateSection =
44 indent: settings:
45 lib.concatStringsSep (lib.fixedWidthString indent " " "") (
46 lib.filter (x: x != null) (lib.mapAttrsToList generateLine settings)
47 );
48
49 # generateSection includes a final newline hence weird closing brace
50 mkConf =
51 settings:
52 if settings.global or false then
53 generateSection 0 settings
54 else
55 ''
56 ${lib.concatMapStringsSep "\n" (files: ''"${files}"'') (lib.toList settings.files)} {
57 ${generateSection 2 settings}}
58 '';
59
60 settings = lib.sortProperties (
61 lib.attrValues (
62 lib.filterAttrs (_: settings: settings.enable) (
63 lib.foldAttrs lib.recursiveUpdate { } [
64 {
65 header = {
66 enable = true;
67 missingok = true;
68 notifempty = true;
69 frequency = "weekly";
70 rotate = 4;
71 };
72 }
73 cfg.settings
74 {
75 header = {
76 global = true;
77 priority = 100;
78 };
79 }
80 ]
81 )
82 )
83 );
84 configFile = pkgs.writeTextFile {
85 name = "logrotate.conf";
86 text = lib.concatStringsSep "\n" (map mkConf settings);
87 checkPhase = lib.optionalString cfg.checkConfig ''
88 # logrotate --debug also checks that users specified in config
89 # file exist, but we only have sandboxed users here so brown these
90 # out. according to man page that means su, create and createolddir.
91 # files required to exist also won't be present, so missingok is forced.
92 user=$(${pkgs.buildPackages.coreutils}/bin/id -un)
93 group=$(${pkgs.buildPackages.coreutils}/bin/id -gn)
94 sed -e "s/\bsu\s.*/su $user $group/" \
95 -e "s/\b\(create\s\+[0-9]*\s*\|createolddir\s\+[0-9]*\s\+\).*/\1$user $group/" \
96 -e "1imissingok" -e "s/\bnomissingok\b//" \
97 $out > logrotate.conf
98 # Since this makes for very verbose builds only show real error.
99 # There is no way to control log level, but logrotate hardcodes
100 # 'error:' at common log level, so we can use grep, taking care
101 # to keep error codes
102 set -o pipefail
103 if ! ${pkgs.buildPackages.logrotate}/sbin/logrotate -s logrotate.status \
104 --debug logrotate.conf 2>&1 \
105 | ( ! grep "error:" ) > logrotate-error; then
106 echo "Logrotate configuration check failed."
107 echo "The failing configuration (after adjustments to pass tests in sandbox) was:"
108 printf "%s\n" "-------"
109 cat logrotate.conf
110 printf "%s\n" "-------"
111 echo "The error reported by logrotate was as follow:"
112 printf "%s\n" "-------"
113 cat logrotate-error
114 printf "%s\n" "-------"
115 echo "You can disable this check with services.logrotate.checkConfig = false,"
116 echo "but if you think it should work please report this failure along with"
117 echo "the config file being tested!"
118 false
119 fi
120 '';
121 };
122
123 mailOption = lib.optionalString (lib.foldr (n: a: a || (n.mail or false) != false) false (
124 lib.attrValues cfg.settings
125 )) "--mail=${pkgs.mailutils}/bin/mail";
126in
127{
128 imports = [
129 (lib.mkRemovedOptionModule [
130 "services"
131 "logrotate"
132 "config"
133 ] "Modify services.logrotate.settings.header instead")
134 (lib.mkRemovedOptionModule [
135 "services"
136 "logrotate"
137 "extraConfig"
138 ] "Modify services.logrotate.settings.header instead")
139 (lib.mkRemovedOptionModule [
140 "services"
141 "logrotate"
142 "paths"
143 ] "Add attributes to services.logrotate.settings instead")
144 ];
145
146 options = {
147 services.logrotate = {
148 enable = lib.mkEnableOption "the logrotate systemd service" // {
149 default = lib.foldr (n: a: a || n.enable) false (lib.attrValues cfg.settings);
150 defaultText = lib.literalExpression "cfg.settings != {}";
151 };
152
153 allowNetworking = lib.mkEnableOption "network access for logrotate";
154
155 settings = lib.mkOption {
156 default = { };
157 description = ''
158 logrotate freeform settings: each attribute here will define its own section,
159 ordered by {option}`services.logrotate.settings.<name>.priority`,
160 which can either define files to rotate with their settings
161 or settings common to all further files settings.
162 All attribute names not explicitly defined as sub-options here are passed through
163 as logrotate config directives,
164 refer to <https://linux.die.net/man/8/logrotate> for details.
165 '';
166 example = lib.literalExpression ''
167 {
168 # global options
169 header = {
170 dateext = true;
171 };
172 # example custom files
173 "/var/log/mylog.log" = {
174 frequency = "daily";
175 rotate = 3;
176 };
177 "multiple paths" = {
178 files = [
179 "/var/log/first*.log"
180 "/var/log/second.log"
181 ];
182 };
183 # specify custom order of sections
184 "/var/log/myservice/*.log" = {
185 # ensure lower priority
186 priority = 110;
187 postrotate = '''
188 systemctl reload myservice
189 ''';
190 };
191 };
192 '';
193 type = lib.types.attrsOf (
194 lib.types.submodule (
195 { name, ... }:
196 {
197 freeformType =
198 with lib.types;
199 attrsOf (
200 nullOr (oneOf [
201 int
202 bool
203 str
204 ])
205 );
206
207 options = {
208 enable = lib.mkEnableOption "setting individual kill switch" // {
209 default = true;
210 };
211
212 global = lib.mkOption {
213 type = lib.types.bool;
214 default = false;
215 description = ''
216 Whether this setting is a global option or not: set to have these
217 settings apply to all files settings with a higher priority.
218 '';
219 };
220 files = lib.mkOption {
221 type = with lib.types; either str (listOf str);
222 default = name;
223 defaultText = ''
224 The attrset name if not specified
225 '';
226 description = ''
227 Single or list of files for which rules are defined.
228 The files are quoted with double-quotes in logrotate configuration,
229 so globs and spaces are supported.
230 Note this setting is ignored if globals is true.
231 '';
232 };
233
234 frequency = lib.mkOption {
235 type = lib.types.nullOr lib.types.str;
236 default = null;
237 description = ''
238 How often to rotate the logs. Defaults to previously set global setting,
239 which itself defaults to weekly.
240 '';
241 };
242
243 priority = lib.mkOption {
244 type = lib.types.int;
245 default = 1000;
246 description = ''
247 Order of this logrotate block in relation to the others. The semantics are
248 the same as with `lib.mkOrder`. Smaller values are inserted first.
249 '';
250 };
251 };
252
253 }
254 )
255 );
256 };
257
258 configFile = lib.mkOption {
259 type = lib.types.path;
260 default = configFile;
261 defaultText = ''
262 A configuration file automatically generated by NixOS.
263 '';
264 description = ''
265 Override the configuration file used by logrotate. By default,
266 NixOS generates one automatically from [](#opt-services.logrotate.settings).
267 '';
268 example = lib.literalExpression ''
269 pkgs.writeText "logrotate.conf" '''
270 missingok
271 "/var/log/*.log" {
272 rotate 4
273 weekly
274 }
275 ''';
276 '';
277 };
278
279 checkConfig = lib.mkOption {
280 type = lib.types.bool;
281 default = true;
282 description = ''
283 Whether the config should be checked at build time.
284
285 Some options are not checkable at build time because of the build sandbox:
286 for example, the test does not know about existing files and system users are
287 not known.
288 These limitations mean we must adjust the file for tests (missingok is forced
289 and users are replaced by dummy users), so tests are complemented by a
290 logrotate-checkconf service that is enabled by default.
291 This extra check can be disabled by disabling it at the systemd level with the
292 {option}`systemd.services.logrotate-checkconf.enable` option.
293
294 Conversely there are still things that might make this check fail incorrectly
295 (e.g. a file path where we don't have access to intermediate directories):
296 in this case you can disable the failing check with this option.
297 '';
298 };
299
300 extraArgs = lib.mkOption {
301 type = lib.types.listOf lib.types.str;
302 default = [ ];
303 description = "Additional command line arguments to pass on logrotate invocation";
304 };
305 };
306 };
307
308 config = lib.mkIf cfg.enable {
309 systemd.services.logrotate = {
310 description = "Logrotate Service";
311 documentation = [
312 "man:logrotate(8)"
313 "man:logrotate(5)"
314 ];
315 startAt = "hourly";
316
317 serviceConfig =
318 {
319 Type = "oneshot";
320 ExecStart = "${lib.getExe pkgs.logrotate} ${utils.escapeSystemdExecArgs cfg.extraArgs} ${mailOption} ${cfg.configFile}";
321
322 # performance
323 Nice = 19;
324 IOSchedulingClass = "best-effort";
325 IOSchedulingPriority = 7;
326
327 # hardening
328 CapabilityBoundingSet = [
329 "CAP_CHOWN"
330 "CAP_DAC_OVERRIDE"
331 "CAP_FOWNER"
332 "CAP_KILL"
333 "CAP_SETUID"
334 "CAP_SETGID"
335 ];
336 DevicePolicy = "closed";
337 LockPersonality = true;
338 MemoryDenyWriteExecute = true;
339 NoNewPrivileges = true;
340 PrivateDevices = true;
341 PrivateTmp = true;
342 ProcSubset = "pid";
343 ProtectClock = true;
344 ProtectControlGroups = true;
345 ProtectHome = true;
346 ProtectHostname = true;
347 ProtectKernelLogs = true;
348 ProtectKernelModules = true;
349 ProtectKernelTunables = true;
350 ProtectProc = "invisible";
351 ProtectSystem = "full";
352 RestrictNamespaces = true;
353 RestrictRealtime = true;
354 RestrictSUIDSGID = false; # can create sgid directories
355 SystemCallArchitectures = "native";
356 SystemCallFilter = [
357 "@system-service"
358 "~@privileged @resources"
359 "@chown @setuid"
360 ];
361 UMask = "0027";
362 }
363 // lib.optionalAttrs (!cfg.allowNetworking) {
364 PrivateNetwork = true; # e.g. mail delivery
365 RestrictAddressFamilies = [ "AF_UNIX" ];
366 };
367 };
368 systemd.services.logrotate-checkconf = {
369 description = "Logrotate configuration check";
370 wantedBy = [ "multi-user.target" ];
371 serviceConfig = {
372 Type = "oneshot";
373 RemainAfterExit = true;
374 ExecStart = "${pkgs.logrotate}/sbin/logrotate ${utils.escapeSystemdExecArgs cfg.extraArgs} --debug ${cfg.configFile}";
375 };
376 };
377 };
378}