lol

nixos/security/wrappers: make well-typed

The security.wrappers option is morally a set of submodules but it's
actually (un)typed as a generic attribute set. This is bad for several
reasons:

1. Some of the "submodule" option are not document;
2. the default values are not documented and are chosen based on
somewhat bizarre rules (issue #23217);
3. It's not possible to override an existing wrapper due to the
dumb types.attrs.merge strategy;
4. It's easy to make mistakes that will go unnoticed, which is
really bad given the sensitivity of this module (issue #47839).

This makes the option a proper set of submodule and add strict types and
descriptions to every sub-option. Considering it's not yet clear if the
way the default values are picked is intended, this reproduces the current
behavior, but it's now documented explicitly.

rnhmjoj 904f68fb c80b1155

+119 -57
+119 -57
nixos/modules/security/wrappers/default.nix
··· 5 5 6 6 parentWrapperDir = dirOf wrapperDir; 7 7 8 - programs = 9 - (lib.mapAttrsToList 10 - (n: v: (if v ? program then v else v // {program=n;})) 11 - wrappers); 12 - 13 8 securityWrapper = pkgs.callPackage ./wrapper.nix { 14 9 inherit parentWrapperDir; 15 10 }; 16 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 = "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 = '' 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 + default = with config; 37 + if (capabilities != "") || !(setuid || setgid || permissions != null) 38 + then "root" 39 + else "nobody"; 40 + description = '' 41 + The owner of the wrapper program. Defaults to <literal>root</literal> 42 + if any capability is set and setuid/setgid/permissions are not, otherwise to 43 + <literal>nobody</litera>. 44 + ''; 45 + }; 46 + options.group = lib.mkOption 47 + { type = lib.types.str; 48 + default = with config; 49 + if (capabilities != "") || !(setuid || setgid || permissions != null) 50 + then "root" 51 + else "nogroup"; 52 + description = '' 53 + The group of the wrapper program. Defaults to <literal>root</literal> 54 + if any capability is set and setuid/setgid/permissions are not, 55 + otherwise to <literal>nogroup</litera>. 56 + ''; 57 + }; 58 + options.permissions = lib.mkOption 59 + { type = lib.types.nullOr fileModeType; 60 + default = null; 61 + example = "u+rx,g+x,o+x"; 62 + apply = x: if x == null then "u+rx,g+x,o+x" else x; 63 + description = '' 64 + The permissions of the wrapper program. The format is that of a 65 + symbolic or numeric file mode understood by <command>chmod</command>. 66 + ''; 67 + }; 68 + options.capabilities = lib.mkOption 69 + { type = lib.types.commas; 70 + default = ""; 71 + description = '' 72 + A comma-separated list of capabilities to be given to the wrapper 73 + program. For capabilities supported by the system check the 74 + <citerefentry> 75 + <refentrytitle>capabilities</refentrytitle> 76 + <manvolnum>7</manvolnum> 77 + </citerefentry> 78 + manual page. 79 + 80 + <note><para> 81 + <literal>cap_setpcap</literal>, which is required for the wrapper 82 + program to be able to raise caps into the Ambient set is NOT raised 83 + to the Ambient set so that the real program cannot modify its own 84 + capabilities!! This may be too restrictive for cases in which the 85 + real program needs cap_setpcap but it at least leans on the side 86 + security paranoid vs. too relaxed. 87 + </para></note> 88 + ''; 89 + }; 90 + options.setuid = lib.mkOption 91 + { type = lib.types.bool; 92 + default = false; 93 + description = "Whether to add the setuid bit the wrapper program."; 94 + }; 95 + options.setgid = lib.mkOption 96 + { type = lib.types.bool; 97 + default = false; 98 + description = "Whether to add the setgid bit the wrapper program."; 99 + }; 100 + }); 101 + 17 102 ###### Activation script for the setcap wrappers 18 103 mkSetcapProgram = 19 104 { program 20 105 , capabilities 21 106 , source 22 - , owner ? "nobody" 23 - , group ? "nogroup" 24 - , permissions ? "u+rx,g+x,o+x" 107 + , owner 108 + , group 109 + , permissions 25 110 , ... 26 111 }: 27 112 assert (lib.versionAtLeast (lib.getVersion config.boot.kernelPackages.kernel) "4.3"); ··· 46 131 mkSetuidProgram = 47 132 { program 48 133 , source 49 - , owner ? "nobody" 50 - , group ? "nogroup" 51 - , setuid ? false 52 - , setgid ? false 53 - , permissions ? "u+rx,g+x,o+x" 134 + , owner 135 + , group 136 + , setuid 137 + , setgid 138 + , permissions 54 139 , ... 55 140 }: 56 141 '' ··· 66 151 67 152 mkWrappedPrograms = 68 153 builtins.map 69 - (s: if (s ? capabilities) 70 - then mkSetcapProgram 71 - ({ owner = "root"; 72 - group = "root"; 73 - } // s) 74 - else if 75 - (s ? setuid && s.setuid) || 76 - (s ? setgid && s.setgid) || 77 - (s ? permissions) 78 - then mkSetuidProgram s 79 - else mkSetuidProgram 80 - ({ owner = "root"; 81 - group = "root"; 82 - setuid = true; 83 - setgid = false; 84 - permissions = "u+rx,g+x,o+x"; 85 - } // s) 86 - ) programs; 154 + (opts: 155 + if opts.capabilities != "" 156 + then mkSetcapProgram opts 157 + else mkSetuidProgram opts 158 + ) (lib.attrValues wrappers); 87 159 in 88 160 { 89 161 imports = [ ··· 95 167 96 168 options = { 97 169 security.wrappers = lib.mkOption { 98 - type = lib.types.attrs; 170 + type = lib.types.attrsOf wrapperType; 99 171 default = {}; 100 172 example = lib.literalExample 101 173 '' ··· 109 181 } 110 182 ''; 111 183 description = '' 112 - This option allows the ownership and permissions on the setuid 113 - wrappers for specific programs to be overridden from the 114 - default (setuid root, but not setgid root). 115 - 116 - <note> 117 - <para>The sub-attribute <literal>source</literal> is mandatory, 118 - it must be the absolute path to the program to be wrapped. 119 - </para> 120 - 121 - <para>The sub-attribute <literal>program</literal> is optional and 122 - can give the wrapper program a new name. The default name is the same 123 - as the attribute name itself.</para> 124 - 125 - <para>Additionally, this option can set capabilities on a 126 - wrapper program that propagates those capabilities down to the 127 - wrapped, real program.</para> 128 - 129 - <para>NOTE: cap_setpcap, which is required for the wrapper 130 - program to be able to raise caps into the Ambient set is NOT 131 - raised to the Ambient set so that the real program cannot 132 - modify its own capabilities!! This may be too restrictive for 133 - cases in which the real program needs cap_setpcap but it at 134 - least leans on the side security paranoid vs. too 135 - relaxed.</para> 136 - </note> 184 + This option effectively allows adding setuid/setgid bits, capabilities, 185 + changing file ownership and permissions of a program without directly 186 + modifying it. This works by creating a wrapper program under the 187 + <option>security.wrapperDir</option> directory, which is then added to 188 + the shell <literal>PATH</literal>. 137 189 ''; 138 190 }; 139 191 ··· 150 202 151 203 ###### implementation 152 204 config = { 205 + 206 + assertions = lib.mapAttrsToList 207 + (name: opts: 208 + { assertion = opts.setuid || opts.setgid -> opts.capabilities == ""; 209 + message = '' 210 + The security.wrappers.${name} wrapper is not valid: 211 + setuid/setgid and capabilities are mutually exclusive. 212 + ''; 213 + } 214 + ) wrappers; 153 215 154 216 security.wrappers = { 155 217 # These are mount related wrappers that require the +s permission.