1{
2 config,
3 lib,
4 pkgs,
5 ...
6}:
7let
8
9 cfg = config.security.sudo;
10
11 toUserString = user: if (lib.isInt user) then "#${toString user}" else "${user}";
12 toGroupString = group: if (lib.isInt group) then "%#${toString group}" else "%${group}";
13
14 toCommandOptionsString =
15 options: "${lib.concatStringsSep ":" options}${lib.optionalString (lib.length options != 0) ":"} ";
16
17 toCommandsString =
18 commands:
19 lib.concatStringsSep ", " (
20 map (
21 command:
22 if (lib.isString command) then
23 command
24 else
25 "${toCommandOptionsString command.options}${command.command}"
26 ) commands
27 );
28
29in
30
31{
32
33 ###### interface
34
35 options.security.sudo = {
36
37 defaultOptions = lib.mkOption {
38 type = with lib.types; listOf str;
39 default = [ "SETENV" ];
40 description = ''
41 Options used for the default rules, granting `root` and the
42 `wheel` group permission to run any command as any user.
43 '';
44 };
45
46 enable = lib.mkOption {
47 type = lib.types.bool;
48 default = true;
49 description = ''
50 Whether to enable the {command}`sudo` command, which
51 allows non-root users to execute commands as root.
52 '';
53 };
54
55 package = lib.mkPackageOption pkgs "sudo" { };
56
57 wheelNeedsPassword = lib.mkOption {
58 type = lib.types.bool;
59 default = true;
60 description = ''
61 Whether users of the `wheel` group must
62 provide a password to run commands as super user via {command}`sudo`.
63 '';
64 };
65
66 execWheelOnly = lib.mkOption {
67 type = lib.types.bool;
68 default = false;
69 description = ''
70 Only allow members of the `wheel` group to execute sudo by
71 setting the executable's permissions accordingly.
72 This prevents users that are not members of `wheel` from
73 exploiting vulnerabilities in sudo such as CVE-2021-3156.
74 '';
75 };
76
77 configFile = lib.mkOption {
78 type = lib.types.lines;
79 # Note: if syntax errors are detected in this file, the NixOS
80 # configuration will fail to build.
81 description = ''
82 This string contains the contents of the
83 {file}`sudoers` file.
84 '';
85 };
86
87 extraRules = lib.mkOption {
88 description = ''
89 Define specific rules to be in the {file}`sudoers` file.
90 More specific rules should come after more general ones in order to
91 yield the expected behavior. You can use mkBefore/mkAfter to ensure
92 this is the case when configuration options are merged.
93 '';
94 default = [ ];
95 example = lib.literalExpression ''
96 [
97 # Allow execution of any command by all users in group sudo,
98 # requiring a password.
99 { groups = [ "sudo" ]; commands = [ "ALL" ]; }
100
101 # Allow execution of "/home/root/secret.sh" by user `backup`, `database`
102 # and the group with GID `1006` without a password.
103 { users = [ "backup" "database" ]; groups = [ 1006 ];
104 commands = [ { command = "/home/root/secret.sh"; options = [ "SETENV" "NOPASSWD" ]; } ]; }
105
106 # Allow all users of group `bar` to run two executables as user `foo`
107 # with arguments being pre-set.
108 { groups = [ "bar" ]; runAs = "foo";
109 commands =
110 [ "/home/baz/cmd1.sh hello-sudo"
111 { command = '''/home/baz/cmd2.sh ""'''; options = [ "SETENV" ]; } ]; }
112 ]
113 '';
114 type =
115 with lib.types;
116 listOf (submodule {
117 options = {
118 users = lib.mkOption {
119 type = with types; listOf (either str int);
120 description = ''
121 The usernames / UIDs this rule should apply for.
122 '';
123 default = [ ];
124 };
125
126 groups = lib.mkOption {
127 type = with types; listOf (either str int);
128 description = ''
129 The groups / GIDs this rule should apply for.
130 '';
131 default = [ ];
132 };
133
134 host = lib.mkOption {
135 type = types.str;
136 default = "ALL";
137 description = ''
138 For what host this rule should apply.
139 '';
140 };
141
142 runAs = lib.mkOption {
143 type = with types; str;
144 default = "ALL:ALL";
145 description = ''
146 Under which user/group the specified command is allowed to run.
147
148 A user can be specified using just the username: `"foo"`.
149 It is also possible to specify a user/group combination using `"foo:bar"`
150 or to only allow running as a specific group with `":bar"`.
151 '';
152 };
153
154 commands = lib.mkOption {
155 description = ''
156 The commands for which the rule should apply.
157 '';
158 type =
159 with types;
160 listOf (
161 either str (submodule {
162
163 options = {
164 command = lib.mkOption {
165 type = with types; str;
166 description = ''
167 A command being either just a path to a binary to allow any arguments,
168 the full command with arguments pre-set or with `""` used as the argument,
169 not allowing arguments to the command at all.
170 '';
171 };
172
173 options = lib.mkOption {
174 type =
175 with types;
176 listOf (enum [
177 "NOPASSWD"
178 "PASSWD"
179 "NOEXEC"
180 "EXEC"
181 "SETENV"
182 "NOSETENV"
183 "LOG_INPUT"
184 "NOLOG_INPUT"
185 "LOG_OUTPUT"
186 "NOLOG_OUTPUT"
187 "MAIL"
188 "NOMAIL"
189 "FOLLOW"
190 "NOFLLOW"
191 "INTERCEPT"
192 "NOINTERCEPT"
193 ]);
194 description = ''
195 Options for running the command. Refer to the [sudo manual](https://www.sudo.ws/docs/man/1.9.15/sudoers.man/#Tag_Spec).
196 '';
197 default = [ ];
198 };
199 };
200
201 })
202 );
203 };
204 };
205 });
206 };
207
208 extraConfig = lib.mkOption {
209 type = lib.types.lines;
210 default = "";
211 description = ''
212 Extra configuration text appended to {file}`sudoers`.
213 '';
214 };
215 };
216
217 ###### implementation
218
219 config = lib.mkIf cfg.enable {
220 assertions = [
221 {
222 assertion = cfg.package.pname != "sudo-rs";
223 message = ''
224 NixOS' `sudo` module does not support `sudo-rs`; see `security.sudo-rs` instead.
225 '';
226 }
227 ];
228
229 security.sudo.extraRules =
230 let
231 defaultRule =
232 {
233 users ? [ ],
234 groups ? [ ],
235 opts ? [ ],
236 }:
237 [
238 {
239 inherit users groups;
240 commands = [
241 {
242 command = "ALL";
243 options = opts ++ cfg.defaultOptions;
244 }
245 ];
246 }
247 ];
248 in
249 lib.mkMerge [
250 # This is ordered before users' `mkBefore` rules,
251 # so as not to introduce unexpected changes.
252 (lib.mkOrder 400 (defaultRule {
253 users = [ "root" ];
254 }))
255
256 # This is ordered to show before (most) other rules, but
257 # late-enough for a user to `mkBefore` it.
258 (lib.mkOrder 600 (defaultRule {
259 groups = [ "wheel" ];
260 opts = (lib.optional (!cfg.wheelNeedsPassword) "NOPASSWD");
261 }))
262 ];
263
264 security.sudo.configFile = lib.concatStringsSep "\n" (
265 lib.filter (s: s != "") [
266 ''
267 # Don't edit this file. Set the NixOS options ‘security.sudo.configFile’
268 # or ‘security.sudo.extraRules’ instead.
269 ''
270 (lib.pipe cfg.extraRules [
271 (lib.filter (rule: lib.length rule.commands != 0))
272 (map (rule: [
273 (map (
274 user: "${toUserString user} ${rule.host}=(${rule.runAs}) ${toCommandsString rule.commands}"
275 ) rule.users)
276 (map (
277 group: "${toGroupString group} ${rule.host}=(${rule.runAs}) ${toCommandsString rule.commands}"
278 ) rule.groups)
279 ]))
280 lib.flatten
281 (lib.concatStringsSep "\n")
282 ])
283 "\n"
284 (lib.optionalString (cfg.extraConfig != "") ''
285 # extraConfig
286 ${cfg.extraConfig}
287 '')
288 ]
289 );
290
291 security.wrappers =
292 let
293 owner = "root";
294 group = if cfg.execWheelOnly then "wheel" else "root";
295 setuid = true;
296 permissions = if cfg.execWheelOnly then "u+rx,g+x" else "u+rx,g+x,o+x";
297 in
298 {
299 sudo = {
300 source = "${cfg.package.out}/bin/sudo";
301 inherit
302 owner
303 group
304 setuid
305 permissions
306 ;
307 };
308 sudoedit = {
309 source = "${cfg.package.out}/bin/sudoedit";
310 inherit
311 owner
312 group
313 setuid
314 permissions
315 ;
316 };
317 };
318
319 environment.systemPackages = [ cfg.package ];
320
321 security.pam.services.sudo = {
322 sshAgentAuth = true;
323 usshAuth = true;
324 };
325
326 environment.etc.sudoers = {
327 source =
328 pkgs.runCommand "sudoers"
329 {
330 src = pkgs.writeText "sudoers-in" cfg.configFile;
331 preferLocalBuild = true;
332 }
333 # Make sure that the sudoers file is syntactically valid.
334 # (currently disabled - NIXOS-66)
335 "${pkgs.buildPackages.sudo}/sbin/visudo -f $src -c && cp $src $out";
336 mode = "0440";
337 };
338
339 };
340
341}