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