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.stdenv.mkDerivation {
14 name = "security-wrapper";
15 phases = [ "installPhase" "fixupPhase" ];
16 buildInputs = [ pkgs.libcap pkgs.libcap_ng pkgs.linuxHeaders ];
17 hardeningEnable = [ "pie" ];
18 installPhase = ''
19 mkdir -p $out/bin
20 $CC -Wall -O2 -DWRAPPER_DIR=\"${parentWrapperDir}\" \
21 -lcap-ng -lcap ${./wrapper.c} -o $out/bin/security-wrapper
22 '';
23 };
24
25 ###### Activation script for the setcap wrappers
26 mkSetcapProgram =
27 { program
28 , capabilities
29 , source
30 , owner ? "nobody"
31 , group ? "nogroup"
32 , permissions ? "u+rx,g+x,o+x"
33 , ...
34 }:
35 assert (lib.versionAtLeast (lib.getVersion config.boot.kernelPackages.kernel) "4.3");
36 ''
37 cp ${securityWrapper}/bin/security-wrapper $wrapperDir/${program}
38 echo -n "${source}" > $wrapperDir/${program}.real
39
40 # Prevent races
41 chmod 0000 $wrapperDir/${program}
42 chown ${owner}.${group} $wrapperDir/${program}
43
44 # Set desired capabilities on the file plus cap_setpcap so
45 # the wrapper program can elevate the capabilities set on
46 # its file into the Ambient set.
47 ${pkgs.libcap.out}/bin/setcap "cap_setpcap,${capabilities}" $wrapperDir/${program}
48
49 # Set the executable bit
50 chmod ${permissions} $wrapperDir/${program}
51 '';
52
53 ###### Activation script for the setuid wrappers
54 mkSetuidProgram =
55 { program
56 , source
57 , owner ? "nobody"
58 , group ? "nogroup"
59 , setuid ? false
60 , setgid ? false
61 , permissions ? "u+rx,g+x,o+x"
62 , ...
63 }:
64 ''
65 cp ${securityWrapper}/bin/security-wrapper $wrapperDir/${program}
66 echo -n "${source}" > $wrapperDir/${program}.real
67
68 # Prevent races
69 chmod 0000 $wrapperDir/${program}
70 chown ${owner}.${group} $wrapperDir/${program}
71
72 chmod "u${if setuid then "+" else "-"}s,g${if setgid then "+" else "-"}s,${permissions}" $wrapperDir/${program}
73 '';
74
75 mkWrappedPrograms =
76 builtins.map
77 (s: if (s ? "capabilities")
78 then mkSetcapProgram
79 ({ owner = "root";
80 group = "root";
81 } // s)
82 else if
83 (s ? "setuid" && s.setuid) ||
84 (s ? "setgid" && s.setgid) ||
85 (s ? "permissions")
86 then mkSetuidProgram s
87 else mkSetuidProgram
88 ({ owner = "root";
89 group = "root";
90 setuid = true;
91 setgid = false;
92 permissions = "u+rx,g+x,o+x";
93 } // s)
94 ) programs;
95in
96{
97
98 ###### interface
99
100 options = {
101 security.wrappers = lib.mkOption {
102 type = lib.types.attrs;
103 default = {};
104 example = lib.literalExample
105 ''
106 { sendmail.source = "/nix/store/.../bin/sendmail";
107 ping = {
108 source = "${pkgs.iputils.out}/bin/ping";
109 owner = "nobody";
110 group = "nogroup";
111 capabilities = "cap_net_raw+ep";
112 };
113 }
114 '';
115 description = ''
116 This option allows the ownership and permissions on the setuid
117 wrappers for specific programs to be overridden from the
118 default (setuid root, but not setgid root).
119
120 <note>
121 <para>The sub-attribute <literal>source</literal> is mandatory,
122 it must be the absolute path to the program to be wrapped.
123 </para>
124
125 <para>The sub-attribute <literal>program</literal> is optional and
126 can give the wrapper program a new name. The default name is the same
127 as the attribute name itself.</para>
128
129 <para>Additionally, this option can set capabilities on a
130 wrapper program that propagates those capabilities down to the
131 wrapped, real program.</para>
132
133 <para>NOTE: cap_setpcap, which is required for the wrapper
134 program to be able to raise caps into the Ambient set is NOT
135 raised to the Ambient set so that the real program cannot
136 modify its own capabilities!! This may be too restrictive for
137 cases in which the real program needs cap_setpcap but it at
138 least leans on the side security paranoid vs. too
139 relaxed.</para>
140 </note>
141 '';
142 };
143
144 security.wrapperDir = lib.mkOption {
145 type = lib.types.path;
146 default = "/run/wrappers/bin";
147 internal = true;
148 description = ''
149 This option defines the path to the wrapper programs. It
150 should not be overriden.
151 '';
152 };
153 };
154
155 ###### implementation
156 config = {
157
158 security.wrappers = {
159 fusermount.source = "${pkgs.fuse}/bin/fusermount";
160 fusermount3.source = "${pkgs.fuse3}/bin/fusermount3";
161 };
162
163 boot.specialFileSystems.${parentWrapperDir} = {
164 fsType = "tmpfs";
165 options = [ "nodev" ];
166 };
167
168 # Make sure our wrapperDir exports to the PATH env variable when
169 # initializing the shell
170 environment.extraInit = ''
171 # Wrappers override other bin directories.
172 export PATH="${wrapperDir}:$PATH"
173 '';
174
175 ###### setcap activation script
176 system.activationScripts.wrappers =
177 lib.stringAfter [ "specialfs" "users" ]
178 ''
179 # Look in the system path and in the default profile for
180 # programs to be wrapped.
181 WRAPPER_PATH=${config.system.path}/bin:${config.system.path}/sbin
182
183 # Remove the old /var/setuid-wrappers path from the system...
184 #
185 # TODO: this is only necessary for upgrades 16.09 => 17.x;
186 # this conditional removal block needs to be removed after
187 # the release.
188 if [ -d /var/setuid-wrappers ]; then
189 rm -rf /var/setuid-wrappers
190 ln -s /run/wrappers/bin /var/setuid-wrappers
191 fi
192
193 # Remove the old /run/setuid-wrappers-dir path from the
194 # system as well...
195 #
196 # TODO: this is only necessary for upgrades 16.09 => 17.x;
197 # this conditional removal block needs to be removed after
198 # the release.
199 if [ -d /run/setuid-wrapper-dirs ]; then
200 rm -rf /run/setuid-wrapper-dirs
201 ln -s /run/wrappers/bin /run/setuid-wrapper-dirs
202 fi
203
204 # TODO: this is only necessary for upgrades 16.09 => 17.x;
205 # this conditional removal block needs to be removed after
206 # the release.
207 if readlink -f /run/booted-system | grep nixos-17 > /dev/null; then
208 rm -rf /run/setuid-wrapper-dirs
209 rm -rf /var/setuid-wrappers
210 fi
211
212 # We want to place the tmpdirs for the wrappers to the parent dir.
213 wrapperDir=$(mktemp --directory --tmpdir="${parentWrapperDir}" wrappers.XXXXXXXXXX)
214 chmod a+rx $wrapperDir
215
216 ${lib.concatStringsSep "\n" mkWrappedPrograms}
217
218 if [ -L ${wrapperDir} ]; then
219 # Atomically replace the symlink
220 # See https://axialcorps.com/2013/07/03/atomically-replacing-files-and-directories/
221 old=$(readlink -f ${wrapperDir})
222 ln --symbolic --force --no-dereference $wrapperDir ${wrapperDir}-tmp
223 mv --no-target-directory ${wrapperDir}-tmp ${wrapperDir}
224 rm --force --recursive $old
225 else
226 # For initial setup
227 ln --symbolic $wrapperDir ${wrapperDir}
228 fi
229 '';
230 };
231}