1{ config, lib, pkgs, ... }:
2let
3
4 inherit (config.security) wrapperDir wrappers;
5
6 parentWrapperDir = dirOf wrapperDir;
7
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;
14
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 };
32
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"; };
42
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.
81
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 });
103
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}"
116
117 # Prevent races
118 chmod 0000 "$wrapperDir/${program}"
119 chown ${owner}:${group} "$wrapperDir/${program}"
120
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}"
125
126 # Set the executable bit
127 chmod ${permissions} "$wrapperDir/${program}"
128 '';
129
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}"
143
144 # Prevent races
145 chmod 0000 "$wrapperDir/${program}"
146 chown ${owner}:${group} "$wrapperDir/${program}"
147
148 chmod "u${if setuid then "+" else "-"}s,g${if setgid then "+" else "-"}s,${permissions}" "$wrapperDir/${program}"
149 '';
150
151 mkWrappedPrograms =
152 builtins.map
153 (opts:
154 if opts.capabilities != ""
155 then mkSetcapProgram opts
156 else mkSetuidProgram opts
157 ) (lib.attrValues wrappers);
158in
159{
160 imports = [
161 (lib.mkRemovedOptionModule [ "security" "setuidOwners" ] "Use security.wrappers instead")
162 (lib.mkRemovedOptionModule [ "security" "setuidPrograms" ] "Use security.wrappers instead")
163 ];
164
165 ###### interface
166
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 };
181
182 # a setgid program
183 locate =
184 { setgid = true;
185 owner = "root";
186 group = "mlocate";
187 source = "''${pkgs.locate}/bin/locate";
188 };
189
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 };
207
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 };
217
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 };
228
229 ###### implementation
230 config = {
231
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;
241
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 };
257
258 boot.specialFileSystems.${parentWrapperDir} = {
259 fsType = "tmpfs";
260 options = [ "nodev" "mode=755" "size=${config.security.wrapperDirSize}" ];
261 };
262
263 # Make sure our wrapperDir exports to the PATH env variable when
264 # initializing the shell
265 environment.extraInit = ''
266 # Wrappers override other bin directories.
267 export PATH="${wrapperDir}:$PATH"
268 '';
269
270 security.apparmor.includes = lib.mapAttrs' (wrapName: wrap: lib.nameValuePair
271 "nixos/security.wrappers/${wrapName}" ''
272 include "${pkgs.apparmorRulesFromClosure { name="security.wrappers.${wrapName}"; } [
273 (securityWrapper wrap.source)
274 ]}"
275 mrpx ${wrap.source},
276 '') wrappers;
277
278 systemd.services.suid-sgid-wrappers = {
279 description = "Create SUID/SGID Wrappers";
280 wantedBy = [ "sysinit.target" ];
281 before = [ "sysinit.target" "shutdown.target" ];
282 conflicts = [ "shutdown.target" ];
283 after = [ "systemd-sysusers.service" ];
284 unitConfig.DefaultDependencies = false;
285 unitConfig.RequiresMountsFor = [ "/nix/store" "/run/wrappers" ];
286 serviceConfig.Type = "oneshot";
287 script = ''
288 chmod 755 "${parentWrapperDir}"
289
290 # We want to place the tmpdirs for the wrappers to the parent dir.
291 wrapperDir=$(mktemp --directory --tmpdir="${parentWrapperDir}" wrappers.XXXXXXXXXX)
292 chmod a+rx "$wrapperDir"
293
294 ${lib.concatStringsSep "\n" mkWrappedPrograms}
295
296 if [ -L ${wrapperDir} ]; then
297 # Atomically replace the symlink
298 # See https://axialcorps.com/2013/07/03/atomically-replacing-files-and-directories/
299 old=$(readlink -f ${wrapperDir})
300 if [ -e "${wrapperDir}-tmp" ]; then
301 rm --force --recursive "${wrapperDir}-tmp"
302 fi
303 ln --symbolic --force --no-dereference "$wrapperDir" "${wrapperDir}-tmp"
304 mv --no-target-directory "${wrapperDir}-tmp" "${wrapperDir}"
305 rm --force --recursive "$old"
306 else
307 # For initial setup
308 ln --symbolic "$wrapperDir" "${wrapperDir}"
309 fi
310 '';
311 };
312
313 ###### wrappers consistency checks
314 system.checks = lib.singleton (pkgs.runCommandLocal
315 "ensure-all-wrappers-paths-exist" { }
316 ''
317 # make sure we produce output
318 mkdir -p $out
319
320 echo -n "Checking that Nix store paths of all wrapped programs exist... "
321
322 declare -A wrappers
323 ${lib.concatStringsSep "\n" (lib.mapAttrsToList (n: v:
324 "wrappers['${n}']='${v.source}'") wrappers)}
325
326 for name in "''${!wrappers[@]}"; do
327 path="''${wrappers[$name]}"
328 if [[ "$path" =~ /nix/store ]] && [ ! -e "$path" ]; then
329 test -t 1 && echo -ne '\033[1;31m'
330 echo "FAIL"
331 echo "The path $path does not exist!"
332 echo 'Please, check the value of `security.wrappers."'$name'".source`.'
333 test -t 1 && echo -ne '\033[0m'
334 exit 1
335 fi
336 done
337
338 echo "OK"
339 '');
340 };
341}