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