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}