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