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