Browse code

Merge pull request #6968 from vieux/cap_add_drop

Add support for --cap-add and --cap-drop

Michael Crosby authored on 2014/07/15 02:42:29
Showing 14 changed files
... ...
@@ -254,6 +254,8 @@ func populateCommand(c *Container, env []string) error {
254 254
 		Resources:          resources,
255 255
 		AllowedDevices:     allowedDevices,
256 256
 		AutoCreatedDevices: autoCreatedDevices,
257
+		CapAdd:             c.hostConfig.CapAdd,
258
+		CapDrop:            c.hostConfig.CapDrop,
257 259
 	}
258 260
 	c.command.SysProcAttr = &syscall.SysProcAttr{Setsid: true}
259 261
 	c.command.Env = env
... ...
@@ -60,6 +60,8 @@ type InitArgs struct {
60 60
 	Console    string
61 61
 	Pipe       int
62 62
 	Root       string
63
+	CapAdd     string
64
+	CapDrop    string
63 65
 }
64 66
 
65 67
 // Driver specific information based on
... ...
@@ -140,6 +142,8 @@ type Command struct {
140 140
 	Mounts             []Mount             `json:"mounts"`
141 141
 	AllowedDevices     []*devices.Device   `json:"allowed_devices"`
142 142
 	AutoCreatedDevices []*devices.Device   `json:"autocreated_devices"`
143
+	CapAdd             []string            `json:"cap_add"`
144
+	CapDrop            []string            `json:"cap_drop"`
143 145
 
144 146
 	Terminal     Terminal `json:"-"`             // standard or tty terminal
145 147
 	Console      string   `json:"-"`             // dev/console path
... ...
@@ -122,6 +122,14 @@ func (d *driver) Run(c *execdriver.Command, pipes *execdriver.Pipes, startCallba
122 122
 		params = append(params, "-w", c.WorkingDir)
123 123
 	}
124 124
 
125
+	if len(c.CapAdd) > 0 {
126
+		params = append(params, "-cap-add", strings.Join(c.CapAdd, " "))
127
+	}
128
+
129
+	if len(c.CapDrop) > 0 {
130
+		params = append(params, "-cap-drop", strings.Join(c.CapDrop, " "))
131
+	}
132
+
125 133
 	params = append(params, "--", c.Entrypoint)
126 134
 	params = append(params, c.Arguments...)
127 135
 
... ...
@@ -4,6 +4,7 @@ package lxc
4 4
 
5 5
 import (
6 6
 	"fmt"
7
+	"strings"
7 8
 	"syscall"
8 9
 
9 10
 	"github.com/docker/libcontainer/namespaces"
... ...
@@ -48,8 +49,13 @@ func finalizeNamespace(args *execdriver.InitArgs) error {
48 48
 			return fmt.Errorf("clear keep caps %s", err)
49 49
 		}
50 50
 
51
+		caps, err := execdriver.TweakCapabilities(container.Capabilities, strings.Split(args.CapAdd, " "), strings.Split(args.CapDrop, " "))
52
+		if err != nil {
53
+			return err
54
+		}
55
+
51 56
 		// drop all other capabilities
52
-		if err := capabilities.DropCapabilities(container.Capabilities); err != nil {
57
+		if err := capabilities.DropCapabilities(caps); err != nil {
53 58
 			return fmt.Errorf("drop capabilities %s", err)
54 59
 		}
55 60
 	}
... ...
@@ -42,6 +42,10 @@ func (d *driver) createContainer(c *execdriver.Command) (*libcontainer.Config, e
42 42
 		if err := d.setPrivileged(container); err != nil {
43 43
 			return nil, err
44 44
 		}
45
+	} else {
46
+		if err := d.setCapabilities(container, c); err != nil {
47
+			return nil, err
48
+		}
45 49
 	}
46 50
 
47 51
 	if err := d.setupCgroups(container, c); err != nil {
... ...
@@ -136,6 +140,11 @@ func (d *driver) setPrivileged(container *libcontainer.Config) (err error) {
136 136
 	return nil
137 137
 }
138 138
 
139
+func (d *driver) setCapabilities(container *libcontainer.Config, c *execdriver.Command) (err error) {
140
+	container.Capabilities, err = execdriver.TweakCapabilities(container.Capabilities, c.CapAdd, c.CapDrop)
141
+	return err
142
+}
143
+
139 144
 func (d *driver) setupCgroups(container *libcontainer.Config, c *execdriver.Command) error {
140 145
 	if c.Resources != nil {
141 146
 		container.Cgroups.CpuShares = c.Resources.CpuShares
142 147
new file mode 100644
... ...
@@ -0,0 +1,63 @@
0
+package execdriver
1
+
2
+import (
3
+	"fmt"
4
+	"strings"
5
+
6
+	"github.com/docker/libcontainer/security/capabilities"
7
+	"github.com/dotcloud/docker/utils"
8
+)
9
+
10
+func TweakCapabilities(basics, adds, drops []string) ([]string, error) {
11
+	var (
12
+		newCaps []string
13
+		allCaps = capabilities.GetAllCapabilities()
14
+	)
15
+
16
+	// look for invalid cap in the drop list
17
+	for _, cap := range drops {
18
+		if strings.ToLower(cap) == "all" {
19
+			continue
20
+		}
21
+		if !utils.StringsContainsNoCase(allCaps, cap) {
22
+			return nil, fmt.Errorf("Unknown capability: %s", cap)
23
+		}
24
+	}
25
+
26
+	// handle --cap-add=all
27
+	if utils.StringsContainsNoCase(adds, "all") {
28
+		basics = capabilities.GetAllCapabilities()
29
+	}
30
+
31
+	if !utils.StringsContainsNoCase(drops, "all") {
32
+		for _, cap := range basics {
33
+			// skip `all` aready handled above
34
+			if strings.ToLower(cap) == "all" {
35
+				continue
36
+			}
37
+
38
+			// if we don't drop `all`, add back all the non-dropped caps
39
+			if !utils.StringsContainsNoCase(drops, cap) {
40
+				newCaps = append(newCaps, cap)
41
+			}
42
+		}
43
+	}
44
+
45
+	for _, cap := range adds {
46
+		// skip `all` aready handled above
47
+		if strings.ToLower(cap) == "all" {
48
+			continue
49
+		}
50
+
51
+		// look for invalid cap in the drop list
52
+		if !utils.StringsContainsNoCase(allCaps, cap) {
53
+			return nil, fmt.Errorf("Unknown capability: %s", cap)
54
+		}
55
+
56
+		// add cap if not already in the list
57
+		if !utils.StringsContainsNoCase(newCaps, cap) {
58
+			newCaps = append(newCaps, cap)
59
+		}
60
+	}
61
+	return newCaps, nil
62
+}
... ...
@@ -43,6 +43,12 @@ You can now use the `stop` parameter to stop running containers before removal
43 43
 **New!**
44 44
 You can now use the `kill` parameter to kill running containers before removal.
45 45
 
46
+`POST /containers/(id)/start`
47
+
48
+**New!**
49
+The `hostConfig` option now accepts the field `CapAdd`, which specifies a list of capabilities
50
+to add, and the field `CapDrop`, which specifies a list of capabilities to drop.
51
+
46 52
 ## v1.13
47 53
 
48 54
 ### Full Documentation
... ...
@@ -241,7 +241,9 @@ Return low-level information on the container `id`
241 241
                             ]
242 242
                          },
243 243
                          "Links": ["/name:alias"],
244
-                         "PublishAllPorts": false
244
+                         "PublishAllPorts": false,
245
+                         "CapAdd: ["NET_ADMIN"],
246
+                         "CapDrop: ["MKNOD"]
245 247
                      }
246 248
         }
247 249
 
... ...
@@ -410,7 +412,9 @@ Start the container `id`
410 410
              "PublishAllPorts":false,
411 411
              "Privileged":false,
412 412
              "Dns": ["8.8.8.8"],
413
-             "VolumesFrom": ["parent", "other:ro"]
413
+             "VolumesFrom": ["parent", "other:ro"],
414
+             "CapAdd: ["NET_ADMIN"],
415
+             "CapDrop: ["MKNOD"]
414 416
         }
415 417
 
416 418
     **Example response**:
... ...
@@ -55,7 +55,7 @@ following options.
55 55
  - [Network Settings](#network-settings)
56 56
  - [Clean Up (--rm)](#clean-up-rm)
57 57
  - [Runtime Constraints on CPU and Memory](#runtime-constraints-on-cpu-and-memory)
58
- - [Runtime Privilege and LXC Configuration](#runtime-privilege-and-lxc-configuration)
58
+ - [Runtime Privilege, Linux Capabilities, and LXC Configuration](#runtime-privilege-linux-capabilities-and-lxc-configuration)
59 59
 
60 60
 ## Detached vs Foreground
61 61
 
... ...
@@ -222,8 +222,10 @@ get the same proportion of CPU cycles, but you can tell the kernel to
222 222
 give more shares of CPU time to one or more containers when you start
223 223
 them via Docker.
224 224
 
225
-## Runtime Privilege and LXC Configuration
225
+## Runtime Privilege, Linux Capabilities, and LXC Configuration
226 226
 
227
+    --cap-add: Add Linux capabilities
228
+    --cap-drop: Drop Linux capabilities
227 229
     --privileged=false: Give extended privileges to this container
228 230
     --lxc-conf=[]: (lxc exec-driver only) Add custom lxc options --lxc-conf="lxc.cgroup.cpuset.cpus = 0,1"
229 231
 
... ...
@@ -242,6 +244,16 @@ host as processes running outside containers on the host. Additional
242 242
 information about running with `--privileged` is available on the
243 243
 [Docker Blog](http://blog.docker.com/2013/09/docker-can-now-run-within-docker/).
244 244
 
245
+In addition to `--privileged`, the operator can have fine grain control over the
246
+capabilities using `--cap-add` and `--cap-drop`. By default, Docker has a default
247
+list of capabilities that are kept. Both flags support the value `all`, so if the
248
+operator wants to have all capabilities but `MKNOD` they could use:
249
+
250
+    $ docker run --cap-add=ALL --cap-drop=MKNOD ...
251
+
252
+For interacting with the network stack, instead of using `--privileged` they
253
+should use `--cap-add=NET_ADMIN` to modify the network interfaces.
254
+
245 255
 If the Docker daemon was started using the `lxc` exec-driver
246 256
 (`docker -d --exec-driver=lxc`) then the operator can also specify LXC options
247 257
 using one or more `--lxc-conf` parameters. These can be new parameters or
... ...
@@ -783,6 +783,116 @@ func TestUnPrivilegedCanMknod(t *testing.T) {
783 783
 	logDone("run - test un-privileged can mknod")
784 784
 }
785 785
 
786
+func TestCapDropInvalid(t *testing.T) {
787
+	cmd := exec.Command(dockerBinary, "run", "--cap-drop=CHPASS", "busybox", "ls")
788
+	out, _, err := runCommandWithOutput(cmd)
789
+	if err == nil {
790
+		t.Fatal(err, out)
791
+	}
792
+
793
+	logDone("run - test --cap-drop=CHPASS invalid")
794
+}
795
+
796
+func TestCapDropCannotMknod(t *testing.T) {
797
+	cmd := exec.Command(dockerBinary, "run", "--cap-drop=MKNOD", "busybox", "sh", "-c", "mknod /tmp/sda b 8 0 && echo ok")
798
+	out, _, err := runCommandWithOutput(cmd)
799
+	if err == nil {
800
+		t.Fatal(err, out)
801
+	}
802
+
803
+	if actual := strings.Trim(out, "\r\n"); actual == "ok" {
804
+		t.Fatalf("expected output not ok received %s", actual)
805
+	}
806
+	deleteAllContainers()
807
+
808
+	logDone("run - test --cap-drop=MKNOD cannot mknod")
809
+}
810
+
811
+func TestCapDropALLCannotMknod(t *testing.T) {
812
+	cmd := exec.Command(dockerBinary, "run", "--cap-drop=ALL", "busybox", "sh", "-c", "mknod /tmp/sda b 8 0 && echo ok")
813
+	out, _, err := runCommandWithOutput(cmd)
814
+	if err == nil {
815
+		t.Fatal(err, out)
816
+	}
817
+
818
+	if actual := strings.Trim(out, "\r\n"); actual == "ok" {
819
+		t.Fatalf("expected output not ok received %s", actual)
820
+	}
821
+	deleteAllContainers()
822
+
823
+	logDone("run - test --cap-drop=ALL cannot mknod")
824
+}
825
+
826
+func TestCapDropALLAddMknodCannotMknod(t *testing.T) {
827
+	cmd := exec.Command(dockerBinary, "run", "--cap-drop=ALL", "--cap-add=MKNOD", "busybox", "sh", "-c", "mknod /tmp/sda b 8 0 && echo ok")
828
+	out, _, err := runCommandWithOutput(cmd)
829
+	if err != nil {
830
+		t.Fatal(err, out)
831
+	}
832
+
833
+	if actual := strings.Trim(out, "\r\n"); actual != "ok" {
834
+		t.Fatalf("expected output ok received %s", actual)
835
+	}
836
+	deleteAllContainers()
837
+
838
+	logDone("run - test --cap-drop=ALL --cap-add=MKNOD can mknod")
839
+}
840
+
841
+func TestCapAddInvalid(t *testing.T) {
842
+	cmd := exec.Command(dockerBinary, "run", "--cap-add=CHPASS", "busybox", "ls")
843
+	out, _, err := runCommandWithOutput(cmd)
844
+	if err == nil {
845
+		t.Fatal(err, out)
846
+	}
847
+
848
+	logDone("run - test --cap-add=CHPASS invalid")
849
+}
850
+
851
+func TestCapAddCanDownInterface(t *testing.T) {
852
+	cmd := exec.Command(dockerBinary, "run", "--cap-add=NET_ADMIN", "busybox", "sh", "-c", "ip link set eth0 down && echo ok")
853
+	out, _, err := runCommandWithOutput(cmd)
854
+	if err != nil {
855
+		t.Fatal(err, out)
856
+	}
857
+
858
+	if actual := strings.Trim(out, "\r\n"); actual != "ok" {
859
+		t.Fatalf("expected output ok received %s", actual)
860
+	}
861
+	deleteAllContainers()
862
+
863
+	logDone("run - test --cap-add=NET_ADMIN can set eth0 down")
864
+}
865
+
866
+func TestCapAddALLCanDownInterface(t *testing.T) {
867
+	cmd := exec.Command(dockerBinary, "run", "--cap-add=ALL", "busybox", "sh", "-c", "ip link set eth0 down && echo ok")
868
+	out, _, err := runCommandWithOutput(cmd)
869
+	if err != nil {
870
+		t.Fatal(err, out)
871
+	}
872
+
873
+	if actual := strings.Trim(out, "\r\n"); actual != "ok" {
874
+		t.Fatalf("expected output ok received %s", actual)
875
+	}
876
+	deleteAllContainers()
877
+
878
+	logDone("run - test --cap-add=ALL can set eth0 down")
879
+}
880
+
881
+func TestCapAddALLDropNetAdminCanDownInterface(t *testing.T) {
882
+	cmd := exec.Command(dockerBinary, "run", "--cap-add=ALL", "--cap-drop=NET_ADMIN", "busybox", "sh", "-c", "ip link set eth0 down && echo ok")
883
+	out, _, err := runCommandWithOutput(cmd)
884
+	if err == nil {
885
+		t.Fatal(err, out)
886
+	}
887
+
888
+	if actual := strings.Trim(out, "\r\n"); actual == "ok" {
889
+		t.Fatalf("expected output not ok received %s", actual)
890
+	}
891
+	deleteAllContainers()
892
+
893
+	logDone("run - test --cap-add=ALL --cap-drop=NET_ADMIN cannot set eth0 down")
894
+}
895
+
786 896
 func TestPrivilegedCanMount(t *testing.T) {
787 897
 	cmd := exec.Command(dockerBinary, "run", "--privileged", "busybox", "sh", "-c", "mount -t tmpfs none /tmp && echo ok")
788 898
 
... ...
@@ -38,6 +38,8 @@ type HostConfig struct {
38 38
 	VolumesFrom     []string
39 39
 	Devices         []DeviceMapping
40 40
 	NetworkMode     NetworkMode
41
+	CapAdd          []string
42
+	CapDrop         []string
41 43
 }
42 44
 
43 45
 func ContainerHostConfigFromJob(job *engine.Job) *HostConfig {
... ...
@@ -65,5 +67,11 @@ func ContainerHostConfigFromJob(job *engine.Job) *HostConfig {
65 65
 	if VolumesFrom := job.GetenvList("VolumesFrom"); VolumesFrom != nil {
66 66
 		hostConfig.VolumesFrom = VolumesFrom
67 67
 	}
68
+	if CapAdd := job.GetenvList("CapAdd"); CapAdd != nil {
69
+		hostConfig.CapAdd = CapAdd
70
+	}
71
+	if CapDrop := job.GetenvList("CapDrop"); CapDrop != nil {
72
+		hostConfig.CapDrop = CapDrop
73
+	}
68 74
 	return hostConfig
69 75
 }
... ...
@@ -50,6 +50,8 @@ func parseRun(cmd *flag.FlagSet, args []string, sysInfo *sysinfo.SysInfo) (*Conf
50 50
 		flVolumesFrom opts.ListOpts
51 51
 		flLxcOpts     opts.ListOpts
52 52
 		flEnvFile     opts.ListOpts
53
+		flCapAdd      opts.ListOpts
54
+		flCapDrop     opts.ListOpts
53 55
 
54 56
 		flAutoRemove      = cmd.Bool([]string{"#rm", "-rm"}, false, "Automatically remove the container when it exits (incompatible with -d)")
55 57
 		flDetach          = cmd.Bool([]string{"d", "-detach"}, false, "Detached mode: run container in the background and print new container ID")
... ...
@@ -86,6 +88,9 @@ func parseRun(cmd *flag.FlagSet, args []string, sysInfo *sysinfo.SysInfo) (*Conf
86 86
 	cmd.Var(&flVolumesFrom, []string{"#volumes-from", "-volumes-from"}, "Mount volumes from the specified container(s)")
87 87
 	cmd.Var(&flLxcOpts, []string{"#lxc-conf", "-lxc-conf"}, "(lxc exec-driver only) Add custom lxc options --lxc-conf=\"lxc.cgroup.cpuset.cpus = 0,1\"")
88 88
 
89
+	cmd.Var(&flCapAdd, []string{"-cap-add"}, "Add Linux capabilities")
90
+	cmd.Var(&flCapDrop, []string{"-cap-drop"}, "Drop Linux capabilities")
91
+
89 92
 	if err := cmd.Parse(args); err != nil {
90 93
 		return nil, nil, cmd, err
91 94
 	}
... ...
@@ -258,6 +263,8 @@ func parseRun(cmd *flag.FlagSet, args []string, sysInfo *sysinfo.SysInfo) (*Conf
258 258
 		VolumesFrom:     flVolumesFrom.GetAll(),
259 259
 		NetworkMode:     netMode,
260 260
 		Devices:         deviceMappings,
261
+		CapAdd:          flCapAdd.GetAll(),
262
+		CapDrop:         flCapDrop.GetAll(),
261 263
 	}
262 264
 
263 265
 	if sysInfo != nil && flMemory > 0 && !sysInfo.SwapLimit {
... ...
@@ -3,11 +3,12 @@ package sysinit
3 3
 import (
4 4
 	"flag"
5 5
 	"fmt"
6
+	"log"
7
+	"os"
8
+
6 9
 	"github.com/dotcloud/docker/daemon/execdriver"
7 10
 	_ "github.com/dotcloud/docker/daemon/execdriver/lxc"
8 11
 	_ "github.com/dotcloud/docker/daemon/execdriver/native"
9
-	"log"
10
-	"os"
11 12
 )
12 13
 
13 14
 func executeProgram(args *execdriver.InitArgs) error {
... ...
@@ -39,6 +40,8 @@ func SysInit() {
39 39
 		pipe       = flag.Int("pipe", 0, "sync pipe fd")
40 40
 		console    = flag.String("console", "", "console (pty slave) path")
41 41
 		root       = flag.String("root", ".", "root path for configuration files")
42
+		capAdd     = flag.String("cap-add", "", "capabilities to add")
43
+		capDrop    = flag.String("cap-drop", "", "capabilities to drop")
42 44
 	)
43 45
 	flag.Parse()
44 46
 
... ...
@@ -54,6 +57,8 @@ func SysInit() {
54 54
 		Console:    *console,
55 55
 		Pipe:       *pipe,
56 56
 		Root:       *root,
57
+		CapAdd:     *capAdd,
58
+		CapDrop:    *capDrop,
57 59
 	}
58 60
 
59 61
 	if err := executeProgram(args); err != nil {
... ...
@@ -907,3 +907,12 @@ func ValidateContextDirectory(srcPath string) error {
907 907
 	})
908 908
 	return finalError
909 909
 }
910
+
911
+func StringsContainsNoCase(slice []string, s string) bool {
912
+	for _, ss := range slice {
913
+		if strings.ToLower(s) == strings.ToLower(ss) {
914
+			return true
915
+		}
916
+	}
917
+	return false
918
+}