at 23.11-pre 10 kB view raw
1{ config, lib, pkgs, ... }: 2let 3 4 inherit (config.security) wrapperDir wrappers; 5 6 parentWrapperDir = dirOf wrapperDir; 7 8 securityWrapper = pkgs.callPackage ./wrapper.nix { 9 inherit parentWrapperDir; 10 }; 11 12 fileModeType = 13 let 14 # taken from the chmod(1) man page 15 symbolic = "[ugoa]*([-+=]([rwxXst]*|[ugo]))+|[-+=][0-7]+"; 16 numeric = "[-+=]?[0-7]{0,4}"; 17 mode = "((${symbolic})(,${symbolic})*)|(${numeric})"; 18 in 19 lib.types.strMatching mode 20 // { description = "file mode string"; }; 21 22 wrapperType = lib.types.submodule ({ name, config, ... }: { 23 options.source = lib.mkOption 24 { type = lib.types.path; 25 description = lib.mdDoc "The absolute path to the program to be wrapped."; 26 }; 27 options.program = lib.mkOption 28 { type = with lib.types; nullOr str; 29 default = name; 30 description = lib.mdDoc '' 31 The name of the wrapper program. Defaults to the attribute name. 32 ''; 33 }; 34 options.owner = lib.mkOption 35 { type = lib.types.str; 36 description = lib.mdDoc "The owner of the wrapper program."; 37 }; 38 options.group = lib.mkOption 39 { type = lib.types.str; 40 description = lib.mdDoc "The group of the wrapper program."; 41 }; 42 options.permissions = lib.mkOption 43 { type = fileModeType; 44 default = "u+rx,g+x,o+x"; 45 example = "a+rx"; 46 description = lib.mdDoc '' 47 The permissions of the wrapper program. The format is that of a 48 symbolic or numeric file mode understood by {command}`chmod`. 49 ''; 50 }; 51 options.capabilities = lib.mkOption 52 { type = lib.types.commas; 53 default = ""; 54 description = lib.mdDoc '' 55 A comma-separated list of capability clauses to be given to the 56 wrapper program. The format for capability clauses is described in the 57 TEXTUAL REPRESENTATION section of the {manpage}`cap_from_text(3)` 58 manual page. For a list of capabilities supported by the system, check 59 the {manpage}`capabilities(7)` manual page. 60 61 ::: {.note} 62 `cap_setpcap`, which is required for the wrapper 63 program to be able to raise caps into the Ambient set is NOT raised 64 to the Ambient set so that the real program cannot modify its own 65 capabilities!! This may be too restrictive for cases in which the 66 real program needs cap_setpcap but it at least leans on the side 67 security paranoid vs. too relaxed. 68 ::: 69 ''; 70 }; 71 options.setuid = lib.mkOption 72 { type = lib.types.bool; 73 default = false; 74 description = lib.mdDoc "Whether to add the setuid bit the wrapper program."; 75 }; 76 options.setgid = lib.mkOption 77 { type = lib.types.bool; 78 default = false; 79 description = lib.mdDoc "Whether to add the setgid bit the wrapper program."; 80 }; 81 }); 82 83 ###### Activation script for the setcap wrappers 84 mkSetcapProgram = 85 { program 86 , capabilities 87 , source 88 , owner 89 , group 90 , permissions 91 , ... 92 }: 93 '' 94 cp ${securityWrapper}/bin/security-wrapper "$wrapperDir/${program}" 95 echo -n "${source}" > "$wrapperDir/${program}.real" 96 97 # Prevent races 98 chmod 0000 "$wrapperDir/${program}" 99 chown ${owner}:${group} "$wrapperDir/${program}" 100 101 # Set desired capabilities on the file plus cap_setpcap so 102 # the wrapper program can elevate the capabilities set on 103 # its file into the Ambient set. 104 ${pkgs.libcap.out}/bin/setcap "cap_setpcap,${capabilities}" "$wrapperDir/${program}" 105 106 # Set the executable bit 107 chmod ${permissions} "$wrapperDir/${program}" 108 ''; 109 110 ###### Activation script for the setuid wrappers 111 mkSetuidProgram = 112 { program 113 , source 114 , owner 115 , group 116 , setuid 117 , setgid 118 , permissions 119 , ... 120 }: 121 '' 122 cp ${securityWrapper}/bin/security-wrapper "$wrapperDir/${program}" 123 echo -n "${source}" > "$wrapperDir/${program}.real" 124 125 # Prevent races 126 chmod 0000 "$wrapperDir/${program}" 127 chown ${owner}:${group} "$wrapperDir/${program}" 128 129 chmod "u${if setuid then "+" else "-"}s,g${if setgid then "+" else "-"}s,${permissions}" "$wrapperDir/${program}" 130 ''; 131 132 mkWrappedPrograms = 133 builtins.map 134 (opts: 135 if opts.capabilities != "" 136 then mkSetcapProgram opts 137 else mkSetuidProgram opts 138 ) (lib.attrValues wrappers); 139in 140{ 141 imports = [ 142 (lib.mkRemovedOptionModule [ "security" "setuidOwners" ] "Use security.wrappers instead") 143 (lib.mkRemovedOptionModule [ "security" "setuidPrograms" ] "Use security.wrappers instead") 144 ]; 145 146 ###### interface 147 148 options = { 149 security.wrappers = lib.mkOption { 150 type = lib.types.attrsOf wrapperType; 151 default = {}; 152 example = lib.literalExpression 153 '' 154 { 155 # a setuid root program 156 doas = 157 { setuid = true; 158 owner = "root"; 159 group = "root"; 160 source = "''${pkgs.doas}/bin/doas"; 161 }; 162 163 # a setgid program 164 locate = 165 { setgid = true; 166 owner = "root"; 167 group = "mlocate"; 168 source = "''${pkgs.locate}/bin/locate"; 169 }; 170 171 # a program with the CAP_NET_RAW capability 172 ping = 173 { owner = "root"; 174 group = "root"; 175 capabilities = "cap_net_raw+ep"; 176 source = "''${pkgs.iputils.out}/bin/ping"; 177 }; 178 } 179 ''; 180 description = lib.mdDoc '' 181 This option effectively allows adding setuid/setgid bits, capabilities, 182 changing file ownership and permissions of a program without directly 183 modifying it. This works by creating a wrapper program under the 184 {option}`security.wrapperDir` directory, which is then added to 185 the shell `PATH`. 186 ''; 187 }; 188 189 security.wrapperDirSize = lib.mkOption { 190 default = "50%"; 191 example = "10G"; 192 type = lib.types.str; 193 description = lib.mdDoc '' 194 Size limit for the /run/wrappers tmpfs. Look at mount(8), tmpfs size option, 195 for the accepted syntax. WARNING: don't set to less than 64MB. 196 ''; 197 }; 198 199 security.wrapperDir = lib.mkOption { 200 type = lib.types.path; 201 default = "/run/wrappers/bin"; 202 internal = true; 203 description = lib.mdDoc '' 204 This option defines the path to the wrapper programs. It 205 should not be overridden. 206 ''; 207 }; 208 }; 209 210 ###### implementation 211 config = { 212 213 assertions = lib.mapAttrsToList 214 (name: opts: 215 { assertion = opts.setuid || opts.setgid -> opts.capabilities == ""; 216 message = '' 217 The security.wrappers.${name} wrapper is not valid: 218 setuid/setgid and capabilities are mutually exclusive. 219 ''; 220 } 221 ) wrappers; 222 223 security.wrappers = 224 let 225 mkSetuidRoot = source: 226 { setuid = true; 227 owner = "root"; 228 group = "root"; 229 inherit source; 230 }; 231 in 232 { # These are mount related wrappers that require the +s permission. 233 fusermount = mkSetuidRoot "${pkgs.fuse}/bin/fusermount"; 234 fusermount3 = mkSetuidRoot "${pkgs.fuse3}/bin/fusermount3"; 235 mount = mkSetuidRoot "${lib.getBin pkgs.util-linux}/bin/mount"; 236 umount = mkSetuidRoot "${lib.getBin pkgs.util-linux}/bin/umount"; 237 }; 238 239 boot.specialFileSystems.${parentWrapperDir} = { 240 fsType = "tmpfs"; 241 options = [ "nodev" "mode=755" "size=${config.security.wrapperDirSize}" ]; 242 }; 243 244 # Make sure our wrapperDir exports to the PATH env variable when 245 # initializing the shell 246 environment.extraInit = '' 247 # Wrappers override other bin directories. 248 export PATH="${wrapperDir}:$PATH" 249 ''; 250 251 security.apparmor.includes."nixos/security.wrappers" = '' 252 include "${pkgs.apparmorRulesFromClosure { name="security.wrappers"; } [ 253 securityWrapper 254 ]}" 255 ''; 256 257 ###### wrappers activation script 258 system.activationScripts.wrappers = 259 lib.stringAfter [ "specialfs" "users" ] 260 '' 261 chmod 755 "${parentWrapperDir}" 262 263 # We want to place the tmpdirs for the wrappers to the parent dir. 264 wrapperDir=$(mktemp --directory --tmpdir="${parentWrapperDir}" wrappers.XXXXXXXXXX) 265 chmod a+rx "$wrapperDir" 266 267 ${lib.concatStringsSep "\n" mkWrappedPrograms} 268 269 if [ -L ${wrapperDir} ]; then 270 # Atomically replace the symlink 271 # See https://axialcorps.com/2013/07/03/atomically-replacing-files-and-directories/ 272 old=$(readlink -f ${wrapperDir}) 273 if [ -e "${wrapperDir}-tmp" ]; then 274 rm --force --recursive "${wrapperDir}-tmp" 275 fi 276 ln --symbolic --force --no-dereference "$wrapperDir" "${wrapperDir}-tmp" 277 mv --no-target-directory "${wrapperDir}-tmp" "${wrapperDir}" 278 rm --force --recursive "$old" 279 else 280 # For initial setup 281 ln --symbolic "$wrapperDir" "${wrapperDir}" 282 fi 283 ''; 284 285 ###### wrappers consistency checks 286 system.checks = lib.singleton (pkgs.runCommandLocal 287 "ensure-all-wrappers-paths-exist" { } 288 '' 289 # make sure we produce output 290 mkdir -p $out 291 292 echo -n "Checking that Nix store paths of all wrapped programs exist... " 293 294 declare -A wrappers 295 ${lib.concatStringsSep "\n" (lib.mapAttrsToList (n: v: 296 "wrappers['${n}']='${v.source}'") wrappers)} 297 298 for name in "''${!wrappers[@]}"; do 299 path="''${wrappers[$name]}" 300 if [[ "$path" =~ /nix/store ]] && [ ! -e "$path" ]; then 301 test -t 1 && echo -ne '\033[1;31m' 302 echo "FAIL" 303 echo "The path $path does not exist!" 304 echo 'Please, check the value of `security.wrappers."'$name'".source`.' 305 test -t 1 && echo -ne '\033[0m' 306 exit 1 307 fi 308 done 309 310 echo "OK" 311 ''); 312 }; 313}