grafana-alloy: don't build the frontend twice
[NixPkgs.git] / nixos / modules / security / wrappers / default.nix
blobb5dae96d79c6bb758eb38f594ab67fcbeea96e00
1 { config, lib, pkgs, ... }:
2 let
4   inherit (config.security) wrapperDir wrappers;
6   parentWrapperDir = dirOf wrapperDir;
8   # This is security-sensitive code, and glibc vulns happen from time to time.
9   # musl is security-focused and generally more minimal, so it's a better choice here.
10   # The dynamic linker is still a fairly complex piece of code, and the wrappers are
11   # quite small, so linking it statically is more appropriate.
12   securityWrapper = sourceProg : pkgs.pkgsStatic.callPackage ./wrapper.nix {
13     inherit sourceProg;
15     # glibc definitions of insecure environment variables
16     #
17     # We extract the single header file we need into its own derivation,
18     # so that we don't have to pull full glibc sources to build wrappers.
19     #
20     # They're taken from pkgs.glibc so that we don't have to keep as close
21     # an eye on glibc changes. Not every relevant variable is in this header,
22     # so we maintain a slightly stricter list in wrapper.c itself as well.
23     unsecvars = lib.overrideDerivation (pkgs.srcOnly pkgs.glibc)
24       ({ name, ... }: {
25         name = "${name}-unsecvars";
26         installPhase = ''
27           mkdir $out
28           cp sysdeps/generic/unsecvars.h $out
29         '';
30       });
31   };
33   fileModeType =
34     let
35       # taken from the chmod(1) man page
36       symbolic = "[ugoa]*([-+=]([rwxXst]*|[ugo]))+|[-+=][0-7]+";
37       numeric = "[-+=]?[0-7]{0,4}";
38       mode = "((${symbolic})(,${symbolic})*)|(${numeric})";
39     in
40      lib.types.strMatching mode
41      // { description = "file mode string"; };
43   wrapperType = lib.types.submodule ({ name, config, ... }: {
44     options.source = lib.mkOption
45       { type = lib.types.path;
46         description = "The absolute path to the program to be wrapped.";
47       };
48     options.program = lib.mkOption
49       { type = with lib.types; nullOr str;
50         default = name;
51         description = ''
52           The name of the wrapper program. Defaults to the attribute name.
53         '';
54       };
55     options.owner = lib.mkOption
56       { type = lib.types.str;
57         description = "The owner of the wrapper program.";
58       };
59     options.group = lib.mkOption
60       { type = lib.types.str;
61         description = "The group of the wrapper program.";
62       };
63     options.permissions = lib.mkOption
64       { type = fileModeType;
65         default  = "u+rx,g+x,o+x";
66         example = "a+rx";
67         description = ''
68           The permissions of the wrapper program. The format is that of a
69           symbolic or numeric file mode understood by {command}`chmod`.
70         '';
71       };
72     options.capabilities = lib.mkOption
73       { type = lib.types.commas;
74         default = "";
75         description = ''
76           A comma-separated list of capability clauses to be given to the
77           wrapper program. The format for capability clauses is described in the
78           “TEXTUAL REPRESENTATION” section of the {manpage}`cap_from_text(3)`
79           manual page. For a list of capabilities supported by the system, check
80           the {manpage}`capabilities(7)` manual page.
82           ::: {.note}
83           `cap_setpcap`, which is required for the wrapper
84           program to be able to raise caps into the Ambient set is NOT raised
85           to the Ambient set so that the real program cannot modify its own
86           capabilities!! This may be too restrictive for cases in which the
87           real program needs cap_setpcap but it at least leans on the side
88           security paranoid vs. too relaxed.
89           :::
90         '';
91       };
92     options.setuid = lib.mkOption
93       { type = lib.types.bool;
94         default = false;
95         description = "Whether to add the setuid bit the wrapper program.";
96       };
97     options.setgid = lib.mkOption
98       { type = lib.types.bool;
99         default = false;
100         description = "Whether to add the setgid bit the wrapper program.";
101       };
102   });
104   ###### Activation script for the setcap wrappers
105   mkSetcapProgram =
106     { program
107     , capabilities
108     , source
109     , owner
110     , group
111     , permissions
112     , ...
113     }:
114     ''
115       cp ${securityWrapper source}/bin/security-wrapper "$wrapperDir/${program}"
117       # Prevent races
118       chmod 0000 "$wrapperDir/${program}"
119       chown ${owner}:${group} "$wrapperDir/${program}"
121       # Set desired capabilities on the file plus cap_setpcap so
122       # the wrapper program can elevate the capabilities set on
123       # its file into the Ambient set.
124       ${pkgs.libcap.out}/bin/setcap "cap_setpcap,${capabilities}" "$wrapperDir/${program}"
126       # Set the executable bit
127       chmod ${permissions} "$wrapperDir/${program}"
128     '';
130   ###### Activation script for the setuid wrappers
131   mkSetuidProgram =
132     { program
133     , source
134     , owner
135     , group
136     , setuid
137     , setgid
138     , permissions
139     , ...
140     }:
141     ''
142       cp ${securityWrapper source}/bin/security-wrapper "$wrapperDir/${program}"
144       # Prevent races
145       chmod 0000 "$wrapperDir/${program}"
146       chown ${owner}:${group} "$wrapperDir/${program}"
148       chmod "u${if setuid then "+" else "-"}s,g${if setgid then "+" else "-"}s,${permissions}" "$wrapperDir/${program}"
149     '';
151   mkWrappedPrograms =
152     builtins.map
153       (opts:
154         if opts.capabilities != ""
155         then mkSetcapProgram opts
156         else mkSetuidProgram opts
157       ) (lib.attrValues wrappers);
160   imports = [
161     (lib.mkRemovedOptionModule [ "security" "setuidOwners" ] "Use security.wrappers instead")
162     (lib.mkRemovedOptionModule [ "security" "setuidPrograms" ] "Use security.wrappers instead")
163   ];
165   ###### interface
167   options = {
168     security.wrappers = lib.mkOption {
169       type = lib.types.attrsOf wrapperType;
170       default = {};
171       example = lib.literalExpression
172         ''
173           {
174             # a setuid root program
175             doas =
176               { setuid = true;
177                 owner = "root";
178                 group = "root";
179                 source = "''${pkgs.doas}/bin/doas";
180               };
182             # a setgid program
183             locate =
184               { setgid = true;
185                 owner = "root";
186                 group = "mlocate";
187                 source = "''${pkgs.locate}/bin/locate";
188               };
190             # a program with the CAP_NET_RAW capability
191             ping =
192               { owner = "root";
193                 group = "root";
194                 capabilities = "cap_net_raw+ep";
195                 source = "''${pkgs.iputils.out}/bin/ping";
196               };
197           }
198         '';
199       description = ''
200         This option effectively allows adding setuid/setgid bits, capabilities,
201         changing file ownership and permissions of a program without directly
202         modifying it. This works by creating a wrapper program under the
203         {option}`security.wrapperDir` directory, which is then added to
204         the shell `PATH`.
205       '';
206     };
208     security.wrapperDirSize = lib.mkOption {
209       default = "50%";
210       example = "10G";
211       type = lib.types.str;
212       description = ''
213         Size limit for the /run/wrappers tmpfs. Look at mount(8), tmpfs size option,
214         for the accepted syntax. WARNING: don't set to less than 64MB.
215       '';
216     };
218     security.wrapperDir = lib.mkOption {
219       type        = lib.types.path;
220       default     = "/run/wrappers/bin";
221       internal    = true;
222       description = ''
223         This option defines the path to the wrapper programs. It
224         should not be overridden.
225       '';
226     };
227   };
229   ###### implementation
230   config = {
232     assertions = lib.mapAttrsToList
233       (name: opts:
234         { assertion = opts.setuid || opts.setgid -> opts.capabilities == "";
235           message = ''
236             The security.wrappers.${name} wrapper is not valid:
237                 setuid/setgid and capabilities are mutually exclusive.
238           '';
239         }
240       ) wrappers;
242     security.wrappers =
243       let
244         mkSetuidRoot = source:
245           { setuid = true;
246             owner = "root";
247             group = "root";
248             inherit source;
249           };
250       in
251       { # These are mount related wrappers that require the +s permission.
252         fusermount  = mkSetuidRoot "${pkgs.fuse}/bin/fusermount";
253         fusermount3 = mkSetuidRoot "${pkgs.fuse3}/bin/fusermount3";
254         mount  = mkSetuidRoot "${lib.getBin pkgs.util-linux}/bin/mount";
255         umount = mkSetuidRoot "${lib.getBin pkgs.util-linux}/bin/umount";
256       };
258     # Make sure our wrapperDir exports to the PATH env variable when
259     # initializing the shell
260     environment.extraInit = ''
261       # Wrappers override other bin directories.
262       export PATH="${wrapperDir}:$PATH"
263     '';
265     security.apparmor.includes = lib.mapAttrs' (wrapName: wrap: lib.nameValuePair
266      "nixos/security.wrappers/${wrapName}" ''
267       include "${pkgs.apparmorRulesFromClosure { name="security.wrappers.${wrapName}"; } [
268         (securityWrapper wrap.source)
269       ]}"
270       mrpx ${wrap.source},
271     '') wrappers;
273     systemd.mounts = [{
274       where = parentWrapperDir;
275       what = "tmpfs";
276       type = "tmpfs";
277       options = lib.concatStringsSep "," ([
278         "nodev"
279         "mode=755"
280         "size=${config.security.wrapperDirSize}"
281       ]);
282     }];
284     systemd.services.suid-sgid-wrappers = {
285       description = "Create SUID/SGID Wrappers";
286       wantedBy = [ "sysinit.target" ];
287       before = [ "sysinit.target" "shutdown.target" ];
288       conflicts = [ "shutdown.target" ];
289       after = [ "systemd-sysusers.service" ];
290       unitConfig.DefaultDependencies = false;
291       unitConfig.RequiresMountsFor = [ "/nix/store" "/run/wrappers" ];
292       serviceConfig.Type = "oneshot";
293       script = ''
294         chmod 755 "${parentWrapperDir}"
296         # We want to place the tmpdirs for the wrappers to the parent dir.
297         wrapperDir=$(mktemp --directory --tmpdir="${parentWrapperDir}" wrappers.XXXXXXXXXX)
298         chmod a+rx "$wrapperDir"
300         ${lib.concatStringsSep "\n" mkWrappedPrograms}
302         if [ -L ${wrapperDir} ]; then
303           # Atomically replace the symlink
304           # See https://axialcorps.com/2013/07/03/atomically-replacing-files-and-directories/
305           old=$(readlink -f ${wrapperDir})
306           if [ -e "${wrapperDir}-tmp" ]; then
307             rm --force --recursive "${wrapperDir}-tmp"
308           fi
309           ln --symbolic --force --no-dereference "$wrapperDir" "${wrapperDir}-tmp"
310           mv --no-target-directory "${wrapperDir}-tmp" "${wrapperDir}"
311           rm --force --recursive "$old"
312         else
313           # For initial setup
314           ln --symbolic "$wrapperDir" "${wrapperDir}"
315         fi
316       '';
317     };
319     ###### wrappers consistency checks
320     system.checks = lib.singleton (pkgs.runCommandLocal
321       "ensure-all-wrappers-paths-exist" { }
322       ''
323         # make sure we produce output
324         mkdir -p $out
326         echo -n "Checking that Nix store paths of all wrapped programs exist... "
328         declare -A wrappers
329         ${lib.concatStringsSep "\n" (lib.mapAttrsToList (n: v:
330           "wrappers['${n}']='${v.source}'") wrappers)}
332         for name in "''${!wrappers[@]}"; do
333           path="''${wrappers[$name]}"
334           if [[ "$path" =~ /nix/store ]] && [ ! -e "$path" ]; then
335             test -t 1 && echo -ne '\033[1;31m'
336             echo "FAIL"
337             echo "The path $path does not exist!"
338             echo 'Please, check the value of `security.wrappers."'$name'".source`.'
339             test -t 1 && echo -ne '\033[0m'
340             exit 1
341           fi
342         done
344         echo "OK"
345       '');
346   };