MINOR: capabilities: add cap_sys_admin support

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.
This commit is contained in:
Valentine Krasnobaeva 2024-04-26 21:47:54 +02:00 committed by Willy Tarreau
parent 13ef552488
commit 5cbb278fae
6 changed files with 63 additions and 42 deletions

View File

@ -4607,33 +4607,35 @@ compiled with USE_LINUX_CAP=1, it is able to preserve capabilities given in
'setcap' keyword during switching from root user to a non-root. 'setcap' keyword during switching from root user to a non-root.
Since version v3.1 haproxy also checks if capabilities given in 'setcap' Since version v3.1 haproxy also checks if capabilities given in 'setcap'
keyword were set in its binary file permitted set by administrator keyword were set in its binary file Permitted set by administrator
(capget syscall). If this a case it performs transition of these capabilities (capget syscall). If this a case it performs transition of these capabilities
in its process effective set (capset syscall), while running as a non-root in its process Effective set (capset syscall), while running as a non-root
user. user.
This was done to avoid all potential use cases when haproxy starts and runs as This was done to avoid all potential use cases when haproxy starts and runs as
root: transparent proxy mode, binding to privileged ports. root: transparent proxy mode, binding to privileged ports.
'setcap' keyword supports following network capabilities: 'setcap' keyword supports following network capabilities:
- cap_net_admin - cap_net_admin: transparent proxying, binding socket to a specific network
- cap_net_raw (subset of cap_net_admin) interface, using set-mark action;
- cap_net_bind_service - cap_net_raw (subset of cap_net_admin): transparent proxying;
- cap_net_bind_service: binding socket to a specific network interface;
- cap_sys_admin: creating socket in a specific network namespace.
Haproxy never does the transition of these capabilities from its permitted set Haproxy never does the transition of these capabilities from its Permitted set
to the effective, if they are not listed as 'setcap' argument. See more to the Effective, if they are not listed as 'setcap' argument. See more
information about 'setcap' keyword and supported capabilities in the chapter information about 'setcap' keyword and supported capabilities in the chapter
3.1 Process management and security in the Configuration guide. 3.1 Process management and security in the Configuration guide.
Administrator may add needed capabilities in the haproxy binary file permitted Administrator may add needed capabilities in the haproxy binary file Permitted
set with the following command: set with the following command:
Example: Example:
# setcap cap_net_admin,cap_net_bind_service=p /usr/local/sbin/haproxy # setcap cap_net_admin,cap_net_bind_service=p /usr/local/sbin/haproxy
Added capabilities will be seen in process permitted set after its start. Added capabilities will be seen in process Permitted set after its start.
If the same capabilities are the arguments of 'setcap' keyword, they could be If the same capabilities are the arguments of 'setcap' keyword, they could be
also seen in the process effective set. This could be check with the following also seen in the process Effective set. This could be check with the following
command: command:
Example: Example:
@ -4647,20 +4649,20 @@ Example:
See more details about setcap and capabilities sets in Linux man pages See more details about setcap and capabilities sets in Linux man pages
(capabilities(7)). (capabilities(7)).
In some cases like transparent proxying, binding socket to a specific network In some use cases like transparent proxying or creating socket in a specific
interface, using set-mark action, configuration file parser detects that network namespace, configuration file parser detects that cap_net_raw or
cap_net_admin or cap_net_raw capabilities are needed. Then, during cap_sys_admin or some other supported capabilities are needed. Then, during
initialization stage, haproxy process checks, if these capabilities could be the initialization stage, haproxy process checks, if these capabilities could
put in its effective set. If it's not possible due to capget or capset syscall be put in its Effective set. If it's not possible due to capget or capset
failure (restrictions set on syscalls by some security modules like SELinux, syscall failure (restrictions set on syscalls by some security modules like
Seccomp, etc), process emits diagnostic warnings (start with -dD). SELinux, Seccomp, etc), process emits diagnostic warnings (start with -dD).
Due to support of many different platforms with different system settings, Due to support of many different platforms with different system settings,
it's impossible for the parser to deduce from the configuration file, if it's impossible for the parser to deduce from the configuration file, if
binding to privileged ports will be done. So, in the case of insufficient binding to privileged ports will be done. So, in the case of insufficient
privileges (run as non-root) process will terminate only with an alert privileges (run as non-root) process will terminate only with an alert
message like below. It's up to a user to recheck its configuration and message like below. It's up to a user to recheck its configuration and haproxy
capabilities set for haproxy binary. binary capabilities set.
Example: Example:
$ haproxy -dD -f haproxy.cfg $ haproxy -dD -f haproxy.cfg

View File

@ -46,7 +46,7 @@
#define MODE_DUMP_NB_L 0x10000 /* dump line numbers when the configuration file is dump */ #define MODE_DUMP_NB_L 0x10000 /* dump line numbers when the configuration file is dump */
/* list of last checks to perform, depending on config options */ /* list of last checks to perform, depending on config options */
/* unused: 0x00000001 */ #define LSTCHK_SYSADM 0x00000001 /* check that we have CAP_SYS_ADMIN */
#define LSTCHK_NETADM 0x00000002 /* check that we have CAP_NET_ADMIN */ #define LSTCHK_NETADM 0x00000002 /* check that we have CAP_NET_ADMIN */
/* Global tuning options */ /* Global tuning options */

View File

@ -169,6 +169,8 @@ static int bind_parse_namespace(char **args, int cur_arg, struct proxy *px, stru
ha_alert("Cannot open namespace '%s'.\n", args[cur_arg + 1]); ha_alert("Cannot open namespace '%s'.\n", args[cur_arg + 1]);
return ERR_ALERT | ERR_FATAL; return ERR_ALERT | ERR_FATAL;
} }
global.last_checks |= LSTCHK_SYSADM;
return 0; return 0;
} }
#endif #endif

View File

@ -3626,13 +3626,13 @@ int main(int argc, char **argv)
if ((global.mode & (MODE_MWORKER | MODE_DAEMON)) == 0) if ((global.mode & (MODE_MWORKER | MODE_DAEMON)) == 0)
set_identity(argv[0]); set_identity(argv[0]);
/* set_identity() above might have dropped LSTCHK_NETADM if /* set_identity() above might have dropped LSTCHK_NETADM or/and
* it changed to a new UID while preserving enough permissions * LSTCHK_SYSADM if it changed to a new UID while preserving enough
* to honnor LSTCHK_NETADM. * permissions to honnor LSTCHK_NETADM/LSTCHK_SYSADM.
*/ */
if ((global.last_checks & LSTCHK_NETADM) && getuid()) { if ((global.last_checks & (LSTCHK_NETADM|LSTCHK_SYSADM)) && getuid()) {
/* If global.uid is present in config, it is already set as euid /* If global.uid is present in config, it is already set as euid
* and ruid by set_identity() call just above, so it's better to * and ruid by set_identity() just above, so it's better to
* remind the user to fix uncoherent settings. * remind the user to fix uncoherent settings.
*/ */
if (global.uid) { if (global.uid) {

View File

@ -39,6 +39,9 @@ static const struct {
#endif #endif
#ifdef CAP_NET_BIND_SERVICE #ifdef CAP_NET_BIND_SERVICE
{ CAP_NET_BIND_SERVICE, "cap_net_bind_service" }, { CAP_NET_BIND_SERVICE, "cap_net_bind_service" },
#endif
#ifdef CAP_SYS_ADMIN
{ CAP_SYS_ADMIN, "cap_sys_admin" },
#endif #endif
/* must be last */ /* must be last */
{ 0, 0 } { 0, 0 }
@ -59,23 +62,24 @@ static inline int capset(cap_user_header_t hdrp, const cap_user_data_t datap)
/* defaults to zero, i.e. we don't keep any cap after setuid() */ /* defaults to zero, i.e. we don't keep any cap after setuid() */
static uint32_t caplist; static uint32_t caplist;
/* try to check if CAP_NET_ADMIN or CAP_NET_RAW are in the process effective /* try to check if CAP_NET_ADMIN, CAP_NET_RAW or CAP_SYS_ADMIN are in the
* set in the case when euid is non-root. If there is a match, * process Effective set in the case when euid is non-root. If there is a
* LSTCHK_NETADM is unset from global.last_checks to avoid warning due to * match, LSTCHK_NETADM or LSTCHK_SYSADM is unset respectively from
* global.last_checks verifications later in the init process. * global.last_checks to avoid warning due to global.last_checks verifications
* If there is no CAP_NET_ADMIN, nor CAP_NET_RAW in the effective set, try to * later at the process init stage.
* check process permitted set. In this case we promote from permitted set to * If there is no any supported by haproxy capability in the process Effective
* effective only the capabilities, that were marked by user via 'capset' * set, try to check the process Permitted set. In this case we promote from
* keyword in the global section (caplist). If there is match with * Permitted set to Effective only the capabilities, that were marked by user
* caplist and CAP_NET_ADMIN or/and CAP_NET_RAW in this caplist, LSTCHK_NETADM * via 'capset' keyword in the global section (caplist). If there is match with
* will be unset by the same reason. * 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. * 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 * 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(), * a transition to unprivileged user later in prepare_caps_for_setuid(),
* which specially manages its capabilities in that case. * which specially manages its capabilities in that case.
* Always returns 0. Diagnostic warnings will be emitted only, if * Always returns 0. Diagnostic warnings will be emitted only, if
* LSTCHK_NETADM is presented in LSTCHK_NETADM and some failures are * LSTCHK_NETADM/LSTCHK_SYSADM is presented in global.last_checks and some
* encountered. * failures are encountered.
*/ */
int prepare_caps_from_permitted_set(int from_uid, int to_uid, const char *program_name) int prepare_caps_from_permitted_set(int from_uid, int to_uid, const char *program_name)
{ {
@ -99,7 +103,7 @@ int prepare_caps_from_permitted_set(int from_uid, int to_uid, const char *progra
* setcap, see capabilities man page for details. * setcap, see capabilities man page for details.
*/ */
if (capget(&cap_hdr, &start_cap_data) == -1) { if (capget(&cap_hdr, &start_cap_data) == -1) {
if (global.last_checks & LSTCHK_NETADM) if (global.last_checks & (LSTCHK_NETADM | LSTCHK_SYSADM))
ha_diag_warning("Failed to get process capabilities using capget(): %s. " ha_diag_warning("Failed to get process capabilities using capget(): %s. "
"Can't use capabilities that might be set on %s binary " "Can't use capabilities that might be set on %s binary "
"by administrator.\n", strerror(errno), program_name); "by administrator.\n", strerror(errno), program_name);
@ -111,6 +115,11 @@ int prepare_caps_from_permitted_set(int from_uid, int to_uid, const char *progra
return 0; 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 /* second, try to check process permitted set, in this case caplist is
* necessary. Allows to put cap_net_bind_service in process effective * necessary. Allows to put cap_net_bind_service in process effective
* set, if it is in the caplist and also presented in the binary * set, if it is in the caplist and also presented in the binary
@ -121,9 +130,11 @@ int prepare_caps_from_permitted_set(int from_uid, int to_uid, const char *progra
if (capset(&cap_hdr, &start_cap_data) == 0) { if (capset(&cap_hdr, &start_cap_data) == 0) {
if (caplist & ((1 << CAP_NET_ADMIN)|(1 << CAP_NET_RAW))) if (caplist & ((1 << CAP_NET_ADMIN)|(1 << CAP_NET_RAW)))
global.last_checks &= ~LSTCHK_NETADM; global.last_checks &= ~LSTCHK_NETADM;
} else if (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 " ha_diag_warning("Failed to put capabilities from caplist in %s "
"process effective capabilities set using capset(): %s\n", "process Effective capabilities set using capset(): %s\n",
program_name, strerror(errno)); program_name, strerror(errno));
} }
} }
@ -139,7 +150,8 @@ int prepare_caps_from_permitted_set(int from_uid, int to_uid, const char *progra
* - set the effective and permitted caps again * - set the effective and permitted caps again
* - then the caller can safely call setuid() * - then the caller can safely call setuid()
* On success LSTCHK_NETADM is unset from global.last_checks, if CAP_NET_ADMIN * 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. * 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 * 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. * is zero. Returns 0 on success, negative on failure. Alerts may be emitted.
*/ */
@ -185,6 +197,9 @@ int prepare_caps_for_setuid(int from_uid, int to_uid)
if (caplist & ((1 << CAP_NET_ADMIN)|(1 << CAP_NET_RAW))) if (caplist & ((1 << CAP_NET_ADMIN)|(1 << CAP_NET_RAW)))
global.last_checks &= ~LSTCHK_NETADM; global.last_checks &= ~LSTCHK_NETADM;
if (caplist & (1 << CAP_SYS_ADMIN))
global.last_checks &= ~LSTCHK_SYSADM;
/* all's good */ /* all's good */
return 0; return 0;
} }

View File

@ -1241,6 +1241,7 @@ static int srv_parse_namespace(char **args, int *cur_arg,
if (strcmp(arg, "*") == 0) { if (strcmp(arg, "*") == 0) {
/* Use the namespace associated with the connection (if present). */ /* Use the namespace associated with the connection (if present). */
newsrv->flags |= SRV_F_USE_NS_FROM_PP; newsrv->flags |= SRV_F_USE_NS_FROM_PP;
global.last_checks |= LSTCHK_SYSADM;
return 0; return 0;
} }
@ -1259,6 +1260,7 @@ static int srv_parse_namespace(char **args, int *cur_arg,
memprintf(err, "Cannot open namespace '%s'", arg); memprintf(err, "Cannot open namespace '%s'", arg);
return ERR_ALERT | ERR_FATAL; return ERR_ALERT | ERR_FATAL;
} }
global.last_checks |= LSTCHK_SYSADM;
return 0; return 0;
#else #else