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