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}