at 24.11-pre 8.8 kB view raw
1{ config, lib, pkgs, ... }: 2 3with lib; 4 5let 6 7 cfg = config.security.sudo; 8 9 toUserString = user: if (isInt user) then "#${toString user}" else "${user}"; 10 toGroupString = group: if (isInt group) then "%#${toString group}" else "%${group}"; 11 12 toCommandOptionsString = options: 13 "${concatStringsSep ":" options}${optionalString (length options != 0) ":"} "; 14 15 toCommandsString = commands: 16 concatStringsSep ", " ( 17 map (command: 18 if (isString command) then 19 command 20 else 21 "${toCommandOptionsString command.options}${command.command}" 22 ) commands 23 ); 24 25in 26 27{ 28 29 ###### interface 30 31 options.security.sudo = { 32 33 defaultOptions = mkOption { 34 type = with types; listOf str; 35 default = [ "SETENV" ]; 36 description = '' 37 Options used for the default rules, granting `root` and the 38 `wheel` group permission to run any command as any user. 39 ''; 40 }; 41 42 enable = mkOption { 43 type = types.bool; 44 default = true; 45 description = '' 46 Whether to enable the {command}`sudo` command, which 47 allows non-root users to execute commands as root. 48 ''; 49 }; 50 51 package = mkPackageOption pkgs "sudo" { }; 52 53 wheelNeedsPassword = mkOption { 54 type = types.bool; 55 default = true; 56 description = '' 57 Whether users of the `wheel` group must 58 provide a password to run commands as super user via {command}`sudo`. 59 ''; 60 }; 61 62 execWheelOnly = mkOption { 63 type = types.bool; 64 default = false; 65 description = '' 66 Only allow members of the `wheel` group to execute sudo by 67 setting the executable's permissions accordingly. 68 This prevents users that are not members of `wheel` from 69 exploiting vulnerabilities in sudo such as CVE-2021-3156. 70 ''; 71 }; 72 73 configFile = mkOption { 74 type = types.lines; 75 # Note: if syntax errors are detected in this file, the NixOS 76 # configuration will fail to build. 77 description = '' 78 This string contains the contents of the 79 {file}`sudoers` file. 80 ''; 81 }; 82 83 extraRules = mkOption { 84 description = '' 85 Define specific rules to be in the {file}`sudoers` file. 86 More specific rules should come after more general ones in order to 87 yield the expected behavior. You can use mkBefore/mkAfter to ensure 88 this is the case when configuration options are merged. 89 ''; 90 default = []; 91 example = literalExpression '' 92 [ 93 # Allow execution of any command by all users in group sudo, 94 # requiring a password. 95 { groups = [ "sudo" ]; commands = [ "ALL" ]; } 96 97 # Allow execution of "/home/root/secret.sh" by user `backup`, `database` 98 # and the group with GID `1006` without a password. 99 { users = [ "backup" "database" ]; groups = [ 1006 ]; 100 commands = [ { command = "/home/root/secret.sh"; options = [ "SETENV" "NOPASSWD" ]; } ]; } 101 102 # Allow all users of group `bar` to run two executables as user `foo` 103 # with arguments being pre-set. 104 { groups = [ "bar" ]; runAs = "foo"; 105 commands = 106 [ "/home/baz/cmd1.sh hello-sudo" 107 { command = '''/home/baz/cmd2.sh ""'''; options = [ "SETENV" ]; } ]; } 108 ] 109 ''; 110 type = with types; listOf (submodule { 111 options = { 112 users = mkOption { 113 type = with types; listOf (either str int); 114 description = '' 115 The usernames / UIDs this rule should apply for. 116 ''; 117 default = []; 118 }; 119 120 groups = mkOption { 121 type = with types; listOf (either str int); 122 description = '' 123 The groups / GIDs this rule should apply for. 124 ''; 125 default = []; 126 }; 127 128 host = mkOption { 129 type = types.str; 130 default = "ALL"; 131 description = '' 132 For what host this rule should apply. 133 ''; 134 }; 135 136 runAs = mkOption { 137 type = with types; str; 138 default = "ALL:ALL"; 139 description = '' 140 Under which user/group the specified command is allowed to run. 141 142 A user can be specified using just the username: `"foo"`. 143 It is also possible to specify a user/group combination using `"foo:bar"` 144 or to only allow running as a specific group with `":bar"`. 145 ''; 146 }; 147 148 commands = mkOption { 149 description = '' 150 The commands for which the rule should apply. 151 ''; 152 type = with types; listOf (either str (submodule { 153 154 options = { 155 command = mkOption { 156 type = with types; str; 157 description = '' 158 A command being either just a path to a binary to allow any arguments, 159 the full command with arguments pre-set or with `""` used as the argument, 160 not allowing arguments to the command at all. 161 ''; 162 }; 163 164 options = mkOption { 165 type = with types; listOf (enum [ "NOPASSWD" "PASSWD" "NOEXEC" "EXEC" "SETENV" "NOSETENV" "LOG_INPUT" "NOLOG_INPUT" "LOG_OUTPUT" "NOLOG_OUTPUT" "MAIL" "NOMAIL" "FOLLOW" "NOFLLOW" "INTERCEPT" "NOINTERCEPT"]); 166 description = '' 167 Options for running the command. Refer to the [sudo manual](https://www.sudo.ws/docs/man/1.9.15/sudoers.man/#Tag_Spec). 168 ''; 169 default = []; 170 }; 171 }; 172 173 })); 174 }; 175 }; 176 }); 177 }; 178 179 extraConfig = mkOption { 180 type = types.lines; 181 default = ""; 182 description = '' 183 Extra configuration text appended to {file}`sudoers`. 184 ''; 185 }; 186 }; 187 188 189 ###### implementation 190 191 config = mkIf cfg.enable { 192 assertions = [ { 193 assertion = cfg.package.pname != "sudo-rs"; 194 message = '' 195 NixOS' `sudo` module does not support `sudo-rs`; see `security.sudo-rs` instead. 196 ''; 197 } ]; 198 199 security.sudo.extraRules = 200 let 201 defaultRule = { users ? [], groups ? [], opts ? [] }: [ { 202 inherit users groups; 203 commands = [ { 204 command = "ALL"; 205 options = opts ++ cfg.defaultOptions; 206 } ]; 207 } ]; 208 in mkMerge [ 209 # This is ordered before users' `mkBefore` rules, 210 # so as not to introduce unexpected changes. 211 (mkOrder 400 (defaultRule { users = [ "root" ]; })) 212 213 # This is ordered to show before (most) other rules, but 214 # late-enough for a user to `mkBefore` it. 215 (mkOrder 600 (defaultRule { 216 groups = [ "wheel" ]; 217 opts = (optional (!cfg.wheelNeedsPassword) "NOPASSWD"); 218 })) 219 ]; 220 221 security.sudo.configFile = concatStringsSep "\n" (filter (s: s != "") [ 222 '' 223 # Don't edit this file. Set the NixOS options security.sudo.configFile 224 # or security.sudo.extraRules instead. 225 '' 226 (pipe cfg.extraRules [ 227 (filter (rule: length rule.commands != 0)) 228 (map (rule: [ 229 (map (user: "${toUserString user} ${rule.host}=(${rule.runAs}) ${toCommandsString rule.commands}") rule.users) 230 (map (group: "${toGroupString group} ${rule.host}=(${rule.runAs}) ${toCommandsString rule.commands}") rule.groups) 231 ])) 232 flatten 233 (concatStringsSep "\n") 234 ]) 235 "\n" 236 (optionalString (cfg.extraConfig != "") '' 237 # extraConfig 238 ${cfg.extraConfig} 239 '') 240 ]); 241 242 security.wrappers = let 243 owner = "root"; 244 group = if cfg.execWheelOnly then "wheel" else "root"; 245 setuid = true; 246 permissions = if cfg.execWheelOnly then "u+rx,g+x" else "u+rx,g+x,o+x"; 247 in { 248 sudo = { 249 source = "${cfg.package.out}/bin/sudo"; 250 inherit owner group setuid permissions; 251 }; 252 sudoedit = { 253 source = "${cfg.package.out}/bin/sudoedit"; 254 inherit owner group setuid permissions; 255 }; 256 }; 257 258 environment.systemPackages = [ cfg.package ]; 259 260 security.pam.services.sudo = { sshAgentAuth = true; usshAuth = true; }; 261 262 environment.etc.sudoers = 263 { source = 264 pkgs.runCommand "sudoers" 265 { 266 src = pkgs.writeText "sudoers-in" cfg.configFile; 267 preferLocalBuild = true; 268 } 269 # Make sure that the sudoers file is syntactically valid. 270 # (currently disabled - NIXOS-66) 271 "${pkgs.buildPackages.sudo}/sbin/visudo -f $src -c && cp $src $out"; 272 mode = "0440"; 273 }; 274 275 }; 276 277}