summaryrefslogtreecommitdiffstats
path: root/src/linuxcap.c
blob: 63a510f226e92d4e6a6caa60c71c801fe0c604d5 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
/*
 * Minimal handling of Linux kernel capabilities
 *
 * Copyright 2000-2023 Willy Tarreau <w@1wt.eu>
 *
 * This program is free software; you can redistribute it and/or
 * modify it under the terms of the GNU General Public License
 * as published by the Free Software Foundation; either version
 * 2 of the License, or (at your option) any later version.
 *
 */

/* Depending on distros, some have capset(), others use the more complicated
 * libcap. Let's stick to what we need and the kernel documents (capset).
 * Note that prctl is needed here.
 */
#include <linux/capability.h>
#include <sys/prctl.h>
#include <errno.h>
#include <unistd.h>
#include <syscall.h>

#include <haproxy/api.h>
#include <haproxy/cfgparse.h>
#include <haproxy/errors.h>
#include <haproxy/global.h>
#include <haproxy/tools.h>

/* supported names, zero-terminated */
static const struct {
	int cap;
	const char *name;
} known_caps[] = {
#ifdef CAP_NET_RAW
	{ CAP_NET_RAW, "cap_net_raw" },
#endif
#ifdef CAP_NET_ADMIN
	{ CAP_NET_ADMIN, "cap_net_admin" },
#endif
#ifdef CAP_NET_BIND_SERVICE
	{ CAP_NET_BIND_SERVICE, "cap_net_bind_service" },
#endif
#ifdef CAP_SYS_ADMIN
	{ CAP_SYS_ADMIN, "cap_sys_admin" },
#endif
	/* must be last */
	{ 0, 0 }
};

/* provided by sys/capability.h on some distros */
static inline int capget(cap_user_header_t hdrp, const cap_user_data_t datap)
{
	return syscall(SYS_capget, hdrp, datap);
}

/* provided by sys/capability.h on some distros */
static inline int capset(cap_user_header_t hdrp, const cap_user_data_t datap)
{
	return syscall(SYS_capset, hdrp, datap);
}

/* defaults to zero, i.e. we don't keep any cap after setuid() */
static uint32_t caplist;

/* try to check if CAP_NET_ADMIN, CAP_NET_RAW or CAP_SYS_ADMIN are in the
 * process Effective set in the case when euid is non-root. If there is a
 * match, LSTCHK_NETADM or LSTCHK_SYSADM is unset respectively from
 * global.last_checks to avoid warning due to global.last_checks verifications
 * later at the process init stage.
 * If there is no any supported by haproxy capability in the process Effective
 * set, try to check the process Permitted set. In this case we promote from
 * Permitted set to Effective only the capabilities, that were marked by user
 * via 'capset' keyword in the global section (caplist). If there is match with
 * caplist and CAP_NET_ADMIN/CAP_NET_RAW or CAP_SYS_ADMIN are in this list,
 * LSTCHK_NETADM or/and LSTCHK_SYSADM will be unset by the same reason.
 * We do this only if the current euid is non-root and there is no global.uid.
 * Otherwise, the process will continue either to run under root, or it will do
 * a transition to unprivileged user later in prepare_caps_for_setuid(),
 * which specially manages its capabilities in that case.
 * Always returns 0. Diagnostic warnings will be emitted only, if
 * LSTCHK_NETADM/LSTCHK_SYSADM is presented in global.last_checks and some
 * failures are encountered.
 */
int prepare_caps_from_permitted_set(int from_uid, int to_uid, const char *program_name)
{
	struct __user_cap_data_struct start_cap_data = { };
	struct __user_cap_header_struct cap_hdr = {
		.pid = 0, /* current process */
		.version = _LINUX_CAPABILITY_VERSION_1,
	};

	/* started as root */
	if (!from_uid)
		return 0;

	/* will change ruid and euid later in set_identity() */
	if (to_uid)
		return 0;

	/* first, let's check if CAP_NET_ADMIN or CAP_NET_RAW is already in
	 * the process effective set. This may happen, when administrator sets
	 * these capabilities and the file effective bit on haproxy binary via
	 * setcap, see capabilities man page for details.
	 */
	if (capget(&cap_hdr, &start_cap_data) == -1) {
		if (global.last_checks & (LSTCHK_NETADM | LSTCHK_SYSADM))
			ha_diag_warning("Failed to get process capabilities using capget(): %s. "
					"Can't use capabilities that might be set on %s binary "
					"by administrator.\n", strerror(errno), program_name);
		return 0;
	}

	if (start_cap_data.effective & ((1 << CAP_NET_ADMIN)|(1 << CAP_NET_RAW))) {
		global.last_checks &= ~LSTCHK_NETADM;
		return 0;
	}

	if (start_cap_data.effective & ((1 << CAP_SYS_ADMIN))) {
		global.last_checks &= ~LSTCHK_SYSADM;
		return 0;
	}

	/* second, try to check process permitted set, in this case caplist is
	 * necessary. Allows to put cap_net_bind_service in process effective
	 * set, if it is in the caplist and also presented in the binary
	 * permitted set.
	 */
	if (caplist && start_cap_data.permitted & caplist) {
		start_cap_data.effective |= start_cap_data.permitted & caplist;
		if (capset(&cap_hdr, &start_cap_data) == 0) {
			if (caplist & ((1 << CAP_NET_ADMIN)|(1 << CAP_NET_RAW)))
				global.last_checks &= ~LSTCHK_NETADM;
			if (caplist & (1 << CAP_SYS_ADMIN))
				global.last_checks &= ~LSTCHK_SYSADM;
		} else if (global.last_checks & (LSTCHK_NETADM|LSTCHK_SYSADM)) {
			ha_diag_warning("Failed to put capabilities from caplist in %s "
					"process Effective capabilities set using capset(): %s\n",
					program_name, strerror(errno));
		}
	}

	return 0;
}

/* try to apply capabilities before switching UID from <from_uid> to <to_uid>.
 * In practice we need to do this in 4 steps:
 *   - set PR_SET_KEEPCAPS to preserve caps across the final setuid()
 *   - set the effective and permitted caps ;
 *   - switch euid to non-zero
 *   - set the effective and permitted caps again
 *   - then the caller can safely call setuid()
 * On success LSTCHK_NETADM is unset from global.last_checks, if CAP_NET_ADMIN
 * or CAP_NET_RAW was found in the caplist from config. Same for
 * LSTCHK_SYSADM, if CAP_SYS_ADMIN was found in the caplist from config.
 * We don't do this if the current euid is not zero or if the target uid
 * is zero. Returns 0 on success, negative on failure. Alerts may be emitted.
 */
int prepare_caps_for_setuid(int from_uid, int to_uid)
{
	struct __user_cap_data_struct cap_data = { };
	struct __user_cap_header_struct cap_hdr = {
		.pid = 0, /* current process */
		.version = _LINUX_CAPABILITY_VERSION_1,
	};

	if (from_uid != 0)
		return 0;

	if (!to_uid)
		return 0;

	if (!caplist)
		return 0;

	if (prctl(PR_SET_KEEPCAPS, 1) == -1) {
		ha_alert("Failed to preserve capabilities using prctl(): %s\n", strerror(errno));
		return -1;
	}

	cap_data.effective = cap_data.permitted = caplist | (1 << CAP_SETUID);
	if (capset(&cap_hdr, &cap_data) == -1) {
		ha_alert("Failed to preset the capabilities to preserve using capset(): %s\n", strerror(errno));
		return -1;
	}

	if (seteuid(to_uid) == -1) {
		ha_alert("Failed to set effective uid to %d: %s\n", to_uid, strerror(errno));
		return -1;
	}

	cap_data.effective = cap_data.permitted = caplist | (1 << CAP_SETUID);
	if (capset(&cap_hdr, &cap_data) == -1) {
		ha_alert("Failed to set the final capabilities using capset(): %s\n", strerror(errno));
		return -1;
	}

	if (caplist & ((1 << CAP_NET_ADMIN)|(1 << CAP_NET_RAW)))
		global.last_checks &= ~LSTCHK_NETADM;

	if (caplist & (1 << CAP_SYS_ADMIN))
		global.last_checks &= ~LSTCHK_SYSADM;

	/* all's good */
	return 0;
}

/* finalize the capabilities after setuid(). The most important is to drop the
 * CAP_SET_SETUID capability, which would otherwise allow to switch back to any
 * UID and recover everything.
 */
int finalize_caps_after_setuid(int from_uid, int to_uid)
{
	struct __user_cap_data_struct cap_data = { };
	struct __user_cap_header_struct cap_hdr = {
		.pid = 0, /* current process */
		.version = _LINUX_CAPABILITY_VERSION_1,
	};

	if (from_uid != 0)
		return 0;

	if (!to_uid)
		return 0;

	if (!caplist)
		return 0;

	cap_data.effective = cap_data.permitted = caplist;
	if (capset(&cap_hdr, &cap_data) == -1) {
		ha_alert("Failed to drop the setuid capability using capset(): %s\n", strerror(errno));
		return -1;
	}
	/* all's good */
	return 0;
}

/* parse the "setcap" global keyword. Returns -1 on failure, 0 on success. */
static int cfg_parse_global_setcap(char **args, int section_type,
                                   struct proxy *curpx, const struct proxy *defpx,
                                   const char *file, int line, char **err)
{
	char *name = args[1];
	char *next;
	uint32_t caps = 0;
	int id;

	if (!*name) {
		memprintf(err, "'%s' : missing capability name(s). ", args[0]);
		goto dump_caps;
	}

	while (name && *name) {
		next = strchr(name, ',');
		if (next)
			*(next++) = '\0';

		for (id = 0; known_caps[id].cap; id++) {
			if (strcmp(name, known_caps[id].name) == 0) {
				caps |= 1U << known_caps[id].cap;
				break;
			}
		}

		if (!known_caps[id].cap) {
			memprintf(err, "'%s' : unsupported capability '%s'. ", args[0], args[1]);
			goto dump_caps;
		}
		name = next;
	}

	caplist |= caps;
	return 0;


 dump_caps:
	memprintf(err, "%s Supported ones are: ", *err);

	for (id = 0; known_caps[id].cap; id++)
		memprintf(err, "%s%s%s%s", *err,
			  id ? known_caps[id+1].cap ? ", " : " and " : "",
			  known_caps[id].name, known_caps[id+1].cap ? "" : ".");
	return -1;
}

static struct cfg_kw_list cfg_kws = {ILH, {
        { CFG_GLOBAL, "setcap", cfg_parse_global_setcap },
        { 0, NULL, NULL }
}};

INITCALL1(STG_REGISTER, cfg_register_keywords, &cfg_kws);