1{ config, lib, pkgs, ... }:
2let
3
4 inherit (config.security) wrapperDir wrappers;
5
6 parentWrapperDir = dirOf wrapperDir;
7
8 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 };
16
17 ###### 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
31
32 # Prevent races
33 chmod 0000 $wrapperDir/${program}
34 chown ${owner}.${group} $wrapperDir/${program}
35
36 # 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}
40
41 # Set the executable bit
42 chmod ${permissions} $wrapperDir/${program}
43 '';
44
45 ###### 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
59
60 # Prevent races
61 chmod 0000 $wrapperDir/${program}
62 chown ${owner}.${group} $wrapperDir/${program}
63
64 chmod "u${if setuid then "+" else "-"}s,g${if setgid then "+" else "-"}s,${permissions}" $wrapperDir/${program}
65 '';
66
67 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 = [
90 (lib.mkRemovedOptionModule [ "security" "setuidOwners" ] "Use security.wrappers instead")
91 (lib.mkRemovedOptionModule [ "security" "setuidPrograms" ] "Use security.wrappers instead")
92 ];
93
94 ###### interface
95
96 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 };
109 }
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
140 security.wrapperDir = lib.mkOption {
141 type = lib.types.path;
142 default = "/run/wrappers/bin";
143 internal = true;
144 description = ''
145 This option defines the path to the wrapper programs. It
146 should not be overriden.
147 '';
148 };
149 };
150
151 ###### implementation
152 config = {
153
154 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 };
161
162 boot.specialFileSystems.${parentWrapperDir} = {
163 fsType = "tmpfs";
164 options = [ "nodev" "mode=755" ];
165 };
166
167 # Make sure our wrapperDir exports to the PATH env variable when
168 # initializing the shell
169 environment.extraInit = ''
170 # Wrappers override other bin directories.
171 export PATH="${wrapperDir}:$PATH"
172 '';
173
174 security.apparmor.includes."nixos/security.wrappers" = ''
175 include "${pkgs.apparmorRulesFromClosure { name="security.wrappers"; } [
176 securityWrapper
177 pkgs.stdenv.cc.cc
178 pkgs.stdenv.cc.libc
179 ]}"
180 '';
181
182 ###### 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}"
191
192 # 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
195
196 ${lib.concatStringsSep "\n" mkWrappedPrograms}
197
198 if [ -L ${wrapperDir} ]; then
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 '';
213 };
214}