···280280 <itemizedlist>
281281 <listitem>
282282 <para>
283283+ The <literal>security.wrappers</literal> option now requires
284284+ to always specify an owner, group and whether the
285285+ setuid/setgid bit should be set. This is motivated by the fact
286286+ that before NixOS 21.11, specifying either setuid or setgid
287287+ but not owner/group resulted in wrappers owned by
288288+ nobody/nogroup, which is unsafe.
289289+ </para>
290290+ </listitem>
291291+ <listitem>
292292+ <para>
283293 The <literal>paperless</literal> module and package have been
284294 removed. All users should migrate to the successor
285295 <literal>paperless-ng</literal> instead. The Paperless project
+2
nixos/doc/manual/release-notes/rl-2111.section.md
···88888989## Backward Incompatibilities {#sec-release-21.11-incompatibilities}
90909191+- The `security.wrappers` option now requires to always specify an owner, group and whether the setuid/setgid bit should be set.
9292+ 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.
91939294- The `paperless` module and package have been removed. All users should migrate to the
9395 successor `paperless-ng` instead. The Paperless project [has been
···5566 parentWrapperDir = dirOf wrapperDir;
7788- programs =
99- (lib.mapAttrsToList
1010- (n: v: (if v ? program then v else v // {program=n;}))
1111- wrappers);
1212-138 securityWrapper = pkgs.callPackage ./wrapper.nix {
149 inherit parentWrapperDir;
1510 };
16111212+ fileModeType =
1313+ let
1414+ # taken from the chmod(1) man page
1515+ symbolic = "[ugoa]*([-+=]([rwxXst]*|[ugo]))+|[-+=][0-7]+";
1616+ numeric = "[-+=]?[0-7]{0,4}";
1717+ mode = "((${symbolic})(,${symbolic})*)|(${numeric})";
1818+ in
1919+ lib.types.strMatching mode
2020+ // { description = "file mode string"; };
2121+2222+ wrapperType = lib.types.submodule ({ name, config, ... }: {
2323+ options.source = lib.mkOption
2424+ { type = lib.types.path;
2525+ description = "The absolute path to the program to be wrapped.";
2626+ };
2727+ options.program = lib.mkOption
2828+ { type = with lib.types; nullOr str;
2929+ default = name;
3030+ description = ''
3131+ The name of the wrapper program. Defaults to the attribute name.
3232+ '';
3333+ };
3434+ options.owner = lib.mkOption
3535+ { type = lib.types.str;
3636+ description = "The owner of the wrapper program.";
3737+ };
3838+ options.group = lib.mkOption
3939+ { type = lib.types.str;
4040+ description = "The group of the wrapper program.";
4141+ };
4242+ options.permissions = lib.mkOption
4343+ { type = fileModeType;
4444+ default = "u+rx,g+x,o+x";
4545+ example = "a+rx";
4646+ description = ''
4747+ The permissions of the wrapper program. The format is that of a
4848+ symbolic or numeric file mode understood by <command>chmod</command>.
4949+ '';
5050+ };
5151+ options.capabilities = lib.mkOption
5252+ { type = lib.types.commas;
5353+ default = "";
5454+ description = ''
5555+ A comma-separated list of capabilities to be given to the wrapper
5656+ program. For capabilities supported by the system check the
5757+ <citerefentry>
5858+ <refentrytitle>capabilities</refentrytitle>
5959+ <manvolnum>7</manvolnum>
6060+ </citerefentry>
6161+ manual page.
6262+6363+ <note><para>
6464+ <literal>cap_setpcap</literal>, which is required for the wrapper
6565+ program to be able to raise caps into the Ambient set is NOT raised
6666+ to the Ambient set so that the real program cannot modify its own
6767+ capabilities!! This may be too restrictive for cases in which the
6868+ real program needs cap_setpcap but it at least leans on the side
6969+ security paranoid vs. too relaxed.
7070+ </para></note>
7171+ '';
7272+ };
7373+ options.setuid = lib.mkOption
7474+ { type = lib.types.bool;
7575+ default = false;
7676+ description = "Whether to add the setuid bit the wrapper program.";
7777+ };
7878+ options.setgid = lib.mkOption
7979+ { type = lib.types.bool;
8080+ default = false;
8181+ description = "Whether to add the setgid bit the wrapper program.";
8282+ };
8383+ });
8484+1785 ###### Activation script for the setcap wrappers
1886 mkSetcapProgram =
1987 { program
2088 , capabilities
2189 , source
2222- , owner ? "nobody"
2323- , group ? "nogroup"
2424- , permissions ? "u+rx,g+x,o+x"
9090+ , owner
9191+ , group
9292+ , permissions
2593 , ...
2694 }:
2795 assert (lib.versionAtLeast (lib.getVersion config.boot.kernelPackages.kernel) "4.3");
2896 ''
2929- cp ${securityWrapper}/bin/security-wrapper $wrapperDir/${program}
3030- echo -n "${source}" > $wrapperDir/${program}.real
9797+ cp ${securityWrapper}/bin/security-wrapper "$wrapperDir/${program}"
9898+ echo -n "${source}" > "$wrapperDir/${program}.real"
319932100 # Prevent races
3333- chmod 0000 $wrapperDir/${program}
3434- chown ${owner}.${group} $wrapperDir/${program}
101101+ chmod 0000 "$wrapperDir/${program}"
102102+ chown ${owner}.${group} "$wrapperDir/${program}"
3510336104 # Set desired capabilities on the file plus cap_setpcap so
37105 # the wrapper program can elevate the capabilities set on
38106 # its file into the Ambient set.
3939- ${pkgs.libcap.out}/bin/setcap "cap_setpcap,${capabilities}" $wrapperDir/${program}
107107+ ${pkgs.libcap.out}/bin/setcap "cap_setpcap,${capabilities}" "$wrapperDir/${program}"
4010841109 # Set the executable bit
4242- chmod ${permissions} $wrapperDir/${program}
110110+ chmod ${permissions} "$wrapperDir/${program}"
43111 '';
4411245113 ###### Activation script for the setuid wrappers
46114 mkSetuidProgram =
47115 { program
48116 , source
4949- , owner ? "nobody"
5050- , group ? "nogroup"
5151- , setuid ? false
5252- , setgid ? false
5353- , permissions ? "u+rx,g+x,o+x"
117117+ , owner
118118+ , group
119119+ , setuid
120120+ , setgid
121121+ , permissions
54122 , ...
55123 }:
56124 ''
5757- cp ${securityWrapper}/bin/security-wrapper $wrapperDir/${program}
5858- echo -n "${source}" > $wrapperDir/${program}.real
125125+ cp ${securityWrapper}/bin/security-wrapper "$wrapperDir/${program}"
126126+ echo -n "${source}" > "$wrapperDir/${program}.real"
5912760128 # Prevent races
6161- chmod 0000 $wrapperDir/${program}
6262- chown ${owner}.${group} $wrapperDir/${program}
129129+ chmod 0000 "$wrapperDir/${program}"
130130+ chown ${owner}.${group} "$wrapperDir/${program}"
631316464- chmod "u${if setuid then "+" else "-"}s,g${if setgid then "+" else "-"}s,${permissions}" $wrapperDir/${program}
132132+ chmod "u${if setuid then "+" else "-"}s,g${if setgid then "+" else "-"}s,${permissions}" "$wrapperDir/${program}"
65133 '';
6613467135 mkWrappedPrograms =
68136 builtins.map
6969- (s: if (s ? capabilities)
7070- then mkSetcapProgram
7171- ({ owner = "root";
7272- group = "root";
7373- } // s)
7474- else if
7575- (s ? setuid && s.setuid) ||
7676- (s ? setgid && s.setgid) ||
7777- (s ? permissions)
7878- then mkSetuidProgram s
7979- else mkSetuidProgram
8080- ({ owner = "root";
8181- group = "root";
8282- setuid = true;
8383- setgid = false;
8484- permissions = "u+rx,g+x,o+x";
8585- } // s)
8686- ) programs;
137137+ (opts:
138138+ if opts.capabilities != ""
139139+ then mkSetcapProgram opts
140140+ else mkSetuidProgram opts
141141+ ) (lib.attrValues wrappers);
87142in
88143{
89144 imports = [
···9515096151 options = {
97152 security.wrappers = lib.mkOption {
9898- type = lib.types.attrs;
153153+ type = lib.types.attrsOf wrapperType;
99154 default = {};
100155 example = lib.literalExample
101156 ''
102102- { sendmail.source = "/nix/store/.../bin/sendmail";
103103- ping = {
104104- source = "${pkgs.iputils.out}/bin/ping";
105105- owner = "nobody";
106106- group = "nogroup";
107107- capabilities = "cap_net_raw+ep";
108108- };
157157+ {
158158+ # a setuid root program
159159+ doas =
160160+ { setuid = true;
161161+ owner = "root";
162162+ group = "root";
163163+ source = "''${pkgs.doas}/bin/doas";
164164+ };
165165+166166+ # a setgid program
167167+ locate =
168168+ { setgid = true;
169169+ owner = "root";
170170+ group = "mlocate";
171171+ source = "''${pkgs.locate}/bin/locate";
172172+ };
173173+174174+ # a program with the CAP_NET_RAW capability
175175+ ping =
176176+ { owner = "root";
177177+ group = "root";
178178+ capabilities = "cap_net_raw+ep";
179179+ source = "''${pkgs.iputils.out}/bin/ping";
180180+ };
109181 }
110182 '';
111183 description = ''
112112- This option allows the ownership and permissions on the setuid
113113- wrappers for specific programs to be overridden from the
114114- default (setuid root, but not setgid root).
115115-116116- <note>
117117- <para>The sub-attribute <literal>source</literal> is mandatory,
118118- it must be the absolute path to the program to be wrapped.
119119- </para>
120120-121121- <para>The sub-attribute <literal>program</literal> is optional and
122122- can give the wrapper program a new name. The default name is the same
123123- as the attribute name itself.</para>
124124-125125- <para>Additionally, this option can set capabilities on a
126126- wrapper program that propagates those capabilities down to the
127127- wrapped, real program.</para>
128128-129129- <para>NOTE: cap_setpcap, which is required for the wrapper
130130- program to be able to raise caps into the Ambient set is NOT
131131- raised to the Ambient set so that the real program cannot
132132- modify its own capabilities!! This may be too restrictive for
133133- cases in which the real program needs cap_setpcap but it at
134134- least leans on the side security paranoid vs. too
135135- relaxed.</para>
136136- </note>
184184+ This option effectively allows adding setuid/setgid bits, capabilities,
185185+ changing file ownership and permissions of a program without directly
186186+ modifying it. This works by creating a wrapper program under the
187187+ <option>security.wrapperDir</option> directory, which is then added to
188188+ the shell <literal>PATH</literal>.
137189 '';
138190 };
139191···151203 ###### implementation
152204 config = {
153205154154- security.wrappers = {
155155- # These are mount related wrappers that require the +s permission.
156156- fusermount.source = "${pkgs.fuse}/bin/fusermount";
157157- fusermount3.source = "${pkgs.fuse3}/bin/fusermount3";
158158- mount.source = "${lib.getBin pkgs.util-linux}/bin/mount";
159159- umount.source = "${lib.getBin pkgs.util-linux}/bin/umount";
160160- };
206206+ assertions = lib.mapAttrsToList
207207+ (name: opts:
208208+ { assertion = opts.setuid || opts.setgid -> opts.capabilities == "";
209209+ message = ''
210210+ The security.wrappers.${name} wrapper is not valid:
211211+ setuid/setgid and capabilities are mutually exclusive.
212212+ '';
213213+ }
214214+ ) wrappers;
215215+216216+ security.wrappers =
217217+ let
218218+ mkSetuidRoot = source:
219219+ { setuid = true;
220220+ owner = "root";
221221+ group = "root";
222222+ inherit source;
223223+ };
224224+ in
225225+ { # These are mount related wrappers that require the +s permission.
226226+ fusermount = mkSetuidRoot "${pkgs.fuse}/bin/fusermount";
227227+ fusermount3 = mkSetuidRoot "${pkgs.fuse3}/bin/fusermount3";
228228+ mount = mkSetuidRoot "${lib.getBin pkgs.util-linux}/bin/mount";
229229+ umount = mkSetuidRoot "${lib.getBin pkgs.util-linux}/bin/umount";
230230+ };
161231162232 boot.specialFileSystems.${parentWrapperDir} = {
163233 fsType = "tmpfs";
···179249 ]}"
180250 '';
181251182182- ###### setcap activation script
252252+ ###### wrappers activation script
183253 system.activationScripts.wrappers =
184254 lib.stringAfter [ "specialfs" "users" ]
185255 ''
186186- # Look in the system path and in the default profile for
187187- # programs to be wrapped.
188188- WRAPPER_PATH=${config.system.path}/bin:${config.system.path}/sbin
189189-190256 chmod 755 "${parentWrapperDir}"
191257192258 # We want to place the tmpdirs for the wrappers to the parent dir.
193259 wrapperDir=$(mktemp --directory --tmpdir="${parentWrapperDir}" wrappers.XXXXXXXXXX)
194194- chmod a+rx $wrapperDir
260260+ chmod a+rx "$wrapperDir"
195261196262 ${lib.concatStringsSep "\n" mkWrappedPrograms}
197263···199265 # Atomically replace the symlink
200266 # See https://axialcorps.com/2013/07/03/atomically-replacing-files-and-directories/
201267 old=$(readlink -f ${wrapperDir})
202202- if [ -e ${wrapperDir}-tmp ]; then
203203- rm --force --recursive ${wrapperDir}-tmp
268268+ if [ -e "${wrapperDir}-tmp" ]; then
269269+ rm --force --recursive "${wrapperDir}-tmp"
204270 fi
205205- ln --symbolic --force --no-dereference $wrapperDir ${wrapperDir}-tmp
206206- mv --no-target-directory ${wrapperDir}-tmp ${wrapperDir}
207207- rm --force --recursive $old
271271+ ln --symbolic --force --no-dereference "$wrapperDir" "${wrapperDir}-tmp"
272272+ mv --no-target-directory "${wrapperDir}-tmp" "${wrapperDir}"
273273+ rm --force --recursive "$old"
208274 else
209275 # For initial setup
210210- ln --symbolic $wrapperDir ${wrapperDir}
276276+ ln --symbolic "$wrapperDir" "${wrapperDir}"
211277 fi
212278 '';
279279+280280+ ###### wrappers consistency checks
281281+ system.extraDependencies = lib.singleton (pkgs.runCommandLocal
282282+ "ensure-all-wrappers-paths-exist" { }
283283+ ''
284284+ # make sure we produce output
285285+ mkdir -p $out
286286+287287+ echo -n "Checking that Nix store paths of all wrapped programs exist... "
288288+289289+ declare -A wrappers
290290+ ${lib.concatStringsSep "\n" (lib.mapAttrsToList (n: v:
291291+ "wrappers['${n}']='${v.source}'") wrappers)}
292292+293293+ for name in "''${!wrappers[@]}"; do
294294+ path="''${wrappers[$name]}"
295295+ if [[ "$path" =~ /nix/store ]] && [ ! -e "$path" ]; then
296296+ test -t 1 && echo -ne '\033[1;31m'
297297+ echo "FAIL"
298298+ echo "The path $path does not exist!"
299299+ echo 'Please, check the value of `security.wrappers."'$name'".source`.'
300300+ test -t 1 && echo -ne '\033[0m'
301301+ exit 1
302302+ fi
303303+ done
304304+305305+ echo "OK"
306306+ '');
213307 };
214308}