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 gcc -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.fusermount.source = "${pkgs.fuse}/bin/fusermount";
159
160 boot.specialFileSystems.${parentWrapperDir} = {
161 fsType = "tmpfs";
162 options = [ "nodev" ];
163 };
164
165 # Make sure our wrapperDir exports to the PATH env variable when
166 # initializing the shell
167 environment.extraInit = ''
168 # Wrappers override other bin directories.
169 export PATH="${wrapperDir}:$PATH"
170 '';
171
172 ###### setcap activation script
173 system.activationScripts.wrappers =
174 lib.stringAfter [ "specialfs" "users" ]
175 ''
176 # Look in the system path and in the default profile for
177 # programs to be wrapped.
178 WRAPPER_PATH=${config.system.path}/bin:${config.system.path}/sbin
179
180 # Remove the old /var/setuid-wrappers path from the system...
181 #
182 # TODO: this is only necessary for upgrades 16.09 => 17.x;
183 # this conditional removal block needs to be removed after
184 # the release.
185 if [ -d /var/setuid-wrappers ]; then
186 rm -rf /var/setuid-wrappers
187 ln -s /run/wrappers/bin /var/setuid-wrappers
188 fi
189
190 # Remove the old /run/setuid-wrappers-dir path from the
191 # system as well...
192 #
193 # TODO: this is only necessary for upgrades 16.09 => 17.x;
194 # this conditional removal block needs to be removed after
195 # the release.
196 if [ -d /run/setuid-wrapper-dirs ]; then
197 rm -rf /run/setuid-wrapper-dirs
198 ln -s /run/wrappers/bin /run/setuid-wrapper-dirs
199 fi
200
201 # TODO: this is only necessary for upgrades 16.09 => 17.x;
202 # this conditional removal block needs to be removed after
203 # the release.
204 if readlink -f /run/booted-system | grep nixos-17 > /dev/null; then
205 rm -rf /run/setuid-wrapper-dirs
206 rm -rf /var/setuid-wrappers
207 fi
208
209 # We want to place the tmpdirs for the wrappers to the parent dir.
210 wrapperDir=$(mktemp --directory --tmpdir="${parentWrapperDir}" wrappers.XXXXXXXXXX)
211 chmod a+rx $wrapperDir
212
213 ${lib.concatStringsSep "\n" mkWrappedPrograms}
214
215 if [ -L ${wrapperDir} ]; then
216 # Atomically replace the symlink
217 # See https://axialcorps.com/2013/07/03/atomically-replacing-files-and-directories/
218 old=$(readlink -f ${wrapperDir})
219 ln --symbolic --force --no-dereference $wrapperDir ${wrapperDir}-tmp
220 mv --no-target-directory ${wrapperDir}-tmp ${wrapperDir}
221 rm --force --recursive $old
222 else
223 # For initial setup
224 ln --symbolic $wrapperDir ${wrapperDir}
225 fi
226 '';
227 };
228}