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 = ''
57 Whether to enable the <command>doas</command> 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 <code>wheel</code> group must provide a password to
67 run commands as super user via <command>doas</command>.
68 '';
69 };
70
71 extraRules = mkOption {
72 default = [];
73 description = ''
74 Define specific rules to be set in the
75 <filename>/etc/doas.conf</filename> file. More specific rules should
76 come after more general ones in order to yield the expected behavior.
77 You can use <code>mkBefore</code> and/or <code>mkAfter</code> 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 = ''
117 If <code>true</code>, 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 = ''
126 If <code>true</code>, successful executions will not be logged
127 to
128 <citerefentry><refentrytitle>syslogd</refentrytitle><manvolnum>8</manvolnum></citerefentry>.
129 '';
130 };
131
132 persist = mkOption {
133 type = with types; bool;
134 default = false;
135 description = ''
136 If <code>true</code>, 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 = ''
145 If <code>true</code>, environment variables other than those
146 listed in
147 <citerefentry><refentrytitle>doas</refentrytitle><manvolnum>1</manvolnum></citerefentry>
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 = ''
156 Keep or set the specified variables. Variables may also be
157 removed with a leading '-' or set using
158 <code>variable=value</code>. If the first character of
159 <code>value</code> 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 <code>setenv { SSH_AUTH_SOCK }</code> by
165 default. To prevent <code>SSH_AUTH_SOCK</code> from being
166 inherited, add <code>"-SSH_AUTH_SOCK"</code> anywhere in this
167 list.
168 '';
169 };
170
171 users = mkOption {
172 type = with types; listOf (either str int);
173 default = [];
174 description = "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 = "The groups / GIDs this rule should apply for.";
181 };
182
183 runAs = mkOption {
184 type = with types; nullOr str;
185 default = null;
186 description = ''
187 Which user or group the specified command is allowed to run as.
188 When set to <code>null</code> (the default), all users are
189 allowed.
190
191 A user can be specified using just the username:
192 <code>"foo"</code>. It is also possible to only allow running as
193 a specific group with <code>":bar"</code>.
194 '';
195 };
196
197 cmd = mkOption {
198 type = with types; nullOr str;
199 default = null;
200 description = ''
201 The command the user is allowed to run. When set to
202 <code>null</code> (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 = ''
214 Arguments that must be provided to the command. When set to
215 <code>[]</code>, 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 = ''
227 Extra configuration text appended to <filename>doas.conf</filename>.
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}