at 25.11-pre 9.8 kB view raw
1{ 2 config, 3 lib, 4 pkgs, 5 ... 6}: 7let 8 9 cfg = config.security.sudo-rs; 10 11 toUserString = user: if (lib.isInt user) then "#${toString user}" else "${user}"; 12 toGroupString = group: if (lib.isInt group) then "%#${toString group}" else "%${group}"; 13 14 toCommandOptionsString = 15 options: "${lib.concatStringsSep ":" options}${lib.optionalString (lib.length options != 0) ":"} "; 16 17 toCommandsString = 18 commands: 19 lib.concatStringsSep ", " ( 20 map ( 21 command: 22 if (lib.isString command) then 23 command 24 else 25 "${toCommandOptionsString command.options}${command.command}" 26 ) commands 27 ); 28 29in 30 31{ 32 33 ###### interface 34 35 options.security.sudo-rs = { 36 37 defaultOptions = lib.mkOption { 38 type = with lib.types; listOf str; 39 default = [ "SETENV" ]; 40 description = '' 41 Options used for the default rules, granting `root` and the 42 `wheel` group permission to run any command as any user. 43 ''; 44 }; 45 46 enable = lib.mkEnableOption '' 47 a memory-safe implementation of the {command}`sudo` command, 48 which allows non-root users to execute commands as root 49 ''; 50 51 package = lib.mkPackageOption pkgs "sudo-rs" { }; 52 53 wheelNeedsPassword = lib.mkOption { 54 type = lib.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 = lib.mkOption { 63 type = lib.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 = lib.mkOption { 74 type = lib.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 = lib.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 `lib.mkBefore`/`lib.mkAfter` to ensure 88 this is the case when configuration options are merged. 89 ''; 90 default = [ ]; 91 example = lib.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 = 111 with lib.types; 112 listOf (submodule { 113 options = { 114 users = lib.mkOption { 115 type = with lib.types; listOf (either str int); 116 description = '' 117 The usernames / UIDs this rule should apply for. 118 ''; 119 default = [ ]; 120 }; 121 122 groups = lib.mkOption { 123 type = with lib.types; listOf (either str int); 124 description = '' 125 The groups / GIDs this rule should apply for. 126 ''; 127 default = [ ]; 128 }; 129 130 host = lib.mkOption { 131 type = lib.types.str; 132 default = "ALL"; 133 description = '' 134 For what host this rule should apply. 135 ''; 136 }; 137 138 runAs = lib.mkOption { 139 type = with lib.types; str; 140 default = "ALL:ALL"; 141 description = '' 142 Under which user/group the specified command is allowed to run. 143 144 A user can be specified using just the username: `"foo"`. 145 It is also possible to specify a user/group combination using `"foo:bar"` 146 or to only allow running as a specific group with `":bar"`. 147 ''; 148 }; 149 150 commands = lib.mkOption { 151 description = '' 152 The commands for which the rule should apply. 153 ''; 154 type = 155 with lib.types; 156 listOf ( 157 either str (submodule { 158 159 options = { 160 command = lib.mkOption { 161 type = with lib.types; str; 162 description = '' 163 A command being either just a path to a binary to allow any arguments, 164 the full command with arguments pre-set or with `""` used as the argument, 165 not allowing arguments to the command at all. 166 ''; 167 }; 168 169 options = lib.mkOption { 170 type = 171 with lib.types; 172 listOf (enum [ 173 "NOPASSWD" 174 "PASSWD" 175 "NOEXEC" 176 "EXEC" 177 "SETENV" 178 "NOSETENV" 179 "LOG_INPUT" 180 "NOLOG_INPUT" 181 "LOG_OUTPUT" 182 "NOLOG_OUTPUT" 183 ]); 184 description = '' 185 Options for running the command. Refer to the [sudo manual](https://www.sudo.ws/man/1.7.10/sudoers.man.html). 186 ''; 187 default = [ ]; 188 }; 189 }; 190 191 }) 192 ); 193 }; 194 }; 195 }); 196 }; 197 198 extraConfig = lib.mkOption { 199 type = lib.types.lines; 200 default = ""; 201 description = '' 202 Extra configuration text appended to {file}`sudoers`. 203 ''; 204 }; 205 }; 206 207 ###### implementation 208 209 config = lib.mkIf cfg.enable { 210 assertions = [ 211 { 212 assertion = !config.security.sudo.enable; 213 message = "`security.sudo` and `security.sudo-rs` cannot both be enabled"; 214 } 215 ]; 216 security.sudo.enable = lib.mkDefault false; 217 218 security.sudo-rs.extraRules = 219 let 220 defaultRule = 221 { 222 users ? [ ], 223 groups ? [ ], 224 opts ? [ ], 225 }: 226 [ 227 { 228 inherit users groups; 229 commands = [ 230 { 231 command = "ALL"; 232 options = opts ++ cfg.defaultOptions; 233 } 234 ]; 235 } 236 ]; 237 in 238 lib.mkMerge [ 239 # This is ordered before users' `lib.mkBefore` rules, 240 # so as not to introduce unexpected changes. 241 (lib.mkOrder 400 (defaultRule { 242 users = [ "root" ]; 243 })) 244 245 # This is ordered to show before (most) other rules, but 246 # late-enough for a user to `lib.mkBefore` it. 247 (lib.mkOrder 600 (defaultRule { 248 groups = [ "wheel" ]; 249 opts = (lib.optional (!cfg.wheelNeedsPassword) "NOPASSWD"); 250 })) 251 ]; 252 253 security.sudo-rs.configFile = lib.concatStringsSep "\n" ( 254 lib.filter (s: s != "") [ 255 '' 256 # Don't edit this file. Set the NixOS options security.sudo-rs.configFile 257 # or security.sudo-rs.extraRules instead. 258 '' 259 (lib.pipe cfg.extraRules [ 260 (lib.filter (rule: lib.length rule.commands != 0)) 261 (map (rule: [ 262 (map ( 263 user: "${toUserString user} ${rule.host}=(${rule.runAs}) ${toCommandsString rule.commands}" 264 ) rule.users) 265 (map ( 266 group: "${toGroupString group} ${rule.host}=(${rule.runAs}) ${toCommandsString rule.commands}" 267 ) rule.groups) 268 ])) 269 lib.flatten 270 (lib.concatStringsSep "\n") 271 ]) 272 "\n" 273 (lib.optionalString (cfg.extraConfig != "") '' 274 # extraConfig 275 ${cfg.extraConfig} 276 '') 277 ] 278 ); 279 280 security.wrappers = 281 let 282 owner = "root"; 283 group = if cfg.execWheelOnly then "wheel" else "root"; 284 setuid = true; 285 permissions = if cfg.execWheelOnly then "u+rx,g+x" else "u+rx,g+x,o+x"; 286 in 287 { 288 sudo = { 289 source = "${lib.getExe cfg.package}"; 290 inherit 291 owner 292 group 293 setuid 294 permissions 295 ; 296 }; 297 }; 298 299 environment.systemPackages = [ cfg.package ]; 300 301 security.pam.services.sudo = { 302 sshAgentAuth = true; 303 usshAuth = true; 304 }; 305 security.pam.services.sudo-i = { 306 sshAgentAuth = true; 307 usshAuth = true; 308 }; 309 310 environment.etc.sudoers = { 311 source = pkgs.runCommand "sudoers" { 312 src = pkgs.writeText "sudoers-in" cfg.configFile; 313 preferLocalBuild = true; 314 } "${pkgs.buildPackages.sudo-rs}/bin/visudo -f $src -c && cp $src $out"; 315 mode = "0440"; 316 }; 317 318 }; 319 320 meta.maintainers = [ lib.maintainers.nicoo ]; 321 322}