at 24.11-pre 9.3 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 (rule.args == null) 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 (rule.runAs != null) "as ${rule.runAs}"; 31 32 cmd = optionalString (rule.cmd != null) "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 = '' 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 = '' 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 = '' 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. Be aware that 79 this option cannot be used to override the behaviour allowing 80 passwordless operation for root. 81 ''; 82 example = literalExpression '' 83 [ 84 # Allow execution of any command by any user in group doas, requiring 85 # a password and keeping any previously-defined environment variables. 86 { groups = [ "doas" ]; noPass = false; keepEnv = true; } 87 88 # Allow execution of "/home/root/secret.sh" by user `backup` OR user 89 # `database` OR any member of the group with GID `1006`, without a 90 # password. 91 { users = [ "backup" "database" ]; groups = [ 1006 ]; 92 cmd = "/home/root/secret.sh"; noPass = true; } 93 94 # Allow any member of group `bar` to run `/home/baz/cmd1.sh` as user 95 # `foo` with argument `hello-doas`. 96 { groups = [ "bar" ]; runAs = "foo"; 97 cmd = "/home/baz/cmd1.sh"; args = [ "hello-doas" ]; } 98 99 # Allow any member of group `bar` to run `/home/baz/cmd2.sh` as user 100 # `foo` with no arguments. 101 { groups = [ "bar" ]; runAs = "foo"; 102 cmd = "/home/baz/cmd2.sh"; args = [ ]; } 103 104 # Allow user `abusers` to execute "nano" and unset the value of 105 # SSH_AUTH_SOCK, override the value of ALPHA to 1, and inherit the 106 # value of BETA from the current environment. 107 { users = [ "abusers" ]; cmd = "nano"; 108 setEnv = [ "-SSH_AUTH_SOCK" "ALPHA=1" "BETA" ]; } 109 ] 110 ''; 111 type = with types; listOf ( 112 submodule { 113 options = { 114 115 noPass = mkOption { 116 type = with types; bool; 117 default = false; 118 description = '' 119 If `true`, the user is not required to enter a 120 password. 121 ''; 122 }; 123 124 noLog = mkOption { 125 type = with types; bool; 126 default = false; 127 description = '' 128 If `true`, successful executions will not be logged 129 to 130 {manpage}`syslogd(8)`. 131 ''; 132 }; 133 134 persist = mkOption { 135 type = with types; bool; 136 default = false; 137 description = '' 138 If `true`, do not ask for a password again for some 139 time after the user successfully authenticates. 140 ''; 141 }; 142 143 keepEnv = mkOption { 144 type = with types; bool; 145 default = false; 146 description = '' 147 If `true`, environment variables other than those 148 listed in 149 {manpage}`doas(1)` 150 are kept when creating the environment for the new process. 151 ''; 152 }; 153 154 setEnv = mkOption { 155 type = with types; listOf str; 156 default = []; 157 description = '' 158 Keep or set the specified variables. Variables may also be 159 removed with a leading '-' or set using 160 `variable=value`. If the first character of 161 `value` is a '$', the value to be set is taken from 162 the existing environment variable of the indicated name. This 163 option is processed after the default environment has been 164 created. 165 166 NOTE: All rules have `setenv { SSH_AUTH_SOCK }` by 167 default. To prevent `SSH_AUTH_SOCK` from being 168 inherited, add `"-SSH_AUTH_SOCK"` anywhere in this 169 list. 170 ''; 171 }; 172 173 users = mkOption { 174 type = with types; listOf (either str int); 175 default = []; 176 description = "The usernames / UIDs this rule should apply for."; 177 }; 178 179 groups = mkOption { 180 type = with types; listOf (either str int); 181 default = []; 182 description = "The groups / GIDs this rule should apply for."; 183 }; 184 185 runAs = mkOption { 186 type = with types; nullOr str; 187 default = null; 188 description = '' 189 Which user or group the specified command is allowed to run as. 190 When set to `null` (the default), all users are 191 allowed. 192 193 A user can be specified using just the username: 194 `"foo"`. It is also possible to only allow running as 195 a specific group with `":bar"`. 196 ''; 197 }; 198 199 cmd = mkOption { 200 type = with types; nullOr str; 201 default = null; 202 description = '' 203 The command the user is allowed to run. When set to 204 `null` (the default), all commands are allowed. 205 206 NOTE: It is best practice to specify absolute paths. If a 207 relative path is specified, only a restricted PATH will be 208 searched. 209 ''; 210 }; 211 212 args = mkOption { 213 type = with types; nullOr (listOf str); 214 default = null; 215 description = '' 216 Arguments that must be provided to the command. When set to 217 `[]`, the command must be run without any arguments. 218 ''; 219 }; 220 }; 221 } 222 ); 223 }; 224 225 extraConfig = mkOption { 226 type = with types; lines; 227 default = ""; 228 description = '' 229 Extra configuration text appended to {file}`doas.conf`. Be aware that 230 this option cannot be used to override the behaviour allowing 231 passwordless operation for root. 232 ''; 233 }; 234 }; 235 236 237 ###### implementation 238 239 config = mkIf cfg.enable { 240 241 security.doas.extraRules = mkOrder 600 [ 242 { 243 groups = [ "wheel" ]; 244 noPass = !cfg.wheelNeedsPassword; 245 } 246 ]; 247 248 security.wrappers.doas = 249 { setuid = true; 250 owner = "root"; 251 group = "root"; 252 source = "${doas}/bin/doas"; 253 }; 254 255 environment.systemPackages = [ 256 doas 257 ]; 258 259 security.pam.services.doas = { 260 allowNullPassword = true; 261 sshAgentAuth = true; 262 }; 263 264 environment.etc."doas.conf" = { 265 source = pkgs.runCommand "doas-conf" 266 { 267 src = pkgs.writeText "doas-conf-in" '' 268 # To modify this file, set the NixOS options 269 # `security.doas.extraRules` or `security.doas.extraConfig`. To 270 # completely replace the contents of this file, use 271 # `environment.etc."doas.conf"`. 272 273 # extraRules 274 ${concatStringsSep "\n" (lists.flatten (map mkRule cfg.extraRules))} 275 276 # extraConfig 277 ${cfg.extraConfig} 278 279 # "root" is allowed to do anything. 280 permit nopass keepenv root 281 ''; 282 preferLocalBuild = true; 283 } 284 # Make sure that the doas.conf file is syntactically valid. 285 "${pkgs.buildPackages.doas}/bin/doas -C $src && cp $src $out"; 286 mode = "0440"; 287 }; 288 289 }; 290 291 meta.maintainers = with maintainers; [ cole-h ]; 292}