Browse Source

Capabilities refactor
- Add support for exact list of capabilities, support only OCI model
- Support OCI model on CapAdd and CapDrop but remain backward compatibility
- Create variable locally instead of declaring it at the top
- Use const for magic "ALL" value
- Rename `cap` variable as it overlaps with `cap()` built-in
- Normalize and validate capabilities before use
- Move validation for conflicting options to validateHostConfig()
- TweakCapabilities: simplify logic to calculate capabilities

Signed-off-by: Olli Janatuinen <olli.janatuinen@gmail.com>
Signed-off-by: Sebastiaan van Stijn <github@gone.nl>

Olli Janatuinen 6 năm trước cách đây
mục cha
commit
80d7bfd54d

+ 5 - 0
api/server/router/container/container_routes.go

@@ -473,6 +473,11 @@ func (s *containerRouter) postContainersCreate(ctx context.Context, w http.Respo
 		hostConfig.KernelMemoryTCP = 0
 	}
 
+	// Ignore Capabilities because it was added in API 1.40.
+	if hostConfig != nil && versions.LessThan(version, "1.40") {
+		hostConfig.Capabilities = nil
+	}
+
 	ccr, err := s.backend.ContainerCreate(types.ContainerCreateConfig{
 		Name:             name,
 		Config:           config,

+ 10 - 2
api/swagger.yaml

@@ -645,14 +645,22 @@ definitions:
               $ref: "#/definitions/Mount"
 
           # Applicable to UNIX platforms
+          Capabilities:
+            type: "array"
+            description: |
+              A list of kernel capabilities to be available for container (this overrides the default set).
+
+              Conflicts with options 'CapAdd' and 'CapDrop'"
+            items:
+              type: "string"
           CapAdd:
             type: "array"
-            description: "A list of kernel capabilities to add to the container."
+            description: "A list of kernel capabilities to add to the container. Conflicts with option 'Capabilities'"
             items:
               type: "string"
           CapDrop:
             type: "array"
-            description: "A list of kernel capabilities to drop from the container."
+            description: "A list of kernel capabilities to drop from the container. Conflicts with option 'Capabilities'"
             items:
               type: "string"
           Dns:

+ 4 - 3
api/types/container/host_config.go

@@ -370,9 +370,10 @@ type HostConfig struct {
 	// Applicable to UNIX platforms
 	CapAdd          strslice.StrSlice // List of kernel capabilities to add to the container
 	CapDrop         strslice.StrSlice // List of kernel capabilities to remove from the container
-	DNS             []string          `json:"Dns"`        // List of DNS server to lookup
-	DNSOptions      []string          `json:"DnsOptions"` // List of DNSOption to look for
-	DNSSearch       []string          `json:"DnsSearch"`  // List of DNSSearch to look for
+	Capabilities    []string          `json:"Capabilities"` // List of kernel capabilities to be available for container (this overrides the default set)
+	DNS             []string          `json:"Dns"`          // List of DNS server to lookup
+	DNSOptions      []string          `json:"DnsOptions"`   // List of DNSOption to look for
+	DNSSearch       []string          `json:"DnsSearch"`    // List of DNSSearch to look for
 	ExtraHosts      []string          // List of extra hosts
 	GroupAdd        []string          // List of additional groups that the container process will run as
 	IpcMode         IpcMode           // IPC namespace to use for the container

+ 24 - 0
daemon/container.go

@@ -15,6 +15,7 @@ import (
 	"github.com/docker/docker/daemon/network"
 	"github.com/docker/docker/errdefs"
 	"github.com/docker/docker/image"
+	"github.com/docker/docker/oci/caps"
 	"github.com/docker/docker/opts"
 	"github.com/docker/docker/pkg/signal"
 	"github.com/docker/docker/pkg/system"
@@ -295,12 +296,35 @@ func validateHostConfig(hostConfig *containertypes.HostConfig, platform string)
 	if err := validateRestartPolicy(hostConfig.RestartPolicy); err != nil {
 		return err
 	}
+	if err := validateCapabilities(hostConfig); err != nil {
+		return err
+	}
 	if !hostConfig.Isolation.IsValid() {
 		return errors.Errorf("invalid isolation '%s' on %s", hostConfig.Isolation, runtime.GOOS)
 	}
 	return nil
 }
 
+func validateCapabilities(hostConfig *containertypes.HostConfig) error {
+	if len(hostConfig.CapAdd) > 0 && hostConfig.Capabilities != nil {
+		return errdefs.InvalidParameter(errors.Errorf("conflicting options: Capabilities and CapAdd"))
+	}
+	if len(hostConfig.CapDrop) > 0 && hostConfig.Capabilities != nil {
+		return errdefs.InvalidParameter(errors.Errorf("conflicting options: Capabilities and CapDrop"))
+	}
+	if _, err := caps.NormalizeLegacyCapabilities(hostConfig.CapAdd); err != nil {
+		return errors.Wrap(err, "invalid CapAdd")
+	}
+	if _, err := caps.NormalizeLegacyCapabilities(hostConfig.CapDrop); err != nil {
+		return errors.Wrap(err, "invalid CapDrop")
+	}
+	if err := caps.ValidateCapabilities(hostConfig.Capabilities); err != nil {
+		return errors.Wrap(err, "invalid Capabilities")
+	}
+	// TODO consider returning warnings if "Privileged" is combined with Capabilities, CapAdd and/or CapDrop
+	return nil
+}
+
 // validateHealthCheck validates the healthcheck params of Config
 func validateHealthCheck(healthConfig *containertypes.HealthConfig) error {
 	if healthConfig == nil {

+ 6 - 1
daemon/oci_linux.go

@@ -14,6 +14,7 @@ import (
 	"github.com/docker/docker/container"
 	daemonconfig "github.com/docker/docker/daemon/config"
 	"github.com/docker/docker/oci"
+	"github.com/docker/docker/oci/caps"
 	"github.com/docker/docker/pkg/idtools"
 	"github.com/docker/docker/pkg/mount"
 	volumemounts "github.com/docker/docker/volume/mounts"
@@ -762,7 +763,11 @@ func (daemon *Daemon) createSpec(c *container.Container) (retSpec *specs.Spec, e
 	if err := setNamespaces(daemon, &s, c); err != nil {
 		return nil, fmt.Errorf("linux spec namespaces: %v", err)
 	}
-	if err := oci.SetCapabilities(&s, c.HostConfig.CapAdd, c.HostConfig.CapDrop, c.HostConfig.Privileged); err != nil {
+	capabilities, err := caps.TweakCapabilities(oci.DefaultCapabilities(), c.HostConfig.CapAdd, c.HostConfig.CapDrop, c.HostConfig.Capabilities, c.HostConfig.Privileged)
+	if err != nil {
+		return nil, fmt.Errorf("linux spec capabilities: %v", err)
+	}
+	if err := oci.SetCapabilities(&s, capabilities); err != nil {
 		return nil, fmt.Errorf("linux spec capabilities: %v", err)
 	}
 	if err := setSeccomp(daemon, &s, c); err != nil {

+ 6 - 1
daemon/oci_windows.go

@@ -10,6 +10,7 @@ import (
 	containertypes "github.com/docker/docker/api/types/container"
 	"github.com/docker/docker/container"
 	"github.com/docker/docker/oci"
+	"github.com/docker/docker/oci/caps"
 	"github.com/docker/docker/pkg/sysinfo"
 	"github.com/docker/docker/pkg/system"
 	"github.com/opencontainers/runtime-spec/specs-go"
@@ -368,7 +369,11 @@ func (daemon *Daemon) createSpecLinuxFields(c *container.Container, s *specs.Spe
 	}
 	s.Root.Path = "rootfs"
 	s.Root.Readonly = c.HostConfig.ReadonlyRootfs
-	if err := oci.SetCapabilities(s, c.HostConfig.CapAdd, c.HostConfig.CapDrop, c.HostConfig.Privileged); err != nil {
+	capabilities, err := caps.TweakCapabilities(oci.DefaultCapabilities(), c.HostConfig.CapAdd, c.HostConfig.CapDrop, c.HostConfig.Capabilities, c.HostConfig.Privileged)
+	if err != nil {
+		return fmt.Errorf("linux spec capabilities: %v", err)
+	}
+	if err := oci.SetCapabilities(s, capabilities); err != nil {
 		return fmt.Errorf("linux spec capabilities: %v", err)
 	}
 	devPermissions, err := oci.AppendDevicePermissionsFromCgroupRules(nil, c.HostConfig.DeviceCgroupRules)

+ 3 - 0
docs/api/version-history.md

@@ -37,6 +37,9 @@ keywords: "API, Docker, rcli, REST, documentation"
 * `GET /service/{id}` now  returns `MaxReplicas` as part of the `Placement`.
 * `POST /service/create` and `POST /services/(id or name)/update` now take the field `MaxReplicas`
   as part of the service `Placement`, allowing to specify maximum replicas per node for the service.
+* `GET /containers` now returns `Capabilities` field as part of the `HostConfig`.
+* `GET /containers/{id}` now returns `Capabilities` field as part of the `HostConfig`.
+* `POST /containers/create` now takes `Capabilities` field to set exact list kernel capabilities to be available for      container (this overrides the default set).
 
 ## V1.39 API changes
 

+ 5 - 3
integration-cli/docker_api_containers_test.go

@@ -1377,6 +1377,8 @@ func (s *DockerSuite) TestPostContainersCreateWithStringOrSliceCmd(c *check.C) {
 }
 
 // regression #14318
+// for backward compatibility testing with and without CAP_ prefix
+// and with upper and lowercase
 func (s *DockerSuite) TestPostContainersCreateWithStringOrSliceCapAddDrop(c *check.C) {
 	// Windows doesn't support CapAdd/CapDrop
 	testRequires(c, DaemonIsLinux)
@@ -1384,7 +1386,7 @@ func (s *DockerSuite) TestPostContainersCreateWithStringOrSliceCapAddDrop(c *che
 		Image   string
 		CapAdd  string
 		CapDrop string
-	}{"busybox", "NET_ADMIN", "SYS_ADMIN"}
+	}{"busybox", "NET_ADMIN", "cap_sys_admin"}
 	res, _, err := request.Post("/containers/create?name=capaddtest0", request.JSONBody(config))
 	c.Assert(err, checker.IsNil)
 	c.Assert(res.StatusCode, checker.Equals, http.StatusCreated)
@@ -1393,8 +1395,8 @@ func (s *DockerSuite) TestPostContainersCreateWithStringOrSliceCapAddDrop(c *che
 		Image: "busybox",
 	}
 	hostConfig := containertypes.HostConfig{
-		CapAdd:  []string{"NET_ADMIN", "SYS_ADMIN"},
-		CapDrop: []string{"SETGID"},
+		CapAdd:  []string{"net_admin", "SYS_ADMIN"},
+		CapDrop: []string{"SETGID", "CAP_SETPCAP"},
 	}
 
 	cli, err := client.NewClientWithOpts(client.FromEnv)

+ 126 - 0
integration/container/create_test.go

@@ -13,6 +13,7 @@ import (
 	"github.com/docker/docker/api/types/container"
 	"github.com/docker/docker/api/types/network"
 	"github.com/docker/docker/api/types/versions"
+	"github.com/docker/docker/client"
 	ctr "github.com/docker/docker/integration/internal/container"
 	"github.com/docker/docker/internal/test/request"
 	"github.com/docker/docker/oci"
@@ -225,6 +226,131 @@ func TestCreateWithCustomMaskedPaths(t *testing.T) {
 	}
 }
 
+func TestCreateWithCapabilities(t *testing.T) {
+	skip.If(t, testEnv.DaemonInfo.OSType == "windows", "FIXME: test should be able to run on LCOW")
+	skip.If(t, versions.LessThan(testEnv.DaemonAPIVersion(), "1.40"), "Capabilities was added in API v1.40")
+
+	defer setupTest(t)()
+	ctx := context.Background()
+	clientNew := request.NewAPIClient(t)
+	clientOld := request.NewAPIClient(t, client.WithVersion("1.39"))
+
+	testCases := []struct {
+		doc           string
+		hostConfig    container.HostConfig
+		expected      []string
+		expectedError string
+		oldClient     bool
+	}{
+		{
+			doc:        "no capabilities",
+			hostConfig: container.HostConfig{},
+		},
+		{
+			doc: "empty capabilities",
+			hostConfig: container.HostConfig{
+				Capabilities: []string{},
+			},
+			expected: []string{},
+		},
+		{
+			doc: "valid capabilities",
+			hostConfig: container.HostConfig{
+				Capabilities: []string{"CAP_NET_RAW", "CAP_SYS_CHROOT"},
+			},
+			expected: []string{"CAP_NET_RAW", "CAP_SYS_CHROOT"},
+		},
+		{
+			doc: "invalid capabilities",
+			hostConfig: container.HostConfig{
+				Capabilities: []string{"NET_RAW"},
+			},
+			expectedError: `invalid Capabilities: unknown capability: "NET_RAW"`,
+		},
+		{
+			doc: "duplicate capabilities",
+			hostConfig: container.HostConfig{
+				Capabilities: []string{"CAP_SYS_NICE", "CAP_SYS_NICE"},
+			},
+			expected: []string{"CAP_SYS_NICE", "CAP_SYS_NICE"},
+		},
+		{
+			doc: "capabilities API v1.39",
+			hostConfig: container.HostConfig{
+				Capabilities: []string{"CAP_NET_RAW", "CAP_SYS_CHROOT"},
+			},
+			expected:  nil,
+			oldClient: true,
+		},
+		{
+			doc: "empty capadd",
+			hostConfig: container.HostConfig{
+				Capabilities: []string{"CAP_NET_ADMIN"},
+				CapAdd:       []string{},
+			},
+			expected: []string{"CAP_NET_ADMIN"},
+		},
+		{
+			doc: "empty capdrop",
+			hostConfig: container.HostConfig{
+				Capabilities: []string{"CAP_NET_ADMIN"},
+				CapDrop:      []string{},
+			},
+			expected: []string{"CAP_NET_ADMIN"},
+		},
+		{
+			doc: "capadd capdrop",
+			hostConfig: container.HostConfig{
+				CapAdd:  []string{"SYS_NICE", "CAP_SYS_NICE"},
+				CapDrop: []string{"SYS_NICE", "CAP_SYS_NICE"},
+			},
+		},
+		{
+			doc: "conflict with capadd",
+			hostConfig: container.HostConfig{
+				Capabilities: []string{"CAP_NET_ADMIN"},
+				CapAdd:       []string{"SYS_NICE"},
+			},
+			expectedError: `conflicting options: Capabilities and CapAdd`,
+		},
+		{
+			doc: "conflict with capdrop",
+			hostConfig: container.HostConfig{
+				Capabilities: []string{"CAP_NET_ADMIN"},
+				CapDrop:      []string{"NET_RAW"},
+			},
+			expectedError: `conflicting options: Capabilities and CapDrop`,
+		},
+	}
+
+	for _, tc := range testCases {
+		tc := tc
+		t.Run(tc.doc, func(t *testing.T) {
+			t.Parallel()
+			client := clientNew
+			if tc.oldClient {
+				client = clientOld
+			}
+
+			c, err := client.ContainerCreate(context.Background(),
+				&container.Config{Image: "busybox"},
+				&tc.hostConfig,
+				&network.NetworkingConfig{},
+				"",
+			)
+			if tc.expectedError == "" {
+				assert.NilError(t, err)
+				ci, err := client.ContainerInspect(ctx, c.ID)
+				assert.NilError(t, err)
+				assert.Check(t, ci.HostConfig != nil)
+				assert.DeepEqual(t, tc.expected, ci.HostConfig.Capabilities)
+			} else {
+				assert.ErrorContains(t, err, tc.expectedError)
+			}
+		})
+	}
+}
+
 func TestCreateWithCustomReadonlyPaths(t *testing.T) {
 	skip.If(t, testEnv.DaemonInfo.OSType != "linux")
 

+ 74 - 44
oci/caps/utils.go

@@ -4,6 +4,7 @@ import (
 	"fmt"
 	"strings"
 
+	"github.com/docker/docker/errdefs"
 	"github.com/syndtr/gocapability/capability"
 )
 
@@ -67,73 +68,102 @@ func GetAllCapabilities() []string {
 }
 
 // inSlice tests whether a string is contained in a slice of strings or not.
-// Comparison is case insensitive
 func inSlice(slice []string, s string) bool {
 	for _, ss := range slice {
-		if strings.ToLower(s) == strings.ToLower(ss) {
+		if s == ss {
 			return true
 		}
 	}
 	return false
 }
 
-// TweakCapabilities can tweak capabilities by adding or dropping capabilities
-// based on the basics capabilities.
-func TweakCapabilities(basics, adds, drops []string) ([]string, error) {
-	var (
-		newCaps []string
-		allCaps = GetAllCapabilities()
-	)
+const allCapabilities = "ALL"
 
-	// FIXME(tonistiigi): docker format is without CAP_ prefix, oci is with prefix
-	// Currently they are mixed in here. We should do conversion in one place.
+// NormalizeLegacyCapabilities normalizes, and validates CapAdd/CapDrop capabilities
+// by upper-casing them, and adding a CAP_ prefix (if not yet present).
+//
+// This function also accepts the "ALL" magic-value, that's used by CapAdd/CapDrop.
+func NormalizeLegacyCapabilities(caps []string) ([]string, error) {
+	var normalized []string
 
-	// look for invalid cap in the drop list
-	for _, cap := range drops {
-		if strings.ToLower(cap) == "all" {
+	valids := GetAllCapabilities()
+	for _, c := range caps {
+		c = strings.ToUpper(c)
+		if c == allCapabilities {
+			normalized = append(normalized, c)
 			continue
 		}
-
-		if !inSlice(allCaps, "CAP_"+cap) {
-			return nil, fmt.Errorf("Unknown capability drop: %q", cap)
+		if !strings.HasPrefix(c, "CAP_") {
+			c = "CAP_" + c
+		}
+		if !inSlice(valids, c) {
+			return nil, errdefs.InvalidParameter(fmt.Errorf("unknown capability: %q", c))
 		}
+		normalized = append(normalized, c)
 	}
+	return normalized, nil
+}
 
-	// handle --cap-add=all
-	if inSlice(adds, "all") {
-		basics = allCaps
+// ValidateCapabilities validates if caps only contains valid capabilities
+func ValidateCapabilities(caps []string) error {
+	valids := GetAllCapabilities()
+	for _, c := range caps {
+		if !inSlice(valids, c) {
+			return errdefs.InvalidParameter(fmt.Errorf("unknown capability: %q", c))
+		}
 	}
+	return nil
+}
 
-	if !inSlice(drops, "all") {
-		for _, cap := range basics {
-			// skip `all` already handled above
-			if strings.ToLower(cap) == "all" {
-				continue
-			}
-
-			// if we don't drop `all`, add back all the non-dropped caps
-			if !inSlice(drops, cap[4:]) {
-				newCaps = append(newCaps, strings.ToUpper(cap))
-			}
+// TweakCapabilities tweaks capabilities by adding, dropping, or overriding
+// capabilities in the basics capabilities list.
+func TweakCapabilities(basics, adds, drops, capabilities []string, privileged bool) ([]string, error) {
+	switch {
+	case privileged:
+		// Privileged containers get all capabilities
+		return GetAllCapabilities(), nil
+	case capabilities != nil:
+		// Use custom set of capabilities
+		if err := ValidateCapabilities(capabilities); err != nil {
+			return nil, err
 		}
+		return capabilities, nil
+	case len(adds) == 0 && len(drops) == 0:
+		// Nothing to tweak; we're done
+		return basics, nil
 	}
 
-	for _, cap := range adds {
-		// skip `all` already handled above
-		if strings.ToLower(cap) == "all" {
-			continue
-		}
+	capDrop, err := NormalizeLegacyCapabilities(drops)
+	if err != nil {
+		return nil, err
+	}
+	capAdd, err := NormalizeLegacyCapabilities(adds)
+	if err != nil {
+		return nil, err
+	}
 
-		cap = "CAP_" + cap
+	var caps []string
 
-		if !inSlice(allCaps, cap) {
-			return nil, fmt.Errorf("Unknown capability to add: %q", cap)
+	switch {
+	case inSlice(capAdd, allCapabilities):
+		// Add all capabilities except ones on capDrop
+		for _, c := range GetAllCapabilities() {
+			if !inSlice(capDrop, c) {
+				caps = append(caps, c)
+			}
 		}
-
-		// add cap if not already in the list
-		if !inSlice(newCaps, cap) {
-			newCaps = append(newCaps, strings.ToUpper(cap))
+	case inSlice(capDrop, allCapabilities):
+		// "Drop" all capabilities; use what's in capAdd instead
+		caps = capAdd
+	default:
+		// First drop some capabilities
+		for _, c := range basics {
+			if !inSlice(capDrop, c) {
+				caps = append(caps, c)
+			}
 		}
+		// Then add the list of capabilities from capAdd
+		caps = append(caps, capAdd...)
 	}
-	return newCaps, nil
+	return caps, nil
 }

+ 6 - 5
oci/defaults.go

@@ -11,7 +11,8 @@ func iPtr(i int64) *int64        { return &i }
 func u32Ptr(i int64) *uint32     { u := uint32(i); return &u }
 func fmPtr(i int64) *os.FileMode { fm := os.FileMode(i); return &fm }
 
-func defaultCapabilities() []string {
+// DefaultCapabilities returns a Linux kernel default capabilities
+func DefaultCapabilities() []string {
 	return []string{
 		"CAP_CHOWN",
 		"CAP_DAC_OVERRIDE",
@@ -59,10 +60,10 @@ func DefaultLinuxSpec() specs.Spec {
 		Version: specs.Version,
 		Process: &specs.Process{
 			Capabilities: &specs.LinuxCapabilities{
-				Bounding:    defaultCapabilities(),
-				Permitted:   defaultCapabilities(),
-				Inheritable: defaultCapabilities(),
-				Effective:   defaultCapabilities(),
+				Bounding:    DefaultCapabilities(),
+				Permitted:   DefaultCapabilities(),
+				Inheritable: DefaultCapabilities(),
+				Effective:   DefaultCapabilities(),
 			},
 		},
 		Root: &specs.Root{},

+ 1 - 14
oci/oci.go

@@ -5,7 +5,6 @@ import (
 	"regexp"
 	"strconv"
 
-	"github.com/docker/docker/oci/caps"
 	specs "github.com/opencontainers/runtime-spec/specs-go"
 )
 
@@ -14,19 +13,7 @@ var deviceCgroupRuleRegex = regexp.MustCompile("^([acb]) ([0-9]+|\\*):([0-9]+|\\
 
 // SetCapabilities sets the provided capabilities on the spec
 // All capabilities are added if privileged is true
-func SetCapabilities(s *specs.Spec, add, drop []string, privileged bool) error {
-	var (
-		caplist []string
-		err     error
-	)
-	if privileged {
-		caplist = caps.GetAllCapabilities()
-	} else {
-		caplist, err = caps.TweakCapabilities(s.Process.Capabilities.Bounding, add, drop)
-		if err != nil {
-			return err
-		}
-	}
+func SetCapabilities(s *specs.Spec, caplist []string) error {
 	s.Process.Capabilities.Effective = caplist
 	s.Process.Capabilities.Bounding = caplist
 	s.Process.Capabilities.Permitted = caplist