mirror of
https://git.haproxy.org/git/haproxy.git/
synced 2025-08-05 22:56:57 +02:00
If 'namespace' keyword is used in the backend server settings or/and in the bind string, it means that haproxy process will call setns() to change its default namespace to the configured one and then, it will create a socket in this new namespace. setns() syscall requires CAP_SYS_ADMIN capability in the process Effective set (see man 2 setns). Otherwise, the process must be run as root. To avoid to run haproxy as root, let's add cap_sys_admin capability in the same way as we already added the support for some other network capabilities. As CAP_SYS_ADMIN belongs to CAP_SYS_* capabilities type, let's add a separate flag LSTCHK_SYSADM for it. This flag is set, if the 'namespace' keyword was found during configuration parsing. The flag may be unset only in prepare_caps_for_setuid() or in prepare_caps_from_permitted_set(), which inspect process EUID/RUID and Effective and Permitted capabilities sets. If system doesn't support Linux capabilities or 'cap_sys_admin' was not set in 'setcap', but 'namespace' keyword is presented in the configuration, we keep the previous strict behaviour. Process, that has changed uid to the non-priviledged user, will terminate with alert. This alert invites the user to recheck its configuration. In the case, when haproxy will start and run under a non-root user and 'cap_sys_admin' is not set, but 'namespace' keyword is presented, this patch does not change previous behaviour as well. We'll still let the user to try its configuration, but we inform via warning, that unexpected things, like socket creation errors, may occur.
291 lines
8.9 KiB
C
291 lines
8.9 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
|
|
#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);
|