diff --git a/api/server/router/container/container_routes.go b/api/server/router/container/container_routes.go index 09d4a75d10..b36cf725b3 100644 --- a/api/server/router/container/container_routes.go +++ b/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, diff --git a/api/swagger.yaml b/api/swagger.yaml index edf95b72de..cabd2b3921 100644 --- a/api/swagger.yaml +++ b/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: diff --git a/api/types/container/host_config.go b/api/types/container/host_config.go index 44bdfec962..05dd16a925 100644 --- a/api/types/container/host_config.go +++ b/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 diff --git a/daemon/container.go b/daemon/container.go index 56c986564f..a82d60c268 100644 --- a/daemon/container.go +++ b/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 { diff --git a/daemon/oci_linux.go b/daemon/oci_linux.go index e330f4fc66..ca6074cfac 100644 --- a/daemon/oci_linux.go +++ b/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 { diff --git a/daemon/oci_windows.go b/daemon/oci_windows.go index 8cffbc6b17..1cb7abfaa5 100644 --- a/daemon/oci_windows.go +++ b/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) diff --git a/docs/api/version-history.md b/docs/api/version-history.md index 3a838839e2..e77e0d7f70 100644 --- a/docs/api/version-history.md +++ b/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 diff --git a/integration-cli/docker_api_containers_test.go b/integration-cli/docker_api_containers_test.go index 25ddc43e22..9deaa9e56b 100644 --- a/integration-cli/docker_api_containers_test.go +++ b/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) diff --git a/integration/container/create_test.go b/integration/container/create_test.go index 6b1c71a538..7050cdf9a0 100644 --- a/integration/container/create_test.go +++ b/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") diff --git a/oci/caps/utils.go b/oci/caps/utils.go index 9b939fffc4..ffd3f6f508 100644 --- a/oci/caps/utils.go +++ b/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) } - - // handle --cap-add=all - if inSlice(adds, "all") { - basics = allCaps - } - - 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)) - } - } - } - - for _, cap := range adds { - // skip `all` already handled above - if strings.ToLower(cap) == "all" { - continue - } - - cap = "CAP_" + cap - - if !inSlice(allCaps, cap) { - return nil, fmt.Errorf("Unknown capability to add: %q", cap) - } - - // add cap if not already in the list - if !inSlice(newCaps, cap) { - newCaps = append(newCaps, strings.ToUpper(cap)) - } - } - return newCaps, nil + return normalized, nil +} + +// 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 +} + +// 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 + } + + capDrop, err := NormalizeLegacyCapabilities(drops) + if err != nil { + return nil, err + } + capAdd, err := NormalizeLegacyCapabilities(adds) + if err != nil { + return nil, err + } + + var caps []string + + switch { + case inSlice(capAdd, allCapabilities): + // Add all capabilities except ones on capDrop + for _, c := range GetAllCapabilities() { + if !inSlice(capDrop, c) { + caps = append(caps, c) + } + } + 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 caps, nil } diff --git a/oci/defaults.go b/oci/defaults.go index 8ba92407a3..35fbcd1d87 100644 --- a/oci/defaults.go +++ b/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{}, diff --git a/oci/oci.go b/oci/oci.go index adc6a3715c..6c84ba3488 100644 --- a/oci/oci.go +++ b/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