at 25.11-pre 12 kB view raw
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}