mirror of
https://git.haproxy.org/git/haproxy.git/
synced 2025-09-21 05:41:26 +02:00
Since the Linux capabilities support add-on (see the commit bd84387beb26 ("MEDIUM: capabilities: enable support for Linux capabilities")), we can also check haproxy process effective and permitted capabilities sets, when it starts and runs as non-root. Like this, if needed network capabilities are presented only in the process permitted set, we can get this information with capget and put them in the process effective set via capset. To do this properly, let's introduce prepare_caps_from_permitted_set(). First, it checks if binary effective set has CAP_NET_ADMIN or CAP_NET_RAW. If there is a match, LSTCHK_NETADM is removed from global.last_checks list to avoid warning, because in the initialization sequence some last configuration checks are based on LSTCHK_NETADM flag and haproxy process euid may stay unpriviledged. If there are no CAP_NET_ADMIN and CAP_NET_RAW in the effective set, permitted set will be checked and only capabilities given in 'setcap' keyword will be promoted in the process effective set. LSTCHK_NETADM will be also removed in this case by the same reason. In order to be transparent, we promote from permitted set only capabilities given by user in 'setcap' keyword. So, if caplist doesn't include CAP_NET_ADMIN or CAP_NET_RAW, LSTCHK_NETADM would not be unset and warning about missing priviledges will be emitted at initialization. Need to call it before protocol_bind_all() to allow binding to priviledged ports under non-root and 'setcap cap_net_bind_service' must be set in the global section in this case.
276 lines
8.3 KiB
C
276 lines
8.3 KiB
C
/*
|
|
* 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
|
|
/* 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 or CAP_NET_RAW are in the process effective
|
|
* set in the case when euid is non-root. If there is a match,
|
|
* LSTCHK_NETADM is unset from global.last_checks to avoid warning due to
|
|
* global.last_checks verifications later in the init process.
|
|
* If there is no CAP_NET_ADMIN, nor CAP_NET_RAW in the effective set, try to
|
|
* check 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 or/and CAP_NET_RAW in this caplist, LSTCHK_NETADM
|
|
* 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 unpriviledged 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 is presented in LSTCHK_NETADM 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)
|
|
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;
|
|
}
|
|
|
|
/* 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;
|
|
} else if (global.last_checks & LSTCHK_NETADM) {
|
|
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.
|
|
* 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;
|
|
|
|
/* 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);
|