From 0a9a40abcda602dc3403e2108e1348bf4e04051a Mon Sep 17 00:00:00 2001 From: Nicolas Lacasse Date: Tue, 4 Sep 2018 20:31:52 -0700 Subject: [PATCH] runsc: Run sandbox as user nobody. When starting a sandbox without direct file or network access, we create an empty user namespace and run the sandbox in there. However, the root user in that namespace is still mapped to the root user in the parent namespace. This CL maps the "nobody" user from the parent namespace into the child namespace, and runs the sandbox process as user "nobody" inside the new namespace. PiperOrigin-RevId: 211572223 Change-Id: I1b1f9b1a86c0b4e7e5ca7bc93be7d4887678bab6 --- README.md | 7 ++++--- runsc/boot/config.go | 5 +++++ runsc/sandbox/sandbox.go | 33 +++++++++++++++++++++++++++++++-- runsc/specutils/BUILD | 1 + runsc/specutils/namespace.go | 14 ++++++++++++++ runsc/test/testutil/testutil.go | 15 ++++++++------- 6 files changed, 63 insertions(+), 12 deletions(-) diff --git a/README.md b/README.md index 4ec83ab0c..d85948ce5 100644 --- a/README.md +++ b/README.md @@ -179,14 +179,15 @@ here: `https://storage.googleapis.com/gvisor/releases/nightly/${yyyy-mm-dd}/runsc.sha512` **It is important to copy this binary to some place that is accessible to all -users**, since `runsc` executes itself as user `nobody` to avoid unnecessary -privileges. The `/usr/local/bin` directory is a good choice. +users, and make is executable to all users**, since `runsc` executes itself as +user `nobody` to avoid unnecessary privileges. The `/usr/local/bin` directory is +a good place to put the `runsc` binary. ``` wget https://storage.googleapis.com/gvisor/releases/nightly/latest/runsc wget https://storage.googleapis.com/gvisor/releases/nightly/latest/runsc.sha512 sha512sum -c runsc.sha512 -chmod +x runsc +chmod a+x runsc sudo mv runsc /usr/local/bin ``` diff --git a/runsc/boot/config.go b/runsc/boot/config.go index 212f5b003..87a47dd0b 100644 --- a/runsc/boot/config.go +++ b/runsc/boot/config.go @@ -213,6 +213,11 @@ type Config struct { // PanicSignal register signal handling that panics. Usually set to // SIGUSR2(12) to troubleshoot hangs. -1 disables it. PanicSignal int + + // TestOnlyAllowRunAsCurrentUser should only be used in tests. It + // allows runsc to start the sandbox process as the current user if we + // do not have capability to set uid/gid to another user. + TestOnlyAllowRunAsCurrentUser bool } // ToFlags returns a slice of flags that correspond to the given Config. diff --git a/runsc/sandbox/sandbox.go b/runsc/sandbox/sandbox.go index e0fadefcd..dd5a0aa56 100644 --- a/runsc/sandbox/sandbox.go +++ b/runsc/sandbox/sandbox.go @@ -373,8 +373,8 @@ func (s *Sandbox) createSandboxProcess(spec *specs.Spec, conf *boot.Config, bund // User namespace depends on the following options: // - Host network/filesystem: requires to run inside the user namespace // specified in the spec or the current namespace if none is configured. - // - Gofer: when using a Gofer, the sandbox process can run isolated in an - // empty namespace. + // - Gofer: when using a Gofer, the sandbox process can run isolated in a + // new user namespace with only the "nobody" user and group. if conf.Network == boot.NetworkHost || conf.FileAccess == boot.FileAccessDirect { if userns, ok := specutils.GetNS(specs.UserNamespace, spec); ok { log.Infof("Sandbox will be started in container's user namespace: %+v", userns) @@ -391,6 +391,34 @@ func (s *Sandbox) createSandboxProcess(spec *specs.Spec, conf *boot.Config, bund } else { log.Infof("Sandbox will be started in new user namespace") nss = append(nss, specs.LinuxNamespace{Type: specs.UserNamespace}) + + if conf.TestOnlyAllowRunAsCurrentUser { + log.Warningf("Running sandbox in test mode as current user (uid=%d gid=%d). This is only safe in tests!", os.Getuid(), os.Getgid()) + } else if specutils.CanSetUIDGID() { + // If we have CAP_SETUID and CAP_SETGID, then we can also run + // as user nobody. + + // Map nobody in the new namespace to nobody in the parent namespace. + const nobody = 65534 + cmd.SysProcAttr.UidMappings = []syscall.SysProcIDMap{{ + ContainerID: int(nobody), + HostID: int(nobody), + Size: int(1), + }} + cmd.SysProcAttr.GidMappings = []syscall.SysProcIDMap{{ + ContainerID: int(nobody), + HostID: int(nobody), + Size: int(1), + }} + + // Set credentials to run as user and group nobody. + cmd.SysProcAttr.Credential = &syscall.Credential{ + Uid: nobody, + Gid: nobody, + } + } else { + return fmt.Errorf("can't run sandbox process as user nobody since we don't have CAP_SETUID or CAP_SETGID") + } } // Log the fds we are donating to the sandbox process. @@ -399,6 +427,7 @@ func (s *Sandbox) createSandboxProcess(spec *specs.Spec, conf *boot.Config, bund } log.Debugf("Starting sandbox: %s %v", binPath, cmd.Args) + log.Debugf("SysProcAttr: %+v", cmd.SysProcAttr) if err := specutils.StartInNS(cmd, nss); err != nil { return err } diff --git a/runsc/specutils/BUILD b/runsc/specutils/BUILD index 97a504b20..e73b2293f 100644 --- a/runsc/specutils/BUILD +++ b/runsc/specutils/BUILD @@ -18,6 +18,7 @@ go_library( "//pkg/sentry/kernel/auth", "@com_github_cenkalti_backoff//:go_default_library", "@com_github_opencontainers_runtime-spec//specs-go:go_default_library", + "@com_github_syndtr_gocapability//capability:go_default_library", "@org_golang_x_sys//unix:go_default_library", ], ) diff --git a/runsc/specutils/namespace.go b/runsc/specutils/namespace.go index 80eaad965..356943a65 100644 --- a/runsc/specutils/namespace.go +++ b/runsc/specutils/namespace.go @@ -23,6 +23,7 @@ import ( "syscall" specs "github.com/opencontainers/runtime-spec/specs-go" + "github.com/syndtr/gocapability/capability" "golang.org/x/sys/unix" "gvisor.googlesource.com/gvisor/pkg/log" ) @@ -202,3 +203,16 @@ func SetUIDGIDMappings(cmd *exec.Cmd, s *specs.Spec) { }) } } + +// CanSetUIDGID returns true if the user has SETUID and SETGID capabilities. +func CanSetUIDGID() bool { + caps, err := capability.NewPid2(os.Getpid()) + if err != nil { + return false + } + if err := caps.Load(); err != nil { + return false + } + return caps.Get(capability.EFFECTIVE, capability.CAP_SETUID) && + caps.Get(capability.EFFECTIVE, capability.CAP_SETGID) +} diff --git a/runsc/test/testutil/testutil.go b/runsc/test/testutil/testutil.go index 4429b981b..77bd56912 100644 --- a/runsc/test/testutil/testutil.go +++ b/runsc/test/testutil/testutil.go @@ -104,13 +104,14 @@ func FindFile(path string) (string, error) { // TestConfig return the default configuration to use in tests. func TestConfig() *boot.Config { return &boot.Config{ - Debug: true, - LogFormat: "text", - LogPackets: true, - Network: boot.NetworkNone, - Strace: true, - MultiContainer: true, - FileAccess: boot.FileAccessProxyExclusive, + Debug: true, + LogFormat: "text", + LogPackets: true, + Network: boot.NetworkNone, + Strace: true, + MultiContainer: true, + FileAccess: boot.FileAccessProxyExclusive, + TestOnlyAllowRunAsCurrentUser: true, } }