Browse code

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 authored on 2016/02/22 14:31:21
Showing 13 changed files
... ...
@@ -50,6 +50,7 @@ type Container struct {
50 50
 	ShmPath         string
51 51
 	ResolvConfPath  string
52 52
 	SeccompProfile  string
53
+	NoNewPrivileges bool
53 54
 }
54 55
 
55 56
 // CreateDaemonEnvironment returns the list of all environment variables given the list of
56 57
new file mode 100644
... ...
@@ -0,0 +1,9 @@
0
+FROM buildpack-deps:jessie
1
+
2
+COPY . /usr/src/
3
+
4
+WORKDIR /usr/src/
5
+
6
+RUN gcc -g -Wall -static nnp-test.c -o /usr/bin/nnp-test
7
+
8
+RUN chmod +s /usr/bin/nnp-test
0 9
new file mode 100644
... ...
@@ -0,0 +1,10 @@
0
+#include <stdio.h>
1
+#include <unistd.h>
2
+#include <sys/types.h>
3
+
4
+int main(int argc, char *argv[])
5
+{
6
+        printf("EUID=%d\n", geteuid());
7
+        return 0;
8
+}
9
+
... ...
@@ -270,6 +270,7 @@ func (daemon *Daemon) populateCommand(c *container.Container, env []string) erro
270 270
 		SeccompProfile:     c.SeccompProfile,
271 271
 		UIDMapping:         uidMap,
272 272
 		UTS:                uts,
273
+		NoNewPrivileges:    c.NoNewPrivileges,
273 274
 	}
274 275
 	if c.HostConfig.CgroupParent != "" {
275 276
 		c.Command.CgroupParent = c.HostConfig.CgroupParent
... ...
@@ -75,17 +75,23 @@ func parseSecurityOpt(container *container.Container, config *containertypes.Hos
75 75
 	for _, opt := range config.SecurityOpt {
76 76
 		con := strings.SplitN(opt, ":", 2)
77 77
 		if len(con) == 1 {
78
-			return fmt.Errorf("Invalid --security-opt: %q", opt)
79
-		}
80
-		switch con[0] {
81
-		case "label":
82
-			labelOpts = append(labelOpts, con[1])
83
-		case "apparmor":
84
-			container.AppArmorProfile = con[1]
85
-		case "seccomp":
86
-			container.SeccompProfile = con[1]
87
-		default:
88
-			return fmt.Errorf("Invalid --security-opt: %q", opt)
78
+			switch con[0] {
79
+			case "no-new-privileges":
80
+				container.NoNewPrivileges = true
81
+			default:
82
+				return fmt.Errorf("Invalid --security-opt 1: %q", opt)
83
+			}
84
+		} else {
85
+			switch con[0] {
86
+			case "label":
87
+				labelOpts = append(labelOpts, con[1])
88
+			case "apparmor":
89
+				container.AppArmorProfile = con[1]
90
+			case "seccomp":
91
+				container.SeccompProfile = con[1]
92
+			default:
93
+				return fmt.Errorf("Invalid --security-opt 2: %q", opt)
94
+			}
89 95
 		}
90 96
 	}
91 97
 
... ...
@@ -124,6 +124,7 @@ type Command struct {
124 124
 	SeccompProfile     string            `json:"seccomp_profile"`
125 125
 	UIDMapping         []idtools.IDMap   `json:"uidmapping"`
126 126
 	UTS                *UTS              `json:"uts"`
127
+	NoNewPrivileges    bool              `json:"no_new_privileges"`
127 128
 }
128 129
 
129 130
 // SetRootPropagation sets the root mount propagation mode.
... ...
@@ -122,6 +122,8 @@ func (d *Driver) createContainer(c *execdriver.Command, hooks execdriver.Hooks)
122 122
 
123 123
 	d.setupLabels(container, c)
124 124
 	d.setupRlimits(container, c)
125
+
126
+	container.NoNewPrivileges = c.NoNewPrivileges
125 127
 	return container, nil
126 128
 }
127 129
 
... ...
@@ -605,6 +605,8 @@ with the same logic -- if the original volume was specified with a name it will
605 605
     --security-opt="label:disable"     : Turn off label confinement for the container
606 606
     --security-opt="apparmor:PROFILE"  : Set the apparmor profile to be applied
607 607
                                          to the container
608
+    --security-opt="no-new-privileges" : Disable container processes from gaining
609
+                                         new privileges
608 610
 
609 611
 You can override the default labeling scheme for each container by specifying
610 612
 the `--security-opt` flag. For example, you can specify the MCS/MLS level, a
... ...
@@ -631,6 +633,13 @@ command:
631 631
 
632 632
 > **Note**: You would have to write policy defining a `svirt_apache_t` type.
633 633
 
634
+If you want to prevent your container processes from gaining additional
635
+privileges, you can execute the following command:
636
+
637
+    $ docker run --security-opt no-new-privileges -it centos bash
638
+
639
+For more details, see [kernel documentation](https://www.kernel.org/doc/Documentation/prctl/no_new_privs.txt).
640
+
634 641
 ## Specifying custom cgroups
635 642
 
636 643
 Using the `--cgroup-parent` flag, you can pass a specific cgroup to run a
637 644
new file mode 100644
... ...
@@ -0,0 +1,22 @@
0
+#!/bin/bash
1
+set -e
2
+
3
+# Build a C binary for testing no-new-privileges
4
+# and compile it for target daemon
5
+if [ "$DOCKER_ENGINE_GOOS" = "linux" ]; then
6
+	if [ "$DOCKER_ENGINE_OSARCH" = "$DOCKER_CLIENT_OSARCH" ]; then
7
+		tmpdir=$(mktemp -d)
8
+		gcc -g -Wall -static contrib/nnp-test/nnp-test.c -o "${tmpdir}/nnp-test"
9
+
10
+		dockerfile="${tmpdir}/Dockerfile"
11
+		cat <<-EOF > "$dockerfile"
12
+		FROM debian:jessie
13
+		COPY . /usr/bin/
14
+		RUN chmod +s /usr/bin/nnp-test
15
+		EOF
16
+		docker build --force-rm ${DOCKER_BUILD_ARGS} -qt nnp-test "${tmpdir}" > /dev/null
17
+		rm -rf "${tmpdir}"
18
+	else
19
+		docker build ${DOCKER_BUILD_ARGS} -qt nnp-test contrib/nnp-test > /dev/null
20
+	fi
21
+fi
... ...
@@ -7,6 +7,7 @@ if [ $DOCKER_ENGINE_GOOS != "windows" ]; then
7 7
 	bundle .ensure-frozen-images
8 8
 	bundle .ensure-httpserver
9 9
 	bundle .ensure-syscall-test
10
+	bundle .ensure-nnp-test
10 11
 else
11 12
 	# Note this is Windows to Windows CI, not Windows to Linux CI
12 13
 	bundle .ensure-frozen-images-windows
... ...
@@ -895,6 +895,18 @@ func (s *DockerSuite) TestRunSeccompDefaultProfile(c *check.C) {
895 895
 	}
896 896
 }
897 897
 
898
+// TestRunNoNewPrivSetuid checks that --security-opt=no-new-privileges prevents
899
+// effective uid transtions on executing setuid binaries.
900
+func (s *DockerSuite) TestRunNoNewPrivSetuid(c *check.C) {
901
+	testRequires(c, DaemonIsLinux, NotUserNamespace, SameHostDaemon)
902
+
903
+	// test that running a setuid binary results in no effective uid transition
904
+	runCmd := exec.Command(dockerBinary, "run", "--security-opt", "no-new-privileges", "--user", "1000", "nnp-test", "/usr/bin/nnp-test")
905
+	if out, _, err := runCommandWithOutput(runCmd); err != nil || !strings.Contains(out, "EUID=1000") {
906
+		c.Fatalf("expected output to contain EUID=1000, got %s: %v", out, err)
907
+	}
908
+}
909
+
898 910
 func (s *DockerSuite) TestRunApparmorProcDirectory(c *check.C) {
899 911
 	testRequires(c, SameHostDaemon, Apparmor)
900 912
 
... ...
@@ -459,6 +459,8 @@ its root filesystem mounted as read only prohibiting any writes.
459 459
     "label:type:TYPE"   : Set the label type for the container
460 460
     "label:level:LEVEL" : Set the label level for the container
461 461
     "label:disable"     : Turn off label confinement for the container
462
+    "no-new-privileges" : Disable container processes from gaining additional privileges
463
+
462 464
 
463 465
 **--stop-signal**=*SIGTERM*
464 466
   Signal to stop a container. Default is SIGTERM.
... ...
@@ -500,8 +500,8 @@ func parseLoggingOpts(loggingDriver string, loggingOpts []string) (map[string]st
500 500
 func parseSecurityOpts(securityOpts []string) ([]string, error) {
501 501
 	for key, opt := range securityOpts {
502 502
 		con := strings.SplitN(opt, ":", 2)
503
-		if len(con) == 1 {
504
-			return securityOpts, fmt.Errorf("invalid --security-opt: %q", opt)
503
+		if len(con) == 1 && con[0] != "no-new-privileges" {
504
+			return securityOpts, fmt.Errorf("Invalid --security-opt: %q", opt)
505 505
 		}
506 506
 		if con[0] == "seccomp" && con[1] != "unconfined" {
507 507
 			f, err := ioutil.ReadFile(con[1])