#240 Grant CAP_SYS_PTRACE for gssproxy non-privileged user
Merged 5 years ago by rharwood. Opened 5 years ago by slev.
slev/gssproxy fix_non_root_ptrace  into  master

file modified
+4
@@ -94,6 +94,10 @@ 

      GSS_PROXY_LIBS += $(SELINUX_LIBS)

  endif

  

+ if HAVE_CAP

+     GSS_PROXY_LIBS += $(CAP_LIBS)

+ endif

+ 

  GP_RPCGEN_OBJ = rpcgen/gp_rpc_xdr.c rpcgen/gss_proxy_xdr.c rpcgen/gp_xdr.c

  GP_RPCCLI_OBJ = \

      src/client/gpm_display_status.c \

file modified
+17
@@ -274,3 +274,20 @@ 

                 )

      AM_CONDITIONAL([BUILD_HARDENING], [test x"$with_hardening" = xyes])

    ])

+ 

+ AC_DEFUN([WITH_CAP],

+   [ AC_ARG_WITH([cap],

+                 [AC_HELP_STRING([--with-cap],

+                                 [Whether to build with libcap [no]]

+                                )

+                 ],

+                 [],

+                 with_cap=no

+                )

+     if test x"$with_cap" = xyes; then

+         HAVE_CAP=1

+         AC_SUBST(HAVE_CAP)

+         AC_DEFINE_UNQUOTED([HAVE_CAP], [1], [Build with capabilities support])

+     fi

+   ])

+ AM_CONDITIONAL([HAVE_CAP], [test x$with_cap = xyes])

file modified
+10
@@ -280,6 +280,16 @@ 

               [$GSSAPI_LIBS $GSSRPC_LIBS])

  AC_SUBST([GSSRPC_LIBS])

  

+ WITH_CAP

+ if test x$HAVE_CAP != x; then

+     AC_CHECK_FUNC([prctl],,[AC_MSG_ERROR([Failed to find prctl])])

+     AC_CHECK_LIB([cap], [cap_set_proc],[CAP_LIBS=-lcap],

+                  [AC_MSG_ERROR(["Failed to find libcap symbols"])])

+     AC_SUBST([CAP_LIBS])

+     AC_CHECK_HEADERS([sys/capability.h],,

+                      [AC_MSG_ERROR([Could not find libcap headers])])

+ fi

+ 

  AC_CHECK_FUNCS([__secure_getenv secure_getenv])

  

  WITH_INITSCRIPT

@@ -40,6 +40,7 @@ 

  BuildRequires: keyutils-libs-devel

  BuildRequires: libini_config-devel >= 1.2.0

  BuildRequires: libverto-devel

+ BuildRequires: libcap-devel

  BuildRequires: popt-devel

  BuildRequires: findutils

  BuildRequires: systemd-units

file modified
+202 -11
@@ -1,17 +1,28 @@ 

  /* Copyright (C) 2011,2015 the GSS-PROXY contributors, see COPYING for license */

  

- #include <stdlib.h>

- #include <sys/types.h>

- #include <sys/stat.h>

+ #include <config.h>

+ 

+ #include <errno.h>

+ #include <fcntl.h>

+ #include <grp.h>

  #include <locale.h>

+ #include <pwd.h>

  #include <signal.h>

- #include <fcntl.h>

- #include <errno.h>

+ #include <stdio.h>

+ #include <stdlib.h>

  #include <string.h>

+ #include <sys/stat.h>

+ #include <sys/types.h>

  #include <unistd.h>

- #include <stdio.h>

- #include <pwd.h>

- #include <grp.h>

+ 

+ #ifdef HAVE_CAP

+ 

+ #include <linux/capability.h>

+ #include <sys/capability.h>

+ #include <sys/prctl.h>

+ 

+ #endif

+ 

  #include "gp_proxy.h"

  

  void init_server(bool daemonize, int *wait_fd)
@@ -218,10 +229,29 @@ 

      struct passwd *pw, pws;

      int ret;

  

-     if (cfg->proxy_user == NULL) {

-         /* not dropping privs */

-         return 0;

+ #ifdef HAVE_CAP

+     /* When a thread that has a zero value for one or more of

+      * its user IDs resets all of these values to nonzero

+      * a permitted capability set is also cleared.

+      *

+      * Permitted capability set is used as a limiting superset

+      * for the effective capabilities, which in turn are used

+      * by the kernel to perform permission checks.

+      *

+      * This means that such a thread can never reacquire those

+      * capabilities then.

+      *

+      * To change the default behavior SECBIT_KEEP_CAPS flag can

+      * be used. This flag allows keeping permitted capability set

+      * during UID switching. */

+     ret = prctl(PR_SET_KEEPCAPS, 1, 0, 0, 0);

+     if (ret) {

+         ret = errno;

+         GPDEBUG("Failed to set keep capabilities: [%d:%s]\n",

+                 ret, gp_strerror(ret));

+         return ret;

      }

+ #endif

  

      ret = getpwnam_r(cfg->proxy_user, &pws, buf, 2048, &pw);

      if (ret) {
@@ -253,5 +283,166 @@ 

          return ret;

      }

  

+ #ifdef HAVE_CAP

+     /* keep only CAP_SYS_PTRACE capability,

+      * all the other are redundant */

+     ret = drop_caps();

+     if (ret) {

+         return ret;

+     }

+ 

+     /* restore SECBIT_KEEP_CAPS */

+     if (prctl(PR_SET_KEEPCAPS, 0, 0, 0, 0)) {

+         ret = errno;

+         GPDEBUG("Failed to reset keep capabilities: [%d:%s]\n",

+                 ret, gp_strerror(ret));

+         return ret;

+     }

+ #endif

+ 

      return 0;

  }

+ 

+ #ifdef HAVE_CAP

+ /* The capability bounding set is a security mechanism that is

+  * used to limit the file permitted capabilities. To prevent

+  * applying any of them capability bounding set has to be

+  * constrained.

+  *

+  * To drop capabilities from the bounding set a thread has to

+  * have CAP_SETPCAP capability. */

+ int clear_bound_caps()

+ {

+     cap_t caps = NULL;

+     cap_value_t cap = 0;

+     const cap_value_t setpcap_list[] = { CAP_SETPCAP };

+     int ret;

+ 

+     /* obtain a copy of current process capability sets

+      * to raise CAP_SETPCAP */

+     caps = cap_get_proc();

+     if (caps == NULL) {

+         ret = errno;

+         GPDEBUG("Failed to get current capabilities: [%d:%s]\n",

+                 ret, gp_strerror(ret));

+         goto done;

+     }

+ 

+     /* raise CAP_SETPCAP within an effective set */

+     if (cap_set_flag(caps, CAP_EFFECTIVE, 1, setpcap_list, CAP_SET) == -1) {

+         ret = errno;

+         GPDEBUG("Failed to raise setpcap capability flag: [%d:%s]\n",

+                 ret, gp_strerror(ret));

+         goto done;

+     }

+ 

+     /* apply back our capability sets to the current process */

+     if (cap_set_proc(caps) == -1) {

+         ret = errno;

+         GPDEBUG("Failed to set capabilities: [%d:%s]\n",

+                 ret, gp_strerror(ret));

+         goto done;

+     }

+ 

+     /* having CAP_SETPCAP within an effective set completely drop

+      * the bounding set capability */

+     while (CAP_IS_SUPPORTED(cap)) {

+         if (cap_drop_bound(cap) != 0) {

+             ret = errno;

+             GPDEBUG("Failed to drop bounding set capability: [%d:%s]\n",

+                     ret, gp_strerror(ret));

+             goto done;

+         }

+         cap++;

+     }

+     ret = 0;

+ 

+ done:

+     if (caps && cap_free(caps) == -1) {

+         ret = errno;

+         GPDEBUG("Failed to free capability state: [%d:%s]\n",

+                 ret, gp_strerror(ret));

+     }

+     return ret;

+ }

+ 

+ /* To serve a 'program =' functionality ("If specified, this

+  * service will only match when the program being run is the

+  * specified string.") a non-privileged user has to have a

+  * read permission on "/proc/[PID]/exe". This can be achieved

+  * by CAP_SYS_PTRACE capability.

+  *

+  * For now thread has an effective capability set inherited from

+  * privileged user because of SECBIT_KEEP_CAPS flag. But required

+  * is only CAP_SYS_PTRACE within the effective and permitted sets.

+  * It needs to restrict redundant privileged capabilities of a

+  * non-privileged user. */

+ int drop_caps()

+ {

+     cap_t caps = NULL;

+     int ret;

+     const cap_value_t ptrace_list[] = { CAP_SYS_PTRACE };

+ 

+     /* to limit a set of file permitted capabilities completely

+      * drop the bounding set */

+     ret = clear_bound_caps();

+     if (ret) {

+         goto done;

+     }

+ 

+     ret = CAP_IS_SUPPORTED(CAP_SYS_PTRACE);

+     if (ret == -1) {

+         ret = errno;

+         GPDEBUG("Failed to check if capability is supported: [%d:%s]\n",

+                 ret, gp_strerror(ret));

+         goto done;

+     } else if (!ret) {

+         GPDEBUG("Capability CAPS_SYS_PTRACE is not supported\n");

+         ret = EINVAL;

+         goto done;

+     }

+ 

+     /* allocates a clear capability state */

+     caps = cap_init();

+     if (caps == NULL) {

+         ret = errno;

+         GPDEBUG("Failed to init capabilities: [%d:%s]\n",

+                 ret, gp_strerror(ret));

+         goto done;

+     }

+ 

+     /* to raise CAP_SYS_PTRACE within the effective set a same

+      * capability has to be present within the permitted one */

+     if (cap_set_flag(caps, CAP_PERMITTED, 1, ptrace_list, CAP_SET) == -1) {

+         ret = errno;

+         GPDEBUG("Failed to set permitted ptrace capability flag: [%d:%s]\n",

+                 ret, gp_strerror(ret));

+         goto done;

+     }

+ 

+     /* raise CAP_SYS_PTRACE within the effective set */

+     if (cap_set_flag(caps, CAP_EFFECTIVE, 1, ptrace_list, CAP_SET) == -1) {

+         ret = errno;

+         GPDEBUG("Failed to set effective ptrace capability flag: [%d:%s]\n",

+                 ret, gp_strerror(ret));

+         goto done;

+     }

+ 

+     /* apply our new capability sets to the current process */

+     if (cap_set_proc(caps) == -1) {

+         ret = errno;

+         GPDEBUG("Failed to set capabilities: [%d:%s]\n",

+                 ret, gp_strerror(ret));

+         goto done;

+     }

+     ret = 0;

+ 

+ done:

+     if (caps && cap_free(caps) == -1) {

+         ret = errno;

+         GPDEBUG("Failed to free capability state: [%d:%s]\n",

+                 ret, gp_strerror(ret));

+     }

+     return ret;

+ }

+ #endif

file modified
+4
@@ -102,6 +102,10 @@ 

  void init_proc_nfsd(struct gp_config *cfg);

  void write_pid(void);

  int drop_privs(struct gp_config *cfg);

+ #ifdef HAVE_CAP

+ int drop_caps(void);

+ int clear_bound_caps(void);

+ #endif

  

  /* from gp_socket.c */

  void free_unix_socket(verto_ctx *ctx, verto_ev *ev);

file modified
+8 -4
@@ -269,10 +269,14 @@ 

       * so it can continue with dependencies and start nfsd */

      init_done(wait_fd);

  

-     ret = drop_privs(gpctx->config);

-     if (ret) {

-         ret = EXIT_FAILURE;

-         goto cleanup;

+     /* if config option "run_as_user" is missing, then it's no need to

+      * drop privileges */

+     if (gpctx->config->proxy_user) {

+         ret = drop_privs(gpctx->config);

+         if (ret) {

+             ret = EXIT_FAILURE;

+             goto cleanup;

+         }

      }

  

      ret = gp_workers_init(gpctx);

A non-root user should have CAP_SYS_PTRACE capability to
read '/proc/[PID]/exe'.

Actual capabilities become as expected:

grep Cap /proc/"$(pgrep gssproxy)"/status
CapInh: 0000000000000000
CapPrm: 0000000000080000
CapEff: 0000000000080000
CapBnd: 0000000000000000
CapAmb: 0000000000000000

Reading of "/proc/[PID]/exe" is successful.

Fixes: https://pagure.io/gssproxy/issue/239
Signed-off-by: Stanislav Levin slev@altlinux.org

Thank you for this PR.
The general organization looks good. I haven't fully reviewed the new function yet, however I see there are no comments.
Please ad a general comment before the new function that give a general overview about what is the intention of each function.

Whitin the function please add comments about what are the effects of the calls you make, as the cap library and operation is rather obscure to most people it is valuable to explain along what is going on and why it is being done.

A good comment is:
/ The next call shaves yak ABC because we need its fur for the winter /

A bad comment is:
/ shave_yak() shaves the yak /

Simo.

This error message doesn't match the check (i.e., you don't actually look for cap_set_proc)

Can you sort these while you're here?

Should this be a fatal error? Would like to hear an argument in either direction.

I'm also not convinced this should be fatal.

Thanks for the PR! Supplementing Simo's review with some comments inline.

rebased onto 7922f3c

5 years ago

This error message doesn't match the check (i.e., you don't actually look for cap_set_proc)

Fixed.

Can you sort these while you're here?

Sorted.

Should this be a fatal error? Would like to hear an argument in either direction.

ret = prctl(PR_SET_KEEPCAPS, 1, 0, 0, 0) - without setting of SECBIT_KEEP_CAPS flag it's impossible to raise any of capability after a UID switch because all the cap sets are cleared.

I'm also not convinced this should be fatal.

ret = drop_caps() - because of SECBIT_KEEP_CAPS flag after user id switch the non-privileged user gains all the capabilities in the permitted set.

grep Cap /proc/"$(pgrep gssproxy)"/status
CapInh: 0000000000000000
CapPrm: 0000003fffffffff
CapEff: 0000000000000000
CapBnd: 0000003fffffffff
CapAmb: 0000000000000000
capsh --decode='0000003fffffffff' | tr ',' '\n'
0x0000003fffffffff=cap_chown
cap_dac_override
cap_dac_read_search
cap_fowner
cap_fsetid
cap_kill
cap_setgid
cap_setuid
cap_setpcap
cap_linux_immutable
cap_net_bind_service
cap_net_broadcast
cap_net_admin
cap_net_raw
cap_ipc_lock
cap_ipc_owner
cap_sys_module
cap_sys_rawio
cap_sys_chroot
cap_sys_ptrace
cap_sys_pacct
cap_sys_admin
cap_sys_boot
cap_sys_nice
cap_sys_resource
cap_sys_time
cap_sys_tty_config
cap_mknod
cap_lease
cap_audit_write
cap_audit_control
cap_setfcap
cap_mac_override
cap_mac_admin
cap_syslog
cap_wake_alarm
cap_block_suspend
cap_audit_read

This means that user can raise whatever he wants.

So, i guess both should be fatal as long as gssproxy supports access by the program name.
Another suggestion is using pre-caution about non-privileged user limitations ( this problem and for example

systemctl try-reload-or-restart gssproxy
journalctl -n 5 -u gssproxy -g "Keytab"
Keytab /etc/krb5.keytab has no content (-1765328203)

non-privileged user has no permission to read system keytab.
)

Of course, thank you all for review! 👍

Our style doesn't trail the closing */ - it should be on the line above.

Declaration at top of block, please.

I don't understand this comment, sorry!

I'd prefer goto done; cleanup for this - put the (checked) cap_free() after the done:. In general having fewer exit paths makes leakiness easier to reason about.

Same deal on this function.

Thanks @slev. Some more comments inline, but that should be it for me. Will wait to see what @simo says as well.

rebased onto c5e6189

5 years ago

Our style doesn't trail the closing */ - it should be on the line above.

Done.

Declaration at top of block, please.

Done.

I don't understand this comment, sorry!

Rephrased.

I'd prefer goto done; cleanup for this - put the (checked) cap_free() after the done:. In general having fewer exit paths makes leakiness easier to reason about.

Label applied.

@rharwood , ready for next round :)
Thank you!

The code itself looks ok, however 2 points:
1) We should make cap support optional, seem easy as they are just 2 functions and a configure check
2) Caps should be manipulated only if needed (ie running as user), atm it looks to me this code is called also when gssproxy is running as root with all the needed capabilities, seems unnecessary.

1) I will check
2) if the key "run_as_user" is missing in config then no dropping privileges and no caps setting are executed:

int drop_privs(struct gp_config *cfg)                                           
{                                                                               
    char buf[2048];                                                             
    struct passwd *pw, pws;                                                     
    int ret;                                                                    

    if (cfg->proxy_user == NULL) {                                              
        /* not dropping privs */                                                
        return 0;                                                               
    }
    ...

One more thing. I see that there are no tests for "run_as_user" option. Is there an implementation plan for them?

Ah I forgot you check within the function ... can you make the check explicit before calling the function instead ?

If (cfg->proxy_user) {
    ret = drop_privs(cfg);
    ...
}

As for tests, I do not think we have optional tests, and run_As_user requires to urn as root, but our tests (IIRC) are all built to be run as non root ...

Ah I forgot you check within the function ... can you make the check explicit before calling the function instead ?
If (cfg->proxy_user) {
ret = drop_privs(cfg);
...
}

This code is already in the codebase (not related to my PR). But of course, I can change this.

As for tests, I do not think we have optional tests, and run_As_user requires to urn as root, but our tests (IIRC) are all built to be run as non root ...

Same here. I will try to get a look.

Yes please add a separate commit that moves the check into gssproxy.c so that it is clear from there that we do no privs dropping if run_as_user is not specified.

2 new commits added

  • Check for gssproxy user in config before calling of "drop_privs"
  • Make build with capabilities optional
5 years ago

Final question, I think. You make a change such that we don't go through the drop_privs() logic if we don't have run_as_user set; this is fine with the code in master right now. But isn't there some value in running through the drop_caps() part of that, even if we're still going to run as root? Also, don't we want PR_SET_KEEPCAPS set for the running as root case?

Also, please take a look at the CI failure.

@rharwood I requested the change to not go through drop_privs()

Ah I see. Looking at all the commits at once made this a bit confusing. So there's no actual change, just where the check is.

I think my question still stands though - do we want to run through drop_caps/PR_SET_KEEPCAPS when running as root? Because as written we don't.

(CI needs addressing as well.)

3 new commits added

  • Check for gssproxy user in config before calling of "drop_privs"
  • Make build with capabilities optional
  • Grant CAP_SYS_PTRACE for gssproxy non-privileged user
5 years ago

Ah I see. Looking at all the commits at once made this a bit confusing. So there's no actual change, just where the check is.
I think my question still stands though - do we want to run through drop_caps/PR_SET_KEEPCAPS when running as root? Because as written we don't.

We don't want to drop anything when running as root.
But if someone specifies 'run_as_user=root' then capabilities will be dropped.
I guess that it's not expected behavior.

(CI needs addressing as well.)

Fixed.
Should I drop dependency on libcap in the spec file?

On Wed, 2019-01-09 at 10:12 +0000, stanislav levin wrote:

We don't want to drop anything when running as root.
But if someone specifies 'run_as_user=root' then capabilities will be dropped.
I guess that it's not expected behavior.

I wonder if this is something we want to keep and document actually, so
that people can run as root and drop privs if they want ...

As I understand there are no required changes. Right?

On Wed, 2019-01-09 at 10:12 +0000, stanislav levin wrote:

We don't want to drop anything when running as root.
But if someone specifies 'run_as_user=root' then capabilities will be dropped.
I guess that it's not expected behavior.

I wonder if this is something we want to keep and document actually, so
that people can run as root and drop privs if they want ...

Wait, I'm confused again. Why don't we want to drop caps as root? And if we're considering an option to drop caps... maybe it should be the default?

As I understand there are no required changes. Right?

Not that I'm aware of. Just trying to understand (and agree) on the above. Please bear with us while we bikeshed for a moment :)

(Per IRC conversation, we don't want to drop caps as root because we rely on dac_override and the like.)

I'll merge presently.

Commit 2ca4525 fixes this pull-request

Pull-Request has been merged by rharwood

5 years ago