1 { config, lib, pkgs, ... }:
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 {
15 # glibc definitions of insecure environment variables
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.
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)
25 name = "${name}-unsecvars";
28 cp sysdeps/generic/unsecvars.h $out
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})";
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.";
48 options.program = lib.mkOption
49 { type = with lib.types; nullOr str;
52 The name of the wrapper program. Defaults to the attribute name.
55 options.owner = lib.mkOption
56 { type = lib.types.str;
57 description = "The owner of the wrapper program.";
59 options.group = lib.mkOption
60 { type = lib.types.str;
61 description = "The group of the wrapper program.";
63 options.permissions = lib.mkOption
64 { type = fileModeType;
65 default = "u+rx,g+x,o+x";
68 The permissions of the wrapper program. The format is that of a
69 symbolic or numeric file mode understood by {command}`chmod`.
72 options.capabilities = lib.mkOption
73 { type = lib.types.commas;
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.
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.
92 options.setuid = lib.mkOption
93 { type = lib.types.bool;
95 description = "Whether to add the setuid bit the wrapper program.";
97 options.setgid = lib.mkOption
98 { type = lib.types.bool;
100 description = "Whether to add the setgid bit the wrapper program.";
104 ###### Activation script for the setcap wrappers
115 cp ${securityWrapper source}/bin/security-wrapper "$wrapperDir/${program}"
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}"
130 ###### Activation script for the setuid wrappers
142 cp ${securityWrapper source}/bin/security-wrapper "$wrapperDir/${program}"
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}"
154 if opts.capabilities != ""
155 then mkSetcapProgram opts
156 else mkSetuidProgram opts
157 ) (lib.attrValues wrappers);
161 (lib.mkRemovedOptionModule [ "security" "setuidOwners" ] "Use security.wrappers instead")
162 (lib.mkRemovedOptionModule [ "security" "setuidPrograms" ] "Use security.wrappers instead")
168 security.enableWrappers = lib.mkEnableOption "SUID/SGID wrappers" // {
172 security.wrappers = lib.mkOption {
173 type = lib.types.attrsOf wrapperType;
175 example = lib.literalExpression
178 # a setuid root program
183 source = "''${pkgs.doas}/bin/doas";
191 source = "''${pkgs.locate}/bin/locate";
194 # a program with the CAP_NET_RAW capability
198 capabilities = "cap_net_raw+ep";
199 source = "''${pkgs.iputils.out}/bin/ping";
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
212 security.wrapperDirSize = lib.mkOption {
215 type = lib.types.str;
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.
222 security.wrapperDir = lib.mkOption {
223 type = lib.types.path;
224 default = "/run/wrappers/bin";
227 This option defines the path to the wrapper programs. It
228 should not be overridden.
233 ###### implementation
234 config = lib.mkIf config.security.enableWrappers {
236 assertions = lib.mapAttrsToList
238 { assertion = opts.setuid || opts.setgid -> opts.capabilities == "";
240 The security.wrappers.${name} wrapper is not valid:
241 setuid/setgid and capabilities are mutually exclusive.
248 mkSetuidRoot = source:
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";
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"
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)
278 where = parentWrapperDir;
281 options = lib.concatStringsSep "," ([
284 "size=${config.security.wrapperDirSize}"
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";
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"
313 ln --symbolic --force --no-dereference "$wrapperDir" "${wrapperDir}-tmp"
314 mv --no-target-directory "${wrapperDir}-tmp" "${wrapperDir}"
315 rm --force --recursive "$old"
318 ln --symbolic "$wrapperDir" "${wrapperDir}"
323 ###### wrappers consistency checks
324 system.checks = lib.singleton (pkgs.runCommandLocal
325 "ensure-all-wrappers-paths-exist" { }
327 # make sure we produce output
330 echo -n "Checking that Nix store paths of all wrapped programs exist... "
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'
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'