diff --git a/nixos/doc/manual/release-notes/rl-2311.section.md b/nixos/doc/manual/release-notes/rl-2311.section.md index 58d98b0f0ca47..6cd59a95e63c6 100644 --- a/nixos/doc/manual/release-notes/rl-2311.section.md +++ b/nixos/doc/manual/release-notes/rl-2311.section.md @@ -10,6 +10,16 @@ - The `nixos-rebuild` command has been given a `list-generations` subcommand. See `man nixos-rebuild` for more details. +- [`sudo-rs`], a reimplementation of `sudo` in Rust, is now supported. + Switching to it (via `security.sudo.package = pkgs.sudo-rs;`) introduces + slight changes in default behaviour, due to `sudo-rs`' current limitations: + - terminfo-related environment variables aren't preserved for `root` and `wheel`; + - `root` and `wheel` are not given the ability to set (or preserve) + arbitrary environment variables. + +[`sudo-rs`]: https://github.com/memorysafety/sudo-rs/ + + ## New Services {#sec-release-23.11-new-services} - [MCHPRS](https://github.com/MCHPR/MCHPRS), a multithreaded Minecraft server built for redstone. Available as [services.mchprs](#opt-services.mchprs.enable). @@ -202,6 +212,12 @@ - Package `pash` was removed due to being archived upstream. Use `powershell` as an alternative. +- `security.sudo.extraRules` now includes `root`'s default rule, with ordering + priority 400. This is functionally identical for users not specifying rule + order, or relying on `mkBefore` and `mkAfter`, but may impact users calling + `mkOrder n` with n ≤ 400. + + ## Other Notable Changes {#sec-release-23.11-notable-changes} - The Cinnamon module now enables XDG desktop integration by default. If you are experiencing collisions related to xdg-desktop-portal-gtk you can safely remove `xdg.portal.extraPortals = [ pkgs.xdg-desktop-portal-gtk ];` from your NixOS configuration. @@ -284,6 +300,13 @@ The module update takes care of the new config syntax and the data itself (user - New `boot.bcache.enable` (default enabled) allows completely removing `bcache` mount support. +- `security.sudo` now provides two extra options, that do not change the + module's default behaviour: + - `defaultOptions` controls the options used for the default rules; + - `keepTerminfo` controls whether `TERMINFO` and `TERMINFO_DIRS` are preserved + for `root` and the `wheel` group. + + ## Nixpkgs internals {#sec-release-23.11-nixpkgs-internals} - The use of `sourceRoot = "source";`, `sourceRoot = "source/subdir";`, and similar lines in package derivations using the default `unpackPhase` is deprecated as it requires `unpackPhase` to always produce a directory named "source". Use `sourceRoot = src.name`, `sourceRoot = "${src.name}/subdir";`, or `setSourceRoot = "sourceRoot=$(echo */subdir)";` or similar instead. diff --git a/nixos/lib/test-driver/test_driver/machine.py b/nixos/lib/test-driver/test_driver/machine.py index 809fd690d7173..724b5a6a750da 100644 --- a/nixos/lib/test-driver/test_driver/machine.py +++ b/nixos/lib/test-driver/test_driver/machine.py @@ -582,9 +582,7 @@ class Machine: # While sh is bash on NixOS, this is not the case for every distro. # We explicitly call bash here to allow for the driver to boot other distros as well. - out_command = ( - f"{timeout_str} bash -c {shlex.quote(command)} | (base64 -w 0; echo)\n" - ) + out_command = f"{timeout_str} bash -c {shlex.quote(command)} 2>/dev/null | (base64 -w 0; echo)\n" assert self.shell self.shell.send(out_command.encode()) diff --git a/nixos/modules/config/terminfo.nix b/nixos/modules/config/terminfo.nix index 1ae8e82c471e6..d1dbc4e0d0598 100644 --- a/nixos/modules/config/terminfo.nix +++ b/nixos/modules/config/terminfo.nix @@ -6,12 +6,26 @@ with lib; { - options.environment.enableAllTerminfo = with lib; mkOption { - default = false; - type = types.bool; - description = lib.mdDoc '' - Whether to install all terminfo outputs - ''; + options = with lib; { + environment.enableAllTerminfo = mkOption { + default = false; + type = types.bool; + description = lib.mdDoc '' + Whether to install all terminfo outputs + ''; + }; + + security.sudo.keepTerminfo = mkOption { + default = config.security.sudo.package.pname != "sudo-rs"; + defaultText = literalMD '' + `true` unless using `sudo-rs` + ''; + type = types.bool; + description = lib.mdDoc '' + Whether to preserve the `TERMINFO` and `TERMINFO_DIRS` + environment variables, for `root` and the `wheel` group. + ''; + }; }; config = { @@ -54,7 +68,7 @@ with lib; export TERM=$TERM ''; - security.sudo.extraConfig = '' + security.sudo.extraConfig = mkIf config.security.sudo.keepTerminfo '' # Keep terminfo database for root and %wheel. Defaults:root,%wheel env_keep+=TERMINFO_DIRS diff --git a/nixos/modules/security/sudo.nix b/nixos/modules/security/sudo.nix index d225442773c69..4bdbe9671e6d5 100644 --- a/nixos/modules/security/sudo.nix +++ b/nixos/modules/security/sudo.nix @@ -4,9 +4,16 @@ with lib; let + inherit (pkgs) sudo sudo-rs; + cfg = config.security.sudo; - inherit (pkgs) sudo; + enableSSHAgentAuth = + with config.security; + pam.enableSSHAgentAuth && pam.sudo.sshAgentAuth; + + usingMillersSudo = cfg.package.pname == sudo.pname; + usingSudoRs = cfg.package.pname == sudo-rs.pname; toUserString = user: if (isInt user) then "#${toString user}" else "${user}"; toGroupString = group: if (isInt group) then "%#${toString group}" else "%${group}"; @@ -30,41 +37,51 @@ in ###### interface - options = { + options.security.sudo = { - security.sudo.enable = mkOption { - type = types.bool; - default = true; - description = - lib.mdDoc '' - Whether to enable the {command}`sudo` command, which - allows non-root users to execute commands as root. - ''; + defaultOptions = mkOption { + type = with types; listOf str; + default = optional usingMillersSudo "SETENV"; + defaultText = literalMD '' + `[ "SETENV" ]` if using the default `sudo` implementation + ''; + description = mdDoc '' + Options used for the default rules, granting `root` and the + `wheel` group permission to run any command as any user. + ''; }; - security.sudo.package = mkOption { + enable = mkOption { + type = types.bool; + default = true; + description = mdDoc '' + Whether to enable the {command}`sudo` command, which + allows non-root users to execute commands as root. + ''; + }; + + package = mkOption { type = types.package; default = pkgs.sudo; defaultText = literalExpression "pkgs.sudo"; - description = lib.mdDoc '' + description = mdDoc '' Which package to use for `sudo`. ''; }; - security.sudo.wheelNeedsPassword = mkOption { + wheelNeedsPassword = mkOption { type = types.bool; default = true; - description = - lib.mdDoc '' - Whether users of the `wheel` group must - provide a password to run commands as super user via {command}`sudo`. - ''; + description = mdDoc '' + Whether users of the `wheel` group must + provide a password to run commands as super user via {command}`sudo`. + ''; }; - security.sudo.execWheelOnly = mkOption { + execWheelOnly = mkOption { type = types.bool; default = false; - description = lib.mdDoc '' + description = mdDoc '' Only allow members of the `wheel` group to execute sudo by setting the executable's permissions accordingly. This prevents users that are not members of `wheel` from @@ -72,19 +89,18 @@ in ''; }; - security.sudo.configFile = mkOption { + configFile = mkOption { type = types.lines; # Note: if syntax errors are detected in this file, the NixOS # configuration will fail to build. - description = - lib.mdDoc '' - This string contains the contents of the - {file}`sudoers` file. - ''; + description = mdDoc '' + This string contains the contents of the + {file}`sudoers` file. + ''; }; - security.sudo.extraRules = mkOption { - description = lib.mdDoc '' + extraRules = mkOption { + description = mdDoc '' Define specific rules to be in the {file}`sudoers` file. More specific rules should come after more general ones in order to yield the expected behavior. You can use mkBefore/mkAfter to ensure @@ -114,7 +130,7 @@ in options = { users = mkOption { type = with types; listOf (either str int); - description = lib.mdDoc '' + description = mdDoc '' The usernames / UIDs this rule should apply for. ''; default = []; @@ -122,7 +138,7 @@ in groups = mkOption { type = with types; listOf (either str int); - description = lib.mdDoc '' + description = mdDoc '' The groups / GIDs this rule should apply for. ''; default = []; @@ -131,7 +147,7 @@ in host = mkOption { type = types.str; default = "ALL"; - description = lib.mdDoc '' + description = mdDoc '' For what host this rule should apply. ''; }; @@ -139,7 +155,7 @@ in runAs = mkOption { type = with types; str; default = "ALL:ALL"; - description = lib.mdDoc '' + description = mdDoc '' Under which user/group the specified command is allowed to run. A user can be specified using just the username: `"foo"`. @@ -149,7 +165,7 @@ in }; commands = mkOption { - description = lib.mdDoc '' + description = mdDoc '' The commands for which the rule should apply. ''; type = with types; listOf (either str (submodule { @@ -157,7 +173,7 @@ in options = { command = mkOption { type = with types; str; - description = lib.mdDoc '' + description = mdDoc '' A command being either just a path to a binary to allow any arguments, the full command with arguments pre-set or with `""` used as the argument, not allowing arguments to the command at all. @@ -166,7 +182,7 @@ in options = mkOption { type = with types; listOf (enum [ "NOPASSWD" "PASSWD" "NOEXEC" "EXEC" "SETENV" "NOSETENV" "LOG_INPUT" "NOLOG_INPUT" "LOG_OUTPUT" "NOLOG_OUTPUT" ]); - description = lib.mdDoc '' + description = mdDoc '' Options for running the command. Refer to the [sudo manual](https://www.sudo.ws/man/1.7.10/sudoers.man.html). ''; default = []; @@ -179,10 +195,10 @@ in }); }; - security.sudo.extraConfig = mkOption { + extraConfig = mkOption { type = types.lines; default = ""; - description = lib.mdDoc '' + description = mdDoc '' Extra configuration text appended to {file}`sudoers`. ''; }; @@ -192,44 +208,52 @@ in ###### implementation config = mkIf cfg.enable { - assertions = [ - { assertion = cfg.package.pname != "sudo-rs"; - message = "The NixOS `sudo` module does not work with `sudo-rs` yet."; } - ]; + security.sudo.extraRules = + let + defaultRule = { users ? [], groups ? [], opts ? [] }: [ { + inherit users groups; + commands = [ { + command = "ALL"; + options = opts ++ cfg.defaultOptions; + } ]; + } ]; + in mkMerge [ + # This is ordered before users' `mkBefore` rules, + # so as not to introduce unexpected changes. + (mkOrder 400 (defaultRule { users = [ "root" ]; })) - # We `mkOrder 600` so that the default rule shows up first, but there is - # still enough room for a user to `mkBefore` it. - security.sudo.extraRules = mkOrder 600 [ - { groups = [ "wheel" ]; - commands = [ { command = "ALL"; options = (if cfg.wheelNeedsPassword then [ "SETENV" ] else [ "NOPASSWD" "SETENV" ]); } ]; - } - ]; + # This is ordered to show before (most) other rules, but + # late-enough for a user to `mkBefore` it. + (mkOrder 600 (defaultRule { + groups = [ "wheel" ]; + opts = (optional (!cfg.wheelNeedsPassword) "NOPASSWD"); + })) + ]; - security.sudo.configFile = + security.sudo.configFile = concatStringsSep "\n" (filter (s: s != "") [ '' # Don't edit this file. Set the NixOS options ‘security.sudo.configFile’ # or ‘security.sudo.extraRules’ instead. - + '' + (optionalString enableSSHAgentAuth '' # Keep SSH_AUTH_SOCK so that pam_ssh_agent_auth.so can do its magic. Defaults env_keep+=SSH_AUTH_SOCK - - # "root" is allowed to do anything. - root ALL=(ALL:ALL) SETENV: ALL - - # extraRules - ${concatStringsSep "\n" ( - lists.flatten ( - map ( - rule: optionals (length rule.commands != 0) [ - (map (user: "${toUserString user} ${rule.host}=(${rule.runAs}) ${toCommandsString rule.commands}") rule.users) - (map (group: "${toGroupString group} ${rule.host}=(${rule.runAs}) ${toCommandsString rule.commands}") rule.groups) - ] - ) cfg.extraRules - ) - )} - + '') + (concatStringsSep "\n" ( + lists.flatten ( + map ( + rule: optionals (length rule.commands != 0) [ + (map (user: "${toUserString user} ${rule.host}=(${rule.runAs}) ${toCommandsString rule.commands}") rule.users) + (map (group: "${toGroupString group} ${rule.host}=(${rule.runAs}) ${toCommandsString rule.commands}") rule.groups) + ] + ) cfg.extraRules + ) + ) + "\n") + (optionalString (cfg.extraConfig != "") '' + # extraConfig ${cfg.extraConfig} - ''; + '') + ]); security.wrappers = let owner = "root"; @@ -241,7 +265,8 @@ in source = "${cfg.package.out}/bin/sudo"; inherit owner group setuid permissions; }; - sudoedit = { + # sudo-rs does not yet ship a sudoedit (as of v0.2.0) + sudoedit = mkIf usingMillersSudo { source = "${cfg.package.out}/bin/sudoedit"; inherit owner group setuid permissions; }; @@ -250,6 +275,8 @@ in environment.systemPackages = [ sudo ]; security.pam.services.sudo = { sshAgentAuth = true; usshAuth = true; }; + security.pam.services.sudo-i = mkIf usingSudoRs + { sshAgentAuth = true; usshAuth = true; }; environment.etc.sudoers = { source = @@ -258,12 +285,12 @@ in src = pkgs.writeText "sudoers-in" cfg.configFile; preferLocalBuild = true; } - # Make sure that the sudoers file is syntactically valid. - # (currently disabled - NIXOS-66) - "${pkgs.buildPackages.sudo}/sbin/visudo -f $src -c && cp $src $out"; + "${cfg.package}/bin/visudo -f $src -c && cp $src $out"; mode = "0440"; }; }; + meta.maintainers = [ lib.maintainers.nicoo ]; + } diff --git a/nixos/tests/all-tests.nix b/nixos/tests/all-tests.nix index 0574c1db87544..e362d9cb32351 100644 --- a/nixos/tests/all-tests.nix +++ b/nixos/tests/all-tests.nix @@ -741,6 +741,7 @@ in { strongswan-swanctl = handleTest ./strongswan-swanctl.nix {}; stunnel = handleTest ./stunnel.nix {}; sudo = handleTest ./sudo.nix {}; + sudo-rs = handleTest ./sudo-rs.nix {}; swap-file-btrfs = handleTest ./swap-file-btrfs.nix {}; swap-partition = handleTest ./swap-partition.nix {}; swap-random-encryption = handleTest ./swap-random-encryption.nix {}; diff --git a/nixos/tests/sudo-rs.nix b/nixos/tests/sudo-rs.nix new file mode 100644 index 0000000000000..334a2d0fa3ea4 --- /dev/null +++ b/nixos/tests/sudo-rs.nix @@ -0,0 +1,97 @@ +# Some tests to ensure sudo is working properly. +{ pkgs, sudo-rs, ... }: +let + inherit (pkgs.lib) mkIf optionalString; + password = "helloworld"; +in + import ./make-test-python.nix ({ lib, pkgs, ...} : { + name = "sudo"; + meta.maintainers = pkgs.sudo-rs.meta.maintainers; + + nodes.machine = + { lib, ... }: + { + environment.systemPackages = [ pkgs.faketty ]; + users.groups = { foobar = {}; barfoo = {}; baz = { gid = 1337; }; }; + users.users = { + test0 = { isNormalUser = true; extraGroups = [ "wheel" ]; }; + test1 = { isNormalUser = true; password = password; }; + test2 = { isNormalUser = true; extraGroups = [ "foobar" ]; password = password; }; + test3 = { isNormalUser = true; extraGroups = [ "barfoo" ]; }; + test4 = { isNormalUser = true; extraGroups = [ "baz" ]; }; + test5 = { isNormalUser = true; }; + }; + + security.sudo = { + enable = true; + package = sudo-rs; + wheelNeedsPassword = false; + + extraRules = [ + # SUDOERS SYNTAX CHECK (Test whether the module produces a valid output; + # errors being detected by the visudo checks. + + # These should not create any entries + { users = [ "notest1" ]; commands = [ ]; } + { commands = [ { command = "ALL"; options = [ ]; } ]; } + + # Test defining commands with the options syntax, though not setting any options + { users = [ "notest2" ]; commands = [ { command = "ALL"; options = [ ]; } ]; } + + + # CONFIGURATION FOR TEST CASES + { users = [ "test1" ]; groups = [ "foobar" ]; commands = [ "ALL" ]; } + { groups = [ "barfoo" 1337 ]; commands = [ { command = "ALL"; options = [ "NOPASSWD" ]; } ]; } + { users = [ "test5" ]; commands = [ { command = "ALL"; options = [ "NOPASSWD" ]; } ]; runAs = "test1:barfoo"; } + ]; + }; + }; + + nodes.strict = { ... }: { + environment.systemPackages = [ pkgs.faketty ]; + users.users = { + admin = { isNormalUser = true; extraGroups = [ "wheel" ]; }; + noadmin = { isNormalUser = true; }; + }; + + security.sudo = { + package = sudo-rs; + enable = true; + wheelNeedsPassword = false; + execWheelOnly = true; + }; + }; + + testScript = + '' + with subtest("users in wheel group should have passwordless sudo"): + machine.succeed('faketty -- su - test0 -c "sudo -u root true"') + + with subtest("test1 user should have sudo with password"): + machine.succeed('faketty -- su - test1 -c "echo ${password} | sudo -S -u root true"') + + with subtest("test1 user should not be able to use sudo without password"): + machine.fail('faketty -- su - test1 -c "sudo -n -u root true"') + + with subtest("users in group 'foobar' should be able to use sudo with password"): + machine.succeed('faketty -- su - test2 -c "echo ${password} | sudo -S -u root true"') + + with subtest("users in group 'barfoo' should be able to use sudo without password"): + machine.succeed("sudo -u test3 sudo -n -u root true") + + with subtest("users in group 'baz' (GID 1337)"): + machine.succeed("sudo -u test4 sudo -n -u root echo true") + + with subtest("test5 user should be able to run commands under test1"): + machine.succeed("sudo -u test5 sudo -n -u test1 true") + + with subtest("test5 user should not be able to run commands under root"): + machine.fail("sudo -u test5 sudo -n -u root true") + + with subtest("users in wheel should be able to run sudo despite execWheelOnly"): + strict.succeed('faketty -- su - admin -c "sudo -u root true"') + + with subtest("non-wheel users should be unable to run sudo thanks to execWheelOnly"): + strict.fail('faketty -- su - noadmin -c "sudo --help"') + '';; + }) diff --git a/pkgs/tools/security/sudo-rs/default.nix b/pkgs/tools/security/sudo-rs/default.nix index d4621e2292252..3cda1cde8322c 100644 --- a/pkgs/tools/security/sudo-rs/default.nix +++ b/pkgs/tools/security/sudo-rs/default.nix @@ -4,6 +4,7 @@ , fetchpatch , installShellFiles , nix-update-script +, nixosTests , pam , pandoc , rustPlatform @@ -73,7 +74,10 @@ rustPlatform.buildRustPackage rec { "su::context::tests::invalid_shell" ]; - passthru.updateScript = nix-update-script { }; + passthru = { + updateScript = nix-update-script { }; + tests = nixosTests.sudo-rs; + }; meta = with lib; { description = "A memory safe implementation of sudo and su.";