at 24.11-pre 11 kB view raw
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}