1{ config, lib, pkgs, ... }:
2let
3
4 inherit (config.security) wrapperDir wrappers;
5
6 parentWrapperDir = dirOf wrapperDir;
7
8 securityWrapper = pkgs.callPackage ./wrapper.nix {
9 inherit parentWrapperDir;
10 };
11
12 fileModeType =
13 let
14 # taken from the chmod(1) man page
15 symbolic = "[ugoa]*([-+=]([rwxXst]*|[ugo]))+|[-+=][0-7]+";
16 numeric = "[-+=]?[0-7]{0,4}";
17 mode = "((${symbolic})(,${symbolic})*)|(${numeric})";
18 in
19 lib.types.strMatching mode
20 // { description = "file mode string"; };
21
22 wrapperType = lib.types.submodule ({ name, config, ... }: {
23 options.source = lib.mkOption
24 { type = lib.types.path;
25 description = lib.mdDoc "The absolute path to the program to be wrapped.";
26 };
27 options.program = lib.mkOption
28 { type = with lib.types; nullOr str;
29 default = name;
30 description = lib.mdDoc ''
31 The name of the wrapper program. Defaults to the attribute name.
32 '';
33 };
34 options.owner = lib.mkOption
35 { type = lib.types.str;
36 description = lib.mdDoc "The owner of the wrapper program.";
37 };
38 options.group = lib.mkOption
39 { type = lib.types.str;
40 description = lib.mdDoc "The group of the wrapper program.";
41 };
42 options.permissions = lib.mkOption
43 { type = fileModeType;
44 default = "u+rx,g+x,o+x";
45 example = "a+rx";
46 description = lib.mdDoc ''
47 The permissions of the wrapper program. The format is that of a
48 symbolic or numeric file mode understood by {command}`chmod`.
49 '';
50 };
51 options.capabilities = lib.mkOption
52 { type = lib.types.commas;
53 default = "";
54 description = lib.mdDoc ''
55 A comma-separated list of capability clauses to be given to the
56 wrapper program. The format for capability clauses is described in the
57 “TEXTUAL REPRESENTATION” section of the {manpage}`cap_from_text(3)`
58 manual page. For a list of capabilities supported by the system, check
59 the {manpage}`capabilities(7)` manual page.
60
61 ::: {.note}
62 `cap_setpcap`, which is required for the wrapper
63 program to be able to raise caps into the Ambient set is NOT raised
64 to the Ambient set so that the real program cannot modify its own
65 capabilities!! This may be too restrictive for cases in which the
66 real program needs cap_setpcap but it at least leans on the side
67 security paranoid vs. too relaxed.
68 :::
69 '';
70 };
71 options.setuid = lib.mkOption
72 { type = lib.types.bool;
73 default = false;
74 description = lib.mdDoc "Whether to add the setuid bit the wrapper program.";
75 };
76 options.setgid = lib.mkOption
77 { type = lib.types.bool;
78 default = false;
79 description = lib.mdDoc "Whether to add the setgid bit the wrapper program.";
80 };
81 });
82
83 ###### Activation script for the setcap wrappers
84 mkSetcapProgram =
85 { program
86 , capabilities
87 , source
88 , owner
89 , group
90 , permissions
91 , ...
92 }:
93 ''
94 cp ${securityWrapper}/bin/security-wrapper "$wrapperDir/${program}"
95 echo -n "${source}" > "$wrapperDir/${program}.real"
96
97 # Prevent races
98 chmod 0000 "$wrapperDir/${program}"
99 chown ${owner}:${group} "$wrapperDir/${program}"
100
101 # Set desired capabilities on the file plus cap_setpcap so
102 # the wrapper program can elevate the capabilities set on
103 # its file into the Ambient set.
104 ${pkgs.libcap.out}/bin/setcap "cap_setpcap,${capabilities}" "$wrapperDir/${program}"
105
106 # Set the executable bit
107 chmod ${permissions} "$wrapperDir/${program}"
108 '';
109
110 ###### Activation script for the setuid wrappers
111 mkSetuidProgram =
112 { program
113 , source
114 , owner
115 , group
116 , setuid
117 , setgid
118 , permissions
119 , ...
120 }:
121 ''
122 cp ${securityWrapper}/bin/security-wrapper "$wrapperDir/${program}"
123 echo -n "${source}" > "$wrapperDir/${program}.real"
124
125 # Prevent races
126 chmod 0000 "$wrapperDir/${program}"
127 chown ${owner}:${group} "$wrapperDir/${program}"
128
129 chmod "u${if setuid then "+" else "-"}s,g${if setgid then "+" else "-"}s,${permissions}" "$wrapperDir/${program}"
130 '';
131
132 mkWrappedPrograms =
133 builtins.map
134 (opts:
135 if opts.capabilities != ""
136 then mkSetcapProgram opts
137 else mkSetuidProgram opts
138 ) (lib.attrValues wrappers);
139in
140{
141 imports = [
142 (lib.mkRemovedOptionModule [ "security" "setuidOwners" ] "Use security.wrappers instead")
143 (lib.mkRemovedOptionModule [ "security" "setuidPrograms" ] "Use security.wrappers instead")
144 ];
145
146 ###### interface
147
148 options = {
149 security.wrappers = lib.mkOption {
150 type = lib.types.attrsOf wrapperType;
151 default = {};
152 example = lib.literalExpression
153 ''
154 {
155 # a setuid root program
156 doas =
157 { setuid = true;
158 owner = "root";
159 group = "root";
160 source = "''${pkgs.doas}/bin/doas";
161 };
162
163 # a setgid program
164 locate =
165 { setgid = true;
166 owner = "root";
167 group = "mlocate";
168 source = "''${pkgs.locate}/bin/locate";
169 };
170
171 # a program with the CAP_NET_RAW capability
172 ping =
173 { owner = "root";
174 group = "root";
175 capabilities = "cap_net_raw+ep";
176 source = "''${pkgs.iputils.out}/bin/ping";
177 };
178 }
179 '';
180 description = lib.mdDoc ''
181 This option effectively allows adding setuid/setgid bits, capabilities,
182 changing file ownership and permissions of a program without directly
183 modifying it. This works by creating a wrapper program under the
184 {option}`security.wrapperDir` directory, which is then added to
185 the shell `PATH`.
186 '';
187 };
188
189 security.wrapperDirSize = lib.mkOption {
190 default = "50%";
191 example = "10G";
192 type = lib.types.str;
193 description = lib.mdDoc ''
194 Size limit for the /run/wrappers tmpfs. Look at mount(8), tmpfs size option,
195 for the accepted syntax. WARNING: don't set to less than 64MB.
196 '';
197 };
198
199 security.wrapperDir = lib.mkOption {
200 type = lib.types.path;
201 default = "/run/wrappers/bin";
202 internal = true;
203 description = lib.mdDoc ''
204 This option defines the path to the wrapper programs. It
205 should not be overridden.
206 '';
207 };
208 };
209
210 ###### implementation
211 config = {
212
213 assertions = lib.mapAttrsToList
214 (name: opts:
215 { assertion = opts.setuid || opts.setgid -> opts.capabilities == "";
216 message = ''
217 The security.wrappers.${name} wrapper is not valid:
218 setuid/setgid and capabilities are mutually exclusive.
219 '';
220 }
221 ) wrappers;
222
223 security.wrappers =
224 let
225 mkSetuidRoot = source:
226 { setuid = true;
227 owner = "root";
228 group = "root";
229 inherit source;
230 };
231 in
232 { # These are mount related wrappers that require the +s permission.
233 fusermount = mkSetuidRoot "${pkgs.fuse}/bin/fusermount";
234 fusermount3 = mkSetuidRoot "${pkgs.fuse3}/bin/fusermount3";
235 mount = mkSetuidRoot "${lib.getBin pkgs.util-linux}/bin/mount";
236 umount = mkSetuidRoot "${lib.getBin pkgs.util-linux}/bin/umount";
237 };
238
239 boot.specialFileSystems.${parentWrapperDir} = {
240 fsType = "tmpfs";
241 options = [ "nodev" "mode=755" "size=${config.security.wrapperDirSize}" ];
242 };
243
244 # Make sure our wrapperDir exports to the PATH env variable when
245 # initializing the shell
246 environment.extraInit = ''
247 # Wrappers override other bin directories.
248 export PATH="${wrapperDir}:$PATH"
249 '';
250
251 security.apparmor.includes."nixos/security.wrappers" = ''
252 include "${pkgs.apparmorRulesFromClosure { name="security.wrappers"; } [
253 securityWrapper
254 ]}"
255 '';
256
257 ###### wrappers activation script
258 system.activationScripts.wrappers =
259 lib.stringAfter [ "specialfs" "users" ]
260 ''
261 chmod 755 "${parentWrapperDir}"
262
263 # We want to place the tmpdirs for the wrappers to the parent dir.
264 wrapperDir=$(mktemp --directory --tmpdir="${parentWrapperDir}" wrappers.XXXXXXXXXX)
265 chmod a+rx "$wrapperDir"
266
267 ${lib.concatStringsSep "\n" mkWrappedPrograms}
268
269 if [ -L ${wrapperDir} ]; then
270 # Atomically replace the symlink
271 # See https://axialcorps.com/2013/07/03/atomically-replacing-files-and-directories/
272 old=$(readlink -f ${wrapperDir})
273 if [ -e "${wrapperDir}-tmp" ]; then
274 rm --force --recursive "${wrapperDir}-tmp"
275 fi
276 ln --symbolic --force --no-dereference "$wrapperDir" "${wrapperDir}-tmp"
277 mv --no-target-directory "${wrapperDir}-tmp" "${wrapperDir}"
278 rm --force --recursive "$old"
279 else
280 # For initial setup
281 ln --symbolic "$wrapperDir" "${wrapperDir}"
282 fi
283 '';
284
285 ###### wrappers consistency checks
286 system.checks = lib.singleton (pkgs.runCommandLocal
287 "ensure-all-wrappers-paths-exist" { }
288 ''
289 # make sure we produce output
290 mkdir -p $out
291
292 echo -n "Checking that Nix store paths of all wrapped programs exist... "
293
294 declare -A wrappers
295 ${lib.concatStringsSep "\n" (lib.mapAttrsToList (n: v:
296 "wrappers['${n}']='${v.source}'") wrappers)}
297
298 for name in "''${!wrappers[@]}"; do
299 path="''${wrappers[$name]}"
300 if [[ "$path" =~ /nix/store ]] && [ ! -e "$path" ]; then
301 test -t 1 && echo -ne '\033[1;31m'
302 echo "FAIL"
303 echo "The path $path does not exist!"
304 echo 'Please, check the value of `security.wrappers."'$name'".source`.'
305 test -t 1 && echo -ne '\033[0m'
306 exit 1
307 fi
308 done
309
310 echo "OK"
311 '');
312 };
313}