python312Packages.yoda: 2.0.1 -> 2.0.2
[NixPkgs.git] / nixos / modules / security / wrappers / default.nix
blob3bfe921673ed00429a23342ec60f1a0ce258c149
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.enableWrappers = lib.mkEnableOption "SUID/SGID wrappers" // {
169       default = true;
170     };
172     security.wrappers = lib.mkOption {
173       type = lib.types.attrsOf wrapperType;
174       default = {};
175       example = lib.literalExpression
176         ''
177           {
178             # a setuid root program
179             doas =
180               { setuid = true;
181                 owner = "root";
182                 group = "root";
183                 source = "''${pkgs.doas}/bin/doas";
184               };
186             # a setgid program
187             locate =
188               { setgid = true;
189                 owner = "root";
190                 group = "mlocate";
191                 source = "''${pkgs.locate}/bin/locate";
192               };
194             # a program with the CAP_NET_RAW capability
195             ping =
196               { owner = "root";
197                 group = "root";
198                 capabilities = "cap_net_raw+ep";
199                 source = "''${pkgs.iputils.out}/bin/ping";
200               };
201           }
202         '';
203       description = ''
204         This option effectively allows adding setuid/setgid bits, capabilities,
205         changing file ownership and permissions of a program without directly
206         modifying it. This works by creating a wrapper program under the
207         {option}`security.wrapperDir` directory, which is then added to
208         the shell `PATH`.
209       '';
210     };
212     security.wrapperDirSize = lib.mkOption {
213       default = "50%";
214       example = "10G";
215       type = lib.types.str;
216       description = ''
217         Size limit for the /run/wrappers tmpfs. Look at mount(8), tmpfs size option,
218         for the accepted syntax. WARNING: don't set to less than 64MB.
219       '';
220     };
222     security.wrapperDir = lib.mkOption {
223       type        = lib.types.path;
224       default     = "/run/wrappers/bin";
225       internal    = true;
226       description = ''
227         This option defines the path to the wrapper programs. It
228         should not be overridden.
229       '';
230     };
231   };
233   ###### implementation
234   config = lib.mkIf config.security.enableWrappers {
236     assertions = lib.mapAttrsToList
237       (name: opts:
238         { assertion = opts.setuid || opts.setgid -> opts.capabilities == "";
239           message = ''
240             The security.wrappers.${name} wrapper is not valid:
241                 setuid/setgid and capabilities are mutually exclusive.
242           '';
243         }
244       ) wrappers;
246     security.wrappers =
247       let
248         mkSetuidRoot = source:
249           { setuid = true;
250             owner = "root";
251             group = "root";
252             inherit source;
253           };
254       in
255       { # These are mount related wrappers that require the +s permission.
256         fusermount  = mkSetuidRoot "${lib.getBin pkgs.fuse}/bin/fusermount";
257         fusermount3 = mkSetuidRoot "${lib.getBin pkgs.fuse3}/bin/fusermount3";
258         mount  = mkSetuidRoot "${lib.getBin pkgs.util-linux}/bin/mount";
259         umount = mkSetuidRoot "${lib.getBin pkgs.util-linux}/bin/umount";
260       };
262     # Make sure our wrapperDir exports to the PATH env variable when
263     # initializing the shell
264     environment.extraInit = ''
265       # Wrappers override other bin directories.
266       export PATH="${wrapperDir}:$PATH"
267     '';
269     security.apparmor.includes = lib.mapAttrs' (wrapName: wrap: lib.nameValuePair
270      "nixos/security.wrappers/${wrapName}" ''
271       include "${pkgs.apparmorRulesFromClosure { name="security.wrappers.${wrapName}"; } [
272         (securityWrapper wrap.source)
273       ]}"
274       mrpx ${wrap.source},
275     '') wrappers;
277     systemd.mounts = [{
278       where = parentWrapperDir;
279       what = "tmpfs";
280       type = "tmpfs";
281       options = lib.concatStringsSep "," ([
282         "nodev"
283         "mode=755"
284         "size=${config.security.wrapperDirSize}"
285       ]);
286     }];
288     systemd.services.suid-sgid-wrappers = {
289       description = "Create SUID/SGID Wrappers";
290       wantedBy = [ "sysinit.target" ];
291       before = [ "sysinit.target" "shutdown.target" ];
292       conflicts = [ "shutdown.target" ];
293       after = [ "systemd-sysusers.service" ];
294       unitConfig.DefaultDependencies = false;
295       unitConfig.RequiresMountsFor = [ "/nix/store" "/run/wrappers" ];
296       serviceConfig.Type = "oneshot";
297       script = ''
298         chmod 755 "${parentWrapperDir}"
300         # We want to place the tmpdirs for the wrappers to the parent dir.
301         wrapperDir=$(mktemp --directory --tmpdir="${parentWrapperDir}" wrappers.XXXXXXXXXX)
302         chmod a+rx "$wrapperDir"
304         ${lib.concatStringsSep "\n" mkWrappedPrograms}
306         if [ -L ${wrapperDir} ]; then
307           # Atomically replace the symlink
308           # See https://axialcorps.com/2013/07/03/atomically-replacing-files-and-directories/
309           old=$(readlink -f ${wrapperDir})
310           if [ -e "${wrapperDir}-tmp" ]; then
311             rm --force --recursive "${wrapperDir}-tmp"
312           fi
313           ln --symbolic --force --no-dereference "$wrapperDir" "${wrapperDir}-tmp"
314           mv --no-target-directory "${wrapperDir}-tmp" "${wrapperDir}"
315           rm --force --recursive "$old"
316         else
317           # For initial setup
318           ln --symbolic "$wrapperDir" "${wrapperDir}"
319         fi
320       '';
321     };
323     ###### wrappers consistency checks
324     system.checks = lib.singleton (pkgs.runCommandLocal
325       "ensure-all-wrappers-paths-exist" { }
326       ''
327         # make sure we produce output
328         mkdir -p $out
330         echo -n "Checking that Nix store paths of all wrapped programs exist... "
332         declare -A wrappers
333         ${lib.concatStringsSep "\n" (lib.mapAttrsToList (n: v:
334           "wrappers['${n}']='${v.source}'") wrappers)}
336         for name in "''${!wrappers[@]}"; do
337           path="''${wrappers[$name]}"
338           if [[ "$path" =~ /nix/store ]] && [ ! -e "$path" ]; then
339             test -t 1 && echo -ne '\033[1;31m'
340             echo "FAIL"
341             echo "The path $path does not exist!"
342             echo 'Please, check the value of `security.wrappers."'$name'".source`.'
343             test -t 1 && echo -ne '\033[0m'
344             exit 1
345           fi
346         done
348         echo "OK"
349       '');
350   };