at 23.05-pre 9.2 kB view raw
1{ config, lib, pkgs, ... }: 2 3with lib; 4let 5 cfg = config.security.doas; 6 7 inherit (pkgs) doas; 8 9 mkUsrString = user: toString user; 10 11 mkGrpString = group: ":${toString group}"; 12 13 mkOpts = rule: concatStringsSep " " [ 14 (optionalString rule.noPass "nopass") 15 (optionalString rule.noLog "nolog") 16 (optionalString rule.persist "persist") 17 (optionalString rule.keepEnv "keepenv") 18 "setenv { SSH_AUTH_SOCK TERMINFO TERMINFO_DIRS ${concatStringsSep " " rule.setEnv} }" 19 ]; 20 21 mkArgs = rule: 22 if (isNull rule.args) then "" 23 else if (length rule.args == 0) then "args" 24 else "args ${concatStringsSep " " rule.args}"; 25 26 mkRule = rule: 27 let 28 opts = mkOpts rule; 29 30 as = optionalString (!isNull rule.runAs) "as ${rule.runAs}"; 31 32 cmd = optionalString (!isNull rule.cmd) "cmd ${rule.cmd}"; 33 34 args = mkArgs rule; 35 in 36 optionals (length cfg.extraRules > 0) [ 37 ( 38 optionalString (length rule.users > 0) 39 (map (usr: "permit ${opts} ${mkUsrString usr} ${as} ${cmd} ${args}") rule.users) 40 ) 41 ( 42 optionalString (length rule.groups > 0) 43 (map (grp: "permit ${opts} ${mkGrpString grp} ${as} ${cmd} ${args}") rule.groups) 44 ) 45 ]; 46in 47{ 48 49 ###### interface 50 51 options.security.doas = { 52 53 enable = mkOption { 54 type = with types; bool; 55 default = false; 56 description = lib.mdDoc '' 57 Whether to enable the {command}`doas` command, which allows 58 non-root users to execute commands as root. 59 ''; 60 }; 61 62 wheelNeedsPassword = mkOption { 63 type = with types; bool; 64 default = true; 65 description = lib.mdDoc '' 66 Whether users of the `wheel` group must provide a password to 67 run commands as super user via {command}`doas`. 68 ''; 69 }; 70 71 extraRules = mkOption { 72 default = []; 73 description = lib.mdDoc '' 74 Define specific rules to be set in the 75 {file}`/etc/doas.conf` file. More specific rules should 76 come after more general ones in order to yield the expected behavior. 77 You can use `mkBefore` and/or `mkAfter` to ensure 78 this is the case when configuration options are merged. 79 ''; 80 example = literalExpression '' 81 [ 82 # Allow execution of any command by any user in group doas, requiring 83 # a password and keeping any previously-defined environment variables. 84 { groups = [ "doas" ]; noPass = false; keepEnv = true; } 85 86 # Allow execution of "/home/root/secret.sh" by user `backup` OR user 87 # `database` OR any member of the group with GID `1006`, without a 88 # password. 89 { users = [ "backup" "database" ]; groups = [ 1006 ]; 90 cmd = "/home/root/secret.sh"; noPass = true; } 91 92 # Allow any member of group `bar` to run `/home/baz/cmd1.sh` as user 93 # `foo` with argument `hello-doas`. 94 { groups = [ "bar" ]; runAs = "foo"; 95 cmd = "/home/baz/cmd1.sh"; args = [ "hello-doas" ]; } 96 97 # Allow any member of group `bar` to run `/home/baz/cmd2.sh` as user 98 # `foo` with no arguments. 99 { groups = [ "bar" ]; runAs = "foo"; 100 cmd = "/home/baz/cmd2.sh"; args = [ ]; } 101 102 # Allow user `abusers` to execute "nano" and unset the value of 103 # SSH_AUTH_SOCK, override the value of ALPHA to 1, and inherit the 104 # value of BETA from the current environment. 105 { users = [ "abusers" ]; cmd = "nano"; 106 setEnv = [ "-SSH_AUTH_SOCK" "ALPHA=1" "BETA" ]; } 107 ] 108 ''; 109 type = with types; listOf ( 110 submodule { 111 options = { 112 113 noPass = mkOption { 114 type = with types; bool; 115 default = false; 116 description = lib.mdDoc '' 117 If `true`, the user is not required to enter a 118 password. 119 ''; 120 }; 121 122 noLog = mkOption { 123 type = with types; bool; 124 default = false; 125 description = lib.mdDoc '' 126 If `true`, successful executions will not be logged 127 to 128 {manpage}`syslogd(8)`. 129 ''; 130 }; 131 132 persist = mkOption { 133 type = with types; bool; 134 default = false; 135 description = lib.mdDoc '' 136 If `true`, do not ask for a password again for some 137 time after the user successfully authenticates. 138 ''; 139 }; 140 141 keepEnv = mkOption { 142 type = with types; bool; 143 default = false; 144 description = lib.mdDoc '' 145 If `true`, environment variables other than those 146 listed in 147 {manpage}`doas(1)` 148 are kept when creating the environment for the new process. 149 ''; 150 }; 151 152 setEnv = mkOption { 153 type = with types; listOf str; 154 default = []; 155 description = lib.mdDoc '' 156 Keep or set the specified variables. Variables may also be 157 removed with a leading '-' or set using 158 `variable=value`. If the first character of 159 `value` is a '$', the value to be set is taken from 160 the existing environment variable of the indicated name. This 161 option is processed after the default environment has been 162 created. 163 164 NOTE: All rules have `setenv { SSH_AUTH_SOCK }` by 165 default. To prevent `SSH_AUTH_SOCK` from being 166 inherited, add `"-SSH_AUTH_SOCK"` anywhere in this 167 list. 168 ''; 169 }; 170 171 users = mkOption { 172 type = with types; listOf (either str int); 173 default = []; 174 description = lib.mdDoc "The usernames / UIDs this rule should apply for."; 175 }; 176 177 groups = mkOption { 178 type = with types; listOf (either str int); 179 default = []; 180 description = lib.mdDoc "The groups / GIDs this rule should apply for."; 181 }; 182 183 runAs = mkOption { 184 type = with types; nullOr str; 185 default = null; 186 description = lib.mdDoc '' 187 Which user or group the specified command is allowed to run as. 188 When set to `null` (the default), all users are 189 allowed. 190 191 A user can be specified using just the username: 192 `"foo"`. It is also possible to only allow running as 193 a specific group with `":bar"`. 194 ''; 195 }; 196 197 cmd = mkOption { 198 type = with types; nullOr str; 199 default = null; 200 description = lib.mdDoc '' 201 The command the user is allowed to run. When set to 202 `null` (the default), all commands are allowed. 203 204 NOTE: It is best practice to specify absolute paths. If a 205 relative path is specified, only a restricted PATH will be 206 searched. 207 ''; 208 }; 209 210 args = mkOption { 211 type = with types; nullOr (listOf str); 212 default = null; 213 description = lib.mdDoc '' 214 Arguments that must be provided to the command. When set to 215 `[]`, the command must be run without any arguments. 216 ''; 217 }; 218 }; 219 } 220 ); 221 }; 222 223 extraConfig = mkOption { 224 type = with types; lines; 225 default = ""; 226 description = lib.mdDoc '' 227 Extra configuration text appended to {file}`doas.conf`. 228 ''; 229 }; 230 }; 231 232 233 ###### implementation 234 235 config = mkIf cfg.enable { 236 237 security.doas.extraRules = mkOrder 600 [ 238 { 239 groups = [ "wheel" ]; 240 noPass = !cfg.wheelNeedsPassword; 241 } 242 ]; 243 244 security.wrappers.doas = 245 { setuid = true; 246 owner = "root"; 247 group = "root"; 248 source = "${doas}/bin/doas"; 249 }; 250 251 environment.systemPackages = [ 252 doas 253 ]; 254 255 security.pam.services.doas = { 256 allowNullPassword = true; 257 sshAgentAuth = true; 258 }; 259 260 environment.etc."doas.conf" = { 261 source = pkgs.runCommand "doas-conf" 262 { 263 src = pkgs.writeText "doas-conf-in" '' 264 # To modify this file, set the NixOS options 265 # `security.doas.extraRules` or `security.doas.extraConfig`. To 266 # completely replace the contents of this file, use 267 # `environment.etc."doas.conf"`. 268 269 # "root" is allowed to do anything. 270 permit nopass keepenv root 271 272 # extraRules 273 ${concatStringsSep "\n" (lists.flatten (map mkRule cfg.extraRules))} 274 275 # extraConfig 276 ${cfg.extraConfig} 277 ''; 278 preferLocalBuild = true; 279 } 280 # Make sure that the doas.conf file is syntactically valid. 281 "${pkgs.buildPackages.doas}/bin/doas -C $src && cp $src $out"; 282 mode = "0440"; 283 }; 284 285 }; 286 287 meta.maintainers = with maintainers; [ cole-h ]; 288}