Ver código fonte

Add support for NoNewPrivileges in docker

Signed-off-by: Mrunal Patel <mrunalp@gmail.com>

Add tests for no-new-privileges

Signed-off-by: Mrunal Patel <mrunalp@gmail.com>

Update documentation for no-new-privileges

Signed-off-by: Mrunal Patel <mrunalp@gmail.com>
Mrunal Patel 9 anos atrás
pai
commit
74bb1ce9e9

+ 1 - 0
container/container_unix.go

@@ -50,6 +50,7 @@ type Container struct {
 	ShmPath         string
 	ResolvConfPath  string
 	SeccompProfile  string
+	NoNewPrivileges bool
 }
 
 // CreateDaemonEnvironment returns the list of all environment variables given the list of

+ 9 - 0
contrib/nnp-test/Dockerfile

@@ -0,0 +1,9 @@
+FROM buildpack-deps:jessie
+
+COPY . /usr/src/
+
+WORKDIR /usr/src/
+
+RUN gcc -g -Wall -static nnp-test.c -o /usr/bin/nnp-test
+
+RUN chmod +s /usr/bin/nnp-test

+ 10 - 0
contrib/nnp-test/nnp-test.c

@@ -0,0 +1,10 @@
+#include <stdio.h>
+#include <unistd.h>
+#include <sys/types.h>
+
+int main(int argc, char *argv[])
+{
+        printf("EUID=%d\n", geteuid());
+        return 0;
+}
+

+ 1 - 0
daemon/container_operations_unix.go

@@ -270,6 +270,7 @@ func (daemon *Daemon) populateCommand(c *container.Container, env []string) erro
 		SeccompProfile:     c.SeccompProfile,
 		UIDMapping:         uidMap,
 		UTS:                uts,
+		NoNewPrivileges:    c.NoNewPrivileges,
 	}
 	if c.HostConfig.CgroupParent != "" {
 		c.Command.CgroupParent = c.HostConfig.CgroupParent

+ 17 - 11
daemon/daemon_unix.go

@@ -75,17 +75,23 @@ func parseSecurityOpt(container *container.Container, config *containertypes.Hos
 	for _, opt := range config.SecurityOpt {
 		con := strings.SplitN(opt, ":", 2)
 		if len(con) == 1 {
-			return fmt.Errorf("Invalid --security-opt: %q", opt)
-		}
-		switch con[0] {
-		case "label":
-			labelOpts = append(labelOpts, con[1])
-		case "apparmor":
-			container.AppArmorProfile = con[1]
-		case "seccomp":
-			container.SeccompProfile = con[1]
-		default:
-			return fmt.Errorf("Invalid --security-opt: %q", opt)
+			switch con[0] {
+			case "no-new-privileges":
+				container.NoNewPrivileges = true
+			default:
+				return fmt.Errorf("Invalid --security-opt 1: %q", opt)
+			}
+		} else {
+			switch con[0] {
+			case "label":
+				labelOpts = append(labelOpts, con[1])
+			case "apparmor":
+				container.AppArmorProfile = con[1]
+			case "seccomp":
+				container.SeccompProfile = con[1]
+			default:
+				return fmt.Errorf("Invalid --security-opt 2: %q", opt)
+			}
 		}
 	}
 

+ 1 - 0
daemon/execdriver/driver_unix.go

@@ -124,6 +124,7 @@ type Command struct {
 	SeccompProfile     string            `json:"seccomp_profile"`
 	UIDMapping         []idtools.IDMap   `json:"uidmapping"`
 	UTS                *UTS              `json:"uts"`
+	NoNewPrivileges    bool              `json:"no_new_privileges"`
 }
 
 // SetRootPropagation sets the root mount propagation mode.

+ 2 - 0
daemon/execdriver/native/create.go

@@ -122,6 +122,8 @@ func (d *Driver) createContainer(c *execdriver.Command, hooks execdriver.Hooks)
 
 	d.setupLabels(container, c)
 	d.setupRlimits(container, c)
+
+	container.NoNewPrivileges = c.NoNewPrivileges
 	return container, nil
 }
 

+ 9 - 0
docs/reference/run.md

@@ -605,6 +605,8 @@ with the same logic -- if the original volume was specified with a name it will
     --security-opt="label:disable"     : Turn off label confinement for the container
     --security-opt="apparmor:PROFILE"  : Set the apparmor profile to be applied
                                          to the container
+    --security-opt="no-new-privileges" : Disable container processes from gaining
+                                         new privileges
 
 You can override the default labeling scheme for each container by specifying
 the `--security-opt` flag. For example, you can specify the MCS/MLS level, a
@@ -631,6 +633,13 @@ command:
 
 > **Note**: You would have to write policy defining a `svirt_apache_t` type.
 
+If you want to prevent your container processes from gaining additional
+privileges, you can execute the following command:
+
+    $ docker run --security-opt no-new-privileges -it centos bash
+
+For more details, see [kernel documentation](https://www.kernel.org/doc/Documentation/prctl/no_new_privs.txt).
+
 ## Specifying custom cgroups
 
 Using the `--cgroup-parent` flag, you can pass a specific cgroup to run a

+ 22 - 0
hack/make/.ensure-nnp-test

@@ -0,0 +1,22 @@
+#!/bin/bash
+set -e
+
+# Build a C binary for testing no-new-privileges
+# and compile it for target daemon
+if [ "$DOCKER_ENGINE_GOOS" = "linux" ]; then
+	if [ "$DOCKER_ENGINE_OSARCH" = "$DOCKER_CLIENT_OSARCH" ]; then
+		tmpdir=$(mktemp -d)
+		gcc -g -Wall -static contrib/nnp-test/nnp-test.c -o "${tmpdir}/nnp-test"
+
+		dockerfile="${tmpdir}/Dockerfile"
+		cat <<-EOF > "$dockerfile"
+		FROM debian:jessie
+		COPY . /usr/bin/
+		RUN chmod +s /usr/bin/nnp-test
+		EOF
+		docker build --force-rm ${DOCKER_BUILD_ARGS} -qt nnp-test "${tmpdir}" > /dev/null
+		rm -rf "${tmpdir}"
+	else
+		docker build ${DOCKER_BUILD_ARGS} -qt nnp-test contrib/nnp-test > /dev/null
+	fi
+fi

+ 1 - 0
hack/make/.integration-daemon-setup

@@ -7,6 +7,7 @@ if [ $DOCKER_ENGINE_GOOS != "windows" ]; then
 	bundle .ensure-frozen-images
 	bundle .ensure-httpserver
 	bundle .ensure-syscall-test
+	bundle .ensure-nnp-test
 else
 	# Note this is Windows to Windows CI, not Windows to Linux CI
 	bundle .ensure-frozen-images-windows

+ 12 - 0
integration-cli/docker_cli_run_unix_test.go

@@ -895,6 +895,18 @@ func (s *DockerSuite) TestRunSeccompDefaultProfile(c *check.C) {
 	}
 }
 
+// TestRunNoNewPrivSetuid checks that --security-opt=no-new-privileges prevents
+// effective uid transtions on executing setuid binaries.
+func (s *DockerSuite) TestRunNoNewPrivSetuid(c *check.C) {
+	testRequires(c, DaemonIsLinux, NotUserNamespace, SameHostDaemon)
+
+	// test that running a setuid binary results in no effective uid transition
+	runCmd := exec.Command(dockerBinary, "run", "--security-opt", "no-new-privileges", "--user", "1000", "nnp-test", "/usr/bin/nnp-test")
+	if out, _, err := runCommandWithOutput(runCmd); err != nil || !strings.Contains(out, "EUID=1000") {
+		c.Fatalf("expected output to contain EUID=1000, got %s: %v", out, err)
+	}
+}
+
 func (s *DockerSuite) TestRunApparmorProcDirectory(c *check.C) {
 	testRequires(c, SameHostDaemon, Apparmor)
 

+ 2 - 0
man/docker-run.1.md

@@ -459,6 +459,8 @@ its root filesystem mounted as read only prohibiting any writes.
     "label:type:TYPE"   : Set the label type for the container
     "label:level:LEVEL" : Set the label level for the container
     "label:disable"     : Turn off label confinement for the container
+    "no-new-privileges" : Disable container processes from gaining additional privileges
+
 
 **--stop-signal**=*SIGTERM*
   Signal to stop a container. Default is SIGTERM.

+ 2 - 2
runconfig/opts/parse.go

@@ -500,8 +500,8 @@ func parseLoggingOpts(loggingDriver string, loggingOpts []string) (map[string]st
 func parseSecurityOpts(securityOpts []string) ([]string, error) {
 	for key, opt := range securityOpts {
 		con := strings.SplitN(opt, ":", 2)
-		if len(con) == 1 {
-			return securityOpts, fmt.Errorf("invalid --security-opt: %q", opt)
+		if len(con) == 1 && con[0] != "no-new-privileges" {
+			return securityOpts, fmt.Errorf("Invalid --security-opt: %q", opt)
 		}
 		if con[0] == "seccomp" && con[1] != "unconfined" {
 			f, err := ioutil.ReadFile(con[1])