···280 <itemizedlist>
281 <listitem>
282 <para>
0000000000283 The <literal>paperless</literal> module and package have been
284 removed. All users should migrate to the successor
285 <literal>paperless-ng</literal> instead. The Paperless project
···280 <itemizedlist>
281 <listitem>
282 <para>
283+ The <literal>security.wrappers</literal> option now requires
284+ to always specify an owner, group and whether the
285+ setuid/setgid bit should be set. This is motivated by the fact
286+ that before NixOS 21.11, specifying either setuid or setgid
287+ but not owner/group resulted in wrappers owned by
288+ nobody/nogroup, which is unsafe.
289+ </para>
290+ </listitem>
291+ <listitem>
292+ <para>
293 The <literal>paperless</literal> module and package have been
294 removed. All users should migrate to the successor
295 <literal>paperless-ng</literal> instead. The Paperless project
+2
nixos/doc/manual/release-notes/rl-2111.section.md
···8889## Backward Incompatibilities {#sec-release-21.11-incompatibilities}
90009192- The `paperless` module and package have been removed. All users should migrate to the
93 successor `paperless-ng` instead. The Paperless project [has been
···8889## Backward Incompatibilities {#sec-release-21.11-incompatibilities}
9091+- The `security.wrappers` option now requires to always specify an owner, group and whether the setuid/setgid bit should be set.
92+ This is motivated by the fact that before NixOS 21.11, specifying either setuid or setgid but not owner/group resulted in wrappers owned by nobody/nogroup, which is unsafe.
9394- The `paperless` module and package have been removed. All users should migrate to the
95 successor `paperless-ng` instead. The Paperless project [has been
···56 parentWrapperDir = dirOf wrapperDir;
78- programs =
9- (lib.mapAttrsToList
10- (n: v: (if v ? program then v else v // {program=n;}))
11- wrappers);
12-13 securityWrapper = pkgs.callPackage ./wrapper.nix {
14 inherit parentWrapperDir;
15 };
16000000000000000000000000000000000000000000000000000000000000000000000000017 ###### Activation script for the setcap wrappers
18 mkSetcapProgram =
19 { program
20 , capabilities
21 , source
22- , owner ? "nobody"
23- , group ? "nogroup"
24- , permissions ? "u+rx,g+x,o+x"
25 , ...
26 }:
27 assert (lib.versionAtLeast (lib.getVersion config.boot.kernelPackages.kernel) "4.3");
28 ''
29- cp ${securityWrapper}/bin/security-wrapper $wrapperDir/${program}
30- echo -n "${source}" > $wrapperDir/${program}.real
3132 # Prevent races
33- chmod 0000 $wrapperDir/${program}
34- chown ${owner}.${group} $wrapperDir/${program}
3536 # Set desired capabilities on the file plus cap_setpcap so
37 # the wrapper program can elevate the capabilities set on
38 # its file into the Ambient set.
39- ${pkgs.libcap.out}/bin/setcap "cap_setpcap,${capabilities}" $wrapperDir/${program}
4041 # Set the executable bit
42- chmod ${permissions} $wrapperDir/${program}
43 '';
4445 ###### Activation script for the setuid wrappers
46 mkSetuidProgram =
47 { program
48 , source
49- , owner ? "nobody"
50- , group ? "nogroup"
51- , setuid ? false
52- , setgid ? false
53- , permissions ? "u+rx,g+x,o+x"
54 , ...
55 }:
56 ''
57- cp ${securityWrapper}/bin/security-wrapper $wrapperDir/${program}
58- echo -n "${source}" > $wrapperDir/${program}.real
5960 # Prevent races
61- chmod 0000 $wrapperDir/${program}
62- chown ${owner}.${group} $wrapperDir/${program}
6364- chmod "u${if setuid then "+" else "-"}s,g${if setgid then "+" else "-"}s,${permissions}" $wrapperDir/${program}
65 '';
6667 mkWrappedPrograms =
68 builtins.map
69- (s: if (s ? capabilities)
70- then mkSetcapProgram
71- ({ owner = "root";
72- group = "root";
73- } // s)
74- else if
75- (s ? setuid && s.setuid) ||
76- (s ? setgid && s.setgid) ||
77- (s ? permissions)
78- then mkSetuidProgram s
79- else mkSetuidProgram
80- ({ owner = "root";
81- group = "root";
82- setuid = true;
83- setgid = false;
84- permissions = "u+rx,g+x,o+x";
85- } // s)
86- ) programs;
87in
88{
89 imports = [
···9596 options = {
97 security.wrappers = lib.mkOption {
98- type = lib.types.attrs;
99 default = {};
100 example = lib.literalExample
101 ''
102- { sendmail.source = "/nix/store/.../bin/sendmail";
103- ping = {
104- source = "${pkgs.iputils.out}/bin/ping";
105- owner = "nobody";
106- group = "nogroup";
107- capabilities = "cap_net_raw+ep";
108- };
00000000000000000109 }
110 '';
111 description = ''
112- This option allows the ownership and permissions on the setuid
113- wrappers for specific programs to be overridden from the
114- default (setuid root, but not setgid root).
115-116- <note>
117- <para>The sub-attribute <literal>source</literal> is mandatory,
118- it must be the absolute path to the program to be wrapped.
119- </para>
120-121- <para>The sub-attribute <literal>program</literal> is optional and
122- can give the wrapper program a new name. The default name is the same
123- as the attribute name itself.</para>
124-125- <para>Additionally, this option can set capabilities on a
126- wrapper program that propagates those capabilities down to the
127- wrapped, real program.</para>
128-129- <para>NOTE: cap_setpcap, which is required for the wrapper
130- program to be able to raise caps into the Ambient set is NOT
131- raised to the Ambient set so that the real program cannot
132- modify its own capabilities!! This may be too restrictive for
133- cases in which the real program needs cap_setpcap but it at
134- least leans on the side security paranoid vs. too
135- relaxed.</para>
136- </note>
137 '';
138 };
139···151 ###### implementation
152 config = {
153154- security.wrappers = {
155- # These are mount related wrappers that require the +s permission.
156- fusermount.source = "${pkgs.fuse}/bin/fusermount";
157- fusermount3.source = "${pkgs.fuse3}/bin/fusermount3";
158- mount.source = "${lib.getBin pkgs.util-linux}/bin/mount";
159- umount.source = "${lib.getBin pkgs.util-linux}/bin/umount";
160- };
000000000000000000161162 boot.specialFileSystems.${parentWrapperDir} = {
163 fsType = "tmpfs";
···179 ]}"
180 '';
181182- ###### setcap activation script
183 system.activationScripts.wrappers =
184 lib.stringAfter [ "specialfs" "users" ]
185 ''
186- # Look in the system path and in the default profile for
187- # programs to be wrapped.
188- WRAPPER_PATH=${config.system.path}/bin:${config.system.path}/sbin
189-190 chmod 755 "${parentWrapperDir}"
191192 # We want to place the tmpdirs for the wrappers to the parent dir.
193 wrapperDir=$(mktemp --directory --tmpdir="${parentWrapperDir}" wrappers.XXXXXXXXXX)
194- chmod a+rx $wrapperDir
195196 ${lib.concatStringsSep "\n" mkWrappedPrograms}
197···199 # Atomically replace the symlink
200 # See https://axialcorps.com/2013/07/03/atomically-replacing-files-and-directories/
201 old=$(readlink -f ${wrapperDir})
202- if [ -e ${wrapperDir}-tmp ]; then
203- rm --force --recursive ${wrapperDir}-tmp
204 fi
205- ln --symbolic --force --no-dereference $wrapperDir ${wrapperDir}-tmp
206- mv --no-target-directory ${wrapperDir}-tmp ${wrapperDir}
207- rm --force --recursive $old
208 else
209 # For initial setup
210- ln --symbolic $wrapperDir ${wrapperDir}
211 fi
212 '';
0000000000000000000000000000213 };
214}
···56 parentWrapperDir = dirOf wrapperDir;
7000008 securityWrapper = pkgs.callPackage ./wrapper.nix {
9 inherit parentWrapperDir;
10 };
1112+ 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 = "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 = ''
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 = "The owner of the wrapper program.";
37+ };
38+ options.group = lib.mkOption
39+ { type = lib.types.str;
40+ description = "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 = ''
47+ The permissions of the wrapper program. The format is that of a
48+ symbolic or numeric file mode understood by <command>chmod</command>.
49+ '';
50+ };
51+ options.capabilities = lib.mkOption
52+ { type = lib.types.commas;
53+ default = "";
54+ description = ''
55+ A comma-separated list of capabilities to be given to the wrapper
56+ program. For capabilities supported by the system check the
57+ <citerefentry>
58+ <refentrytitle>capabilities</refentrytitle>
59+ <manvolnum>7</manvolnum>
60+ </citerefentry>
61+ manual page.
62+63+ <note><para>
64+ <literal>cap_setpcap</literal>, which is required for the wrapper
65+ program to be able to raise caps into the Ambient set is NOT raised
66+ to the Ambient set so that the real program cannot modify its own
67+ capabilities!! This may be too restrictive for cases in which the
68+ real program needs cap_setpcap but it at least leans on the side
69+ security paranoid vs. too relaxed.
70+ </para></note>
71+ '';
72+ };
73+ options.setuid = lib.mkOption
74+ { type = lib.types.bool;
75+ default = false;
76+ description = "Whether to add the setuid bit the wrapper program.";
77+ };
78+ options.setgid = lib.mkOption
79+ { type = lib.types.bool;
80+ default = false;
81+ description = "Whether to add the setgid bit the wrapper program.";
82+ };
83+ });
84+85 ###### Activation script for the setcap wrappers
86 mkSetcapProgram =
87 { program
88 , capabilities
89 , source
90+ , owner
91+ , group
92+ , permissions
93 , ...
94 }:
95 assert (lib.versionAtLeast (lib.getVersion config.boot.kernelPackages.kernel) "4.3");
96 ''
97+ cp ${securityWrapper}/bin/security-wrapper "$wrapperDir/${program}"
98+ echo -n "${source}" > "$wrapperDir/${program}.real"
99100 # Prevent races
101+ chmod 0000 "$wrapperDir/${program}"
102+ chown ${owner}.${group} "$wrapperDir/${program}"
103104 # Set desired capabilities on the file plus cap_setpcap so
105 # the wrapper program can elevate the capabilities set on
106 # its file into the Ambient set.
107+ ${pkgs.libcap.out}/bin/setcap "cap_setpcap,${capabilities}" "$wrapperDir/${program}"
108109 # Set the executable bit
110+ chmod ${permissions} "$wrapperDir/${program}"
111 '';
112113 ###### Activation script for the setuid wrappers
114 mkSetuidProgram =
115 { program
116 , source
117+ , owner
118+ , group
119+ , setuid
120+ , setgid
121+ , permissions
122 , ...
123 }:
124 ''
125+ cp ${securityWrapper}/bin/security-wrapper "$wrapperDir/${program}"
126+ echo -n "${source}" > "$wrapperDir/${program}.real"
127128 # Prevent races
129+ chmod 0000 "$wrapperDir/${program}"
130+ chown ${owner}.${group} "$wrapperDir/${program}"
131132+ chmod "u${if setuid then "+" else "-"}s,g${if setgid then "+" else "-"}s,${permissions}" "$wrapperDir/${program}"
133 '';
134135 mkWrappedPrograms =
136 builtins.map
137+ (opts:
138+ if opts.capabilities != ""
139+ then mkSetcapProgram opts
140+ else mkSetuidProgram opts
141+ ) (lib.attrValues wrappers);
0000000000000142in
143{
144 imports = [
···150151 options = {
152 security.wrappers = lib.mkOption {
153+ type = lib.types.attrsOf wrapperType;
154 default = {};
155 example = lib.literalExample
156 ''
157+ {
158+ # a setuid root program
159+ doas =
160+ { setuid = true;
161+ owner = "root";
162+ group = "root";
163+ source = "''${pkgs.doas}/bin/doas";
164+ };
165+166+ # a setgid program
167+ locate =
168+ { setgid = true;
169+ owner = "root";
170+ group = "mlocate";
171+ source = "''${pkgs.locate}/bin/locate";
172+ };
173+174+ # a program with the CAP_NET_RAW capability
175+ ping =
176+ { owner = "root";
177+ group = "root";
178+ capabilities = "cap_net_raw+ep";
179+ source = "''${pkgs.iputils.out}/bin/ping";
180+ };
181 }
182 '';
183 description = ''
184+ This option effectively allows adding setuid/setgid bits, capabilities,
185+ changing file ownership and permissions of a program without directly
186+ modifying it. This works by creating a wrapper program under the
187+ <option>security.wrapperDir</option> directory, which is then added to
188+ the shell <literal>PATH</literal>.
00000000000000000000189 '';
190 };
191···203 ###### implementation
204 config = {
205206+ assertions = lib.mapAttrsToList
207+ (name: opts:
208+ { assertion = opts.setuid || opts.setgid -> opts.capabilities == "";
209+ message = ''
210+ The security.wrappers.${name} wrapper is not valid:
211+ setuid/setgid and capabilities are mutually exclusive.
212+ '';
213+ }
214+ ) wrappers;
215+216+ security.wrappers =
217+ let
218+ mkSetuidRoot = source:
219+ { setuid = true;
220+ owner = "root";
221+ group = "root";
222+ inherit source;
223+ };
224+ in
225+ { # These are mount related wrappers that require the +s permission.
226+ fusermount = mkSetuidRoot "${pkgs.fuse}/bin/fusermount";
227+ fusermount3 = mkSetuidRoot "${pkgs.fuse3}/bin/fusermount3";
228+ mount = mkSetuidRoot "${lib.getBin pkgs.util-linux}/bin/mount";
229+ umount = mkSetuidRoot "${lib.getBin pkgs.util-linux}/bin/umount";
230+ };
231232 boot.specialFileSystems.${parentWrapperDir} = {
233 fsType = "tmpfs";
···249 ]}"
250 '';
251252+ ###### wrappers activation script
253 system.activationScripts.wrappers =
254 lib.stringAfter [ "specialfs" "users" ]
255 ''
0000256 chmod 755 "${parentWrapperDir}"
257258 # We want to place the tmpdirs for the wrappers to the parent dir.
259 wrapperDir=$(mktemp --directory --tmpdir="${parentWrapperDir}" wrappers.XXXXXXXXXX)
260+ chmod a+rx "$wrapperDir"
261262 ${lib.concatStringsSep "\n" mkWrappedPrograms}
263···265 # Atomically replace the symlink
266 # See https://axialcorps.com/2013/07/03/atomically-replacing-files-and-directories/
267 old=$(readlink -f ${wrapperDir})
268+ if [ -e "${wrapperDir}-tmp" ]; then
269+ rm --force --recursive "${wrapperDir}-tmp"
270 fi
271+ ln --symbolic --force --no-dereference "$wrapperDir" "${wrapperDir}-tmp"
272+ mv --no-target-directory "${wrapperDir}-tmp" "${wrapperDir}"
273+ rm --force --recursive "$old"
274 else
275 # For initial setup
276+ ln --symbolic "$wrapperDir" "${wrapperDir}"
277 fi
278 '';
279+280+ ###### wrappers consistency checks
281+ system.extraDependencies = lib.singleton (pkgs.runCommandLocal
282+ "ensure-all-wrappers-paths-exist" { }
283+ ''
284+ # make sure we produce output
285+ mkdir -p $out
286+287+ echo -n "Checking that Nix store paths of all wrapped programs exist... "
288+289+ declare -A wrappers
290+ ${lib.concatStringsSep "\n" (lib.mapAttrsToList (n: v:
291+ "wrappers['${n}']='${v.source}'") wrappers)}
292+293+ for name in "''${!wrappers[@]}"; do
294+ path="''${wrappers[$name]}"
295+ if [[ "$path" =~ /nix/store ]] && [ ! -e "$path" ]; then
296+ test -t 1 && echo -ne '\033[1;31m'
297+ echo "FAIL"
298+ echo "The path $path does not exist!"
299+ echo 'Please, check the value of `security.wrappers."'$name'".source`.'
300+ test -t 1 && echo -ne '\033[0m'
301+ exit 1
302+ fi
303+ done
304+305+ echo "OK"
306+ '');
307 };
308}