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>
| ... | ... |
@@ -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. |
| ... | ... |
@@ -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]) |