{ config, lib, pkgs, ... }: with lib; let cfg = config.security.sudo; inherit (pkgs) sudo; toUserString = user: if (isInt user) then "#${toString user}" else "${user}"; toGroupString = group: if (isInt group) then "%#${toString group}" else "%${group}"; toCommandOptionsString = options: "${concatStringsSep ":" options}${optionalString (length options != 0) ":"} "; toCommandsString = commands: concatStringsSep ", " ( map (command: if (isString command) then command else "${toCommandOptionsString command.options}${command.command}" ) commands ); in { ###### interface options = { security.sudo.enable = mkOption { type = types.bool; default = true; description = '' Whether to enable the sudo command, which allows non-root users to execute commands as root. ''; }; security.sudo.wheelNeedsPassword = mkOption { type = types.bool; default = true; description = '' Whether users of the wheel group must provide a password to run commands as super user via sudo. ''; }; security.sudo.configFile = mkOption { type = types.lines; # Note: if syntax errors are detected in this file, the NixOS # configuration will fail to build. description = '' This string contains the contents of the sudoers file. ''; }; security.sudo.extraRules = mkOption { description = '' Define specific rules to be in the 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 this is the case when configuration options are merged. ''; default = []; example = [ # Allow execution of any command by all users in group sudo, # requiring a password. { groups = [ "sudo" ]; commands = [ "ALL" ]; } # Allow execution of "/home/root/secret.sh" by user `backup`, `database` # and the group with GID `1006` without a password. { users = [ "backup" ]; groups = [ 1006 ]; commands = [ { command = "/home/root/secret.sh"; options = [ "SETENV" "NOPASSWD" ]; } ]; } # Allow all users of group `bar` to run two executables as user `foo` # with arguments being pre-set. { groups = [ "bar" ]; runAs = "foo"; commands = [ "/home/baz/cmd1.sh hello-sudo" { command = ''/home/baz/cmd2.sh ""''; options = [ "SETENV" ]; } ]; } ]; type = with types; listOf (submodule { options = { users = mkOption { type = with types; listOf (either string int); description = '' The usernames / UIDs this rule should apply for. ''; default = []; }; groups = mkOption { type = with types; listOf (either string int); description = '' The groups / GIDs this rule should apply for. ''; default = []; }; host = mkOption { type = types.string; default = "ALL"; description = '' For what host this rule should apply. ''; }; runAs = mkOption { type = with types; string; default = "ALL:ALL"; description = '' Under which user/group the specified command is allowed to run. A user can be specified using just the username: "foo". It is also possible to specify a user/group combination using "foo:bar" or to only allow running as a specific group with ":bar". ''; }; commands = mkOption { description = '' The commands for which the rule should apply. ''; type = with types; listOf (either string (submodule { options = { command = mkOption { type = with types; string; description = '' 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. ''; }; options = mkOption { type = with types; listOf (enum [ "NOPASSWD" "PASSWD" "NOEXEC" "EXEC" "SETENV" "NOSETENV" "LOG_INPUT" "NOLOG_INPUT" "LOG_OUTPUT" "NOLOG_OUTPUT" ]); description = '' Options for running the command. Refer to the sudo manual. ''; default = []; }; }; })); }; }; }); }; security.sudo.extraConfig = mkOption { type = types.lines; default = ""; description = '' Extra configuration text appended to sudoers. ''; }; }; ###### implementation config = mkIf cfg.enable { security.sudo.extraRules = [ { groups = [ "wheel" ]; commands = [ { command = "ALL"; options = (if cfg.wheelNeedsPassword then [ "SETENV" ] else [ "NOPASSWD" "SETENV" ]); } ]; } ]; security.sudo.configFile = '' # Don't edit this file. Set the NixOS options ‘security.sudo.configFile’ # or ‘security.sudo.extraRules’ instead. # 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: if (length rule.commands != 0) then [ (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) ] else [] ) cfg.extraRules ) )} ${cfg.extraConfig} ''; security.wrappers = { sudo.source = "${pkgs.sudo.out}/bin/sudo"; sudoedit.source = "${pkgs.sudo.out}/bin/sudoedit"; }; environment.systemPackages = [ sudo ]; security.pam.services.sudo = { sshAgentAuth = true; }; environment.etc = singleton { source = pkgs.runCommand "sudoers" { src = pkgs.writeText "sudoers-in" cfg.configFile; } # Make sure that the sudoers file is syntactically valid. # (currently disabled - NIXOS-66) "${pkgs.buildPackages.sudo}/sbin/visudo -f $src -c && cp $src $out"; target = "sudoers"; mode = "0440"; }; }; }