Skip to content

Commit

Permalink
MINOR: capabilities: add cap_sys_admin support
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
vkssv authored and wtarreau committed Apr 30, 2024
1 parent 13ef552 commit 5cbb278
Show file tree
Hide file tree
Showing 6 changed files with 64 additions and 43 deletions.
42 changes: 22 additions & 20 deletions doc/management.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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.

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
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.

This was done to avoid all potential use cases when haproxy starts and runs as
root: transparent proxy mode, binding to privileged ports.

'setcap' keyword supports following network capabilities:
- cap_net_admin
- cap_net_raw (subset of cap_net_admin)
- cap_net_bind_service

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
- cap_net_admin: transparent proxying, binding socket to a specific network
interface, using set-mark action;
- 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
to the Effective, if they are not listed as 'setcap' argument. See more
information about 'setcap' keyword and supported capabilities in the chapter
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:

Example:
# 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
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:

Example:
Expand All @@ -4647,20 +4649,20 @@ Example:
See more details about setcap and capabilities sets in Linux man pages
(capabilities(7)).

In some cases like transparent proxying, binding socket to a specific network
interface, using set-mark action, configuration file parser detects that
cap_net_admin or cap_net_raw capabilities are needed. Then, during
initialization stage, haproxy process checks, if these capabilities could be
put in its effective set. If it's not possible due to capget or capset syscall
failure (restrictions set on syscalls by some security modules like SELinux,
Seccomp, etc), process emits diagnostic warnings (start with -dD).
In some use cases like transparent proxying or creating socket in a specific
network namespace, configuration file parser detects that cap_net_raw or
cap_sys_admin or some other supported capabilities are needed. Then, during
the initialization stage, haproxy process checks, if these capabilities could
be put in its Effective set. If it's not possible due to capget or capset
syscall failure (restrictions set on syscalls by some security modules like
SELinux, Seccomp, etc), process emits diagnostic warnings (start with -dD).

Due to support of many different platforms with different system settings,
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
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
capabilities set for haproxy binary.
message like below. It's up to a user to recheck its configuration and haproxy
binary capabilities set.

Example:
$ haproxy -dD -f haproxy.cfg
Expand Down
2 changes: 1 addition & 1 deletion include/haproxy/global-t.h
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@
#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 */
/* unused: 0x00000001 */
#define LSTCHK_SYSADM 0x00000001 /* check that we have CAP_SYS_ADMIN */
#define LSTCHK_NETADM 0x00000002 /* check that we have CAP_NET_ADMIN */

/* Global tuning options */
Expand Down
2 changes: 2 additions & 0 deletions src/cfgparse-tcp.c
Original file line number Diff line number Diff line change
Expand Up @@ -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]);
return ERR_ALERT | ERR_FATAL;
}
global.last_checks |= LSTCHK_SYSADM;

return 0;
}
#endif
Expand Down
10 changes: 5 additions & 5 deletions src/haproxy.c
Original file line number Diff line number Diff line change
Expand Up @@ -3626,13 +3626,13 @@ int main(int argc, char **argv)
if ((global.mode & (MODE_MWORKER | MODE_DAEMON)) == 0)
set_identity(argv[0]);

/* set_identity() above might have dropped LSTCHK_NETADM if
* it changed to a new UID while preserving enough permissions
* to honnor LSTCHK_NETADM.
/* set_identity() above might have dropped LSTCHK_NETADM or/and
* LSTCHK_SYSADM if it changed to a new UID while preserving enough
* 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
* 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.
*/
if (global.uid) {
Expand Down
49 changes: 32 additions & 17 deletions src/linuxcap.c
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,9 @@ static const struct {
#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 }
Expand All @@ -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() */
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.
/* 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
* 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 is presented in LSTCHK_NETADM and some failures are
* encountered.
* 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)
{
Expand All @@ -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.
*/
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. "
"Can't use capabilities that might be set on %s binary "
"by administrator.\n", strerror(errno), program_name);
Expand All @@ -111,6 +115,11 @@ int prepare_caps_from_permitted_set(int from_uid, int to_uid, const char *progra
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
Expand All @@ -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 (caplist & ((1 << CAP_NET_ADMIN)|(1 << CAP_NET_RAW)))
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 "
"process effective capabilities set using capset(): %s\n",
"process Effective capabilities set using capset(): %s\n",
program_name, strerror(errno));
}
}
Expand All @@ -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
* - 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.
* 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.
*/
Expand Down Expand Up @@ -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)))
global.last_checks &= ~LSTCHK_NETADM;

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

/* all's good */
return 0;
}
Expand Down
2 changes: 2 additions & 0 deletions src/server.c
Original file line number Diff line number Diff line change
Expand Up @@ -1241,6 +1241,7 @@ static int srv_parse_namespace(char **args, int *cur_arg,
if (strcmp(arg, "*") == 0) {
/* Use the namespace associated with the connection (if present). */
newsrv->flags |= SRV_F_USE_NS_FROM_PP;
global.last_checks |= LSTCHK_SYSADM;
return 0;
}

Expand All @@ -1259,6 +1260,7 @@ static int srv_parse_namespace(char **args, int *cur_arg,
memprintf(err, "Cannot open namespace '%s'", arg);
return ERR_ALERT | ERR_FATAL;
}
global.last_checks |= LSTCHK_SYSADM;

return 0;
#else
Expand Down

0 comments on commit 5cbb278

Please sign in to comment.