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 mount = mkSetuidRoot "${lib.getBin pkgs.util-linux}/bin/mount";
270 umount = mkSetuidRoot "${lib.getBin pkgs.util-linux}/bin/umount";
271 };
272
273 # Make sure our wrapperDir exports to the PATH env variable when
274 # initializing the shell
275 environment.extraInit = ''
276 # Wrappers override other bin directories.
277 export PATH="${wrapperDir}:$PATH"
278 '';
279
280 security.apparmor.includes = lib.mapAttrs' (
281 wrapName: wrap:
282 lib.nameValuePair "nixos/security.wrappers/${wrapName}" ''
283 include "${
284 pkgs.apparmorRulesFromClosure { name = "security.wrappers.${wrapName}"; } [
285 (securityWrapper wrap.source)
286 ]
287 }"
288 mrpx ${wrap.source},
289 ''
290 ) wrappers;
291
292 systemd.mounts = [
293 {
294 where = parentWrapperDir;
295 what = "tmpfs";
296 type = "tmpfs";
297 options = lib.concatStringsSep "," ([
298 "nodev"
299 "mode=755"
300 "size=${config.security.wrapperDirSize}"
301 ]);
302 }
303 ];
304
305 systemd.services.suid-sgid-wrappers = {
306 description = "Create SUID/SGID Wrappers";
307 wantedBy = [ "sysinit.target" ];
308 before = [
309 "sysinit.target"
310 "shutdown.target"
311 ];
312 conflicts = [ "shutdown.target" ];
313 after = [ "systemd-sysusers.service" ];
314 unitConfig.DefaultDependencies = false;
315 unitConfig.RequiresMountsFor = [
316 "/nix/store"
317 "/run/wrappers"
318 ];
319 serviceConfig.RestrictSUIDSGID = false;
320 serviceConfig.Type = "oneshot";
321 script = ''
322 chmod 755 "${parentWrapperDir}"
323
324 # We want to place the tmpdirs for the wrappers to the parent dir.
325 wrapperDir=$(mktemp --directory --tmpdir="${parentWrapperDir}" wrappers.XXXXXXXXXX)
326 chmod a+rx "$wrapperDir"
327
328 ${lib.concatStringsSep "\n" mkWrappedPrograms}
329
330 if [ -L ${wrapperDir} ]; then
331 # Atomically replace the symlink
332 # See https://axialcorps.com/2013/07/03/atomically-replacing-files-and-directories/
333 old=$(readlink -f ${wrapperDir})
334 if [ -e "${wrapperDir}-tmp" ]; then
335 rm --force --recursive "${wrapperDir}-tmp"
336 fi
337 ln --symbolic --force --no-dereference "$wrapperDir" "${wrapperDir}-tmp"
338 mv --no-target-directory "${wrapperDir}-tmp" "${wrapperDir}"
339 rm --force --recursive "$old"
340 else
341 # For initial setup
342 ln --symbolic "$wrapperDir" "${wrapperDir}"
343 fi
344 '';
345 };
346
347 ###### wrappers consistency checks
348 system.checks = lib.singleton (
349 pkgs.runCommand "ensure-all-wrappers-paths-exist"
350 {
351 preferLocalBuild = true;
352 }
353 ''
354 # make sure we produce output
355 mkdir -p $out
356
357 echo -n "Checking that Nix store paths of all wrapped programs exist... "
358
359 declare -A wrappers
360 ${lib.concatStringsSep "\n" (lib.mapAttrsToList (n: v: "wrappers['${n}']='${v.source}'") wrappers)}
361
362 for name in "''${!wrappers[@]}"; do
363 path="''${wrappers[$name]}"
364 if [[ "$path" =~ /nix/store ]] && [ ! -e "$path" ]; then
365 test -t 1 && echo -ne '\033[1;31m'
366 echo "FAIL"
367 echo "The path $path does not exist!"
368 echo 'Please, check the value of `security.wrappers."'$name'".source`.'
369 test -t 1 && echo -ne '\033[0m'
370 exit 1
371 fi
372 done
373
374 echo "OK"
375 ''
376 );
377 };
378}