From ce5d7c51382dd3db71fa601173a7c772d9086c43 Mon Sep 17 00:00:00 2001 From: Olli Janatuinen Date: Mon, 9 Sep 2019 20:24:51 +0300 Subject: [PATCH 1/2] Stack: Support cap_add, cap_drop and privileged on services Signed-off-by: Olli Janatuinen --- cli/compose/convert/service.go | 22 ++++++++++++ cli/compose/convert/service_test.go | 55 +++++++++++++++++++++++++++++ cli/compose/types/types.go | 3 -- 3 files changed, 77 insertions(+), 3 deletions(-) diff --git a/cli/compose/convert/service.go b/cli/compose/convert/service.go index da182bbfe8af..59cf2e7a925c 100644 --- a/cli/compose/convert/service.go +++ b/cli/compose/convert/service.go @@ -14,6 +14,7 @@ import ( "github.com/docker/docker/api/types/swarm" "github.com/docker/docker/api/types/versions" "github.com/docker/docker/client" + "github.com/docker/docker/oci/caps" "github.com/pkg/errors" ) @@ -117,6 +118,11 @@ func Service( } } + capabilities, err := convertCapabilities(apiVersion, service.CapAdd, service.CapDrop, service.Privileged) + if err != nil { + return swarm.ServiceSpec{}, err + } + serviceSpec := swarm.ServiceSpec{ Annotations: swarm.Annotations{ Name: name, @@ -147,6 +153,7 @@ func Service( Isolation: container.Isolation(service.Isolation), Init: service.Init, Sysctls: service.Sysctls, + Capabilities: capabilities, }, LogDriver: logDriver, Resources: resources, @@ -675,3 +682,18 @@ func convertCredentialSpec(namespace Namespace, spec composetypes.CredentialSpec } return &swarmCredSpec, nil } + +func convertCapabilities(apiVersion string, capAdd, capDrop []string, privileged bool) ([]string, error) { + capabilities := []string{} + if privileged || len(capAdd) > 0 || len(capDrop) > 0 { + if versions.LessThan(apiVersion, "1.41") { + return nil, errors.Errorf("Engine version does not support exact list of capabilities") + } + capabilities, err := caps.TweakCapabilities(caps.DefaultCapabilities(), capAdd, capDrop, nil, privileged) + if err != nil { + return nil, err + } + return capabilities, nil + } + return capabilities, nil +} diff --git a/cli/compose/convert/service_test.go b/cli/compose/convert/service_test.go index 85550628f2fa..0a2916292b0c 100644 --- a/cli/compose/convert/service_test.go +++ b/cli/compose/convert/service_test.go @@ -13,6 +13,7 @@ import ( "github.com/docker/docker/api/types/container" "github.com/docker/docker/api/types/swarm" "github.com/docker/docker/client" + "github.com/docker/docker/oci/caps" "github.com/pkg/errors" "gotest.tools/assert" is "gotest.tools/assert/cmp" @@ -623,3 +624,57 @@ func TestConvertUpdateConfigParallelism(t *testing.T) { }) assert.Check(t, is.Equal(parallel, updateConfig.Parallelism)) } + +func TestConvertServiceCapAddAndCapDrop(t *testing.T) { + result, err := Service("1.41", Namespace{name: "foo"}, composetypes.ServiceConfig{}, nil, nil, nil, nil) + assert.NilError(t, err) + assert.Check(t, is.DeepEqual(result.TaskTemplate.ContainerSpec.Capabilities, []string{})) + + result, err = Service("1.40", Namespace{name: "foo"}, + composetypes.ServiceConfig{CapAdd: []string{"SYS_NICE"}}, nil, nil, nil, nil) + assert.Error(t, err, "Engine version does not support exact list of capabilities") + + service := composetypes.ServiceConfig{ + CapAdd: []string{ + "SYS_NICE", + "CAP_NET_ADMIN", + }, + CapDrop: []string{ + "CHOWN", + "DAC_OVERRIDE", + "CAP_FSETID", + "CAP_FOWNER", + }, + } + expected := []string{ + "CAP_MKNOD", + "CAP_NET_RAW", + "CAP_SETGID", + "CAP_SETUID", + "CAP_SETFCAP", + "CAP_SETPCAP", + "CAP_NET_BIND_SERVICE", + "CAP_SYS_CHROOT", + "CAP_KILL", + "CAP_AUDIT_WRITE", + "CAP_SYS_NICE", + "CAP_NET_ADMIN", + } + result, err = Service("1.41", Namespace{name: "foo"}, service, nil, nil, nil, nil) + assert.NilError(t, err) + assert.Check(t, is.DeepEqual(result.TaskTemplate.ContainerSpec.Capabilities, expected)) +} + +func TestConvertServicePrivileged(t *testing.T) { + service := composetypes.ServiceConfig{ + Privileged: true, + } + + result, err := Service("1.40", Namespace{name: "foo"}, service, nil, nil, nil, nil) + assert.Error(t, err, "Engine version does not support exact list of capabilities") + + expected := caps.GetAllCapabilities() + result, err = Service("1.41", Namespace{name: "foo"}, service, nil, nil, nil, nil) + assert.NilError(t, err) + assert.Check(t, is.DeepEqual(result.TaskTemplate.ContainerSpec.Capabilities, expected)) +} diff --git a/cli/compose/types/types.go b/cli/compose/types/types.go index f980032eeaee..b7e1c3dfabb2 100644 --- a/cli/compose/types/types.go +++ b/cli/compose/types/types.go @@ -9,8 +9,6 @@ import ( // UnsupportedProperties not yet supported by this implementation of the compose file var UnsupportedProperties = []string{ "build", - "cap_add", - "cap_drop", "cgroupns_mode", "cgroup_parent", "devices", @@ -21,7 +19,6 @@ var UnsupportedProperties = []string{ "mac_address", "network_mode", "pid", - "privileged", "restart", "security_opt", "shm_size", From fa80a15f575e828d0dfc11f26a3dceafd6d59fdc Mon Sep 17 00:00:00 2001 From: Olli Janatuinen Date: Sun, 16 Feb 2020 13:03:02 +0200 Subject: [PATCH 2/2] Re-vendor Moby Signed-off-by: Olli Janatuinen --- .../docker/docker/oci/caps/defaults.go | 21 +++ .../docker/docker/oci/caps/utils.go | 169 ++++++++++++++++++ 2 files changed, 190 insertions(+) create mode 100644 vendor/github.com/docker/docker/oci/caps/defaults.go create mode 100644 vendor/github.com/docker/docker/oci/caps/utils.go diff --git a/vendor/github.com/docker/docker/oci/caps/defaults.go b/vendor/github.com/docker/docker/oci/caps/defaults.go new file mode 100644 index 000000000000..242ee5811d2a --- /dev/null +++ b/vendor/github.com/docker/docker/oci/caps/defaults.go @@ -0,0 +1,21 @@ +package caps // import "github.com/docker/docker/oci/caps" + +// DefaultCapabilities returns a Linux kernel default capabilities +func DefaultCapabilities() []string { + return []string{ + "CAP_CHOWN", + "CAP_DAC_OVERRIDE", + "CAP_FSETID", + "CAP_FOWNER", + "CAP_MKNOD", + "CAP_NET_RAW", + "CAP_SETGID", + "CAP_SETUID", + "CAP_SETFCAP", + "CAP_SETPCAP", + "CAP_NET_BIND_SERVICE", + "CAP_SYS_CHROOT", + "CAP_KILL", + "CAP_AUDIT_WRITE", + } +} diff --git a/vendor/github.com/docker/docker/oci/caps/utils.go b/vendor/github.com/docker/docker/oci/caps/utils.go new file mode 100644 index 000000000000..ffd3f6f508ce --- /dev/null +++ b/vendor/github.com/docker/docker/oci/caps/utils.go @@ -0,0 +1,169 @@ +package caps // import "github.com/docker/docker/oci/caps" + +import ( + "fmt" + "strings" + + "github.com/docker/docker/errdefs" + "github.com/syndtr/gocapability/capability" +) + +var capabilityList Capabilities + +func init() { + last := capability.CAP_LAST_CAP + // hack for RHEL6 which has no /proc/sys/kernel/cap_last_cap + if last == capability.Cap(63) { + last = capability.CAP_BLOCK_SUSPEND + } + for _, cap := range capability.List() { + if cap > last { + continue + } + capabilityList = append(capabilityList, + &CapabilityMapping{ + Key: "CAP_" + strings.ToUpper(cap.String()), + Value: cap, + }, + ) + } +} + +type ( + // CapabilityMapping maps linux capability name to its value of capability.Cap type + // Capabilities is one of the security systems in Linux Security Module (LSM) + // framework provided by the kernel. + // For more details on capabilities, see http://man7.org/linux/man-pages/man7/capabilities.7.html + CapabilityMapping struct { + Key string `json:"key,omitempty"` + Value capability.Cap `json:"value,omitempty"` + } + // Capabilities contains all CapabilityMapping + Capabilities []*CapabilityMapping +) + +// String returns of CapabilityMapping +func (c *CapabilityMapping) String() string { + return c.Key +} + +// GetCapability returns CapabilityMapping which contains specific key +func GetCapability(key string) *CapabilityMapping { + for _, capp := range capabilityList { + if capp.Key == key { + cpy := *capp + return &cpy + } + } + return nil +} + +// GetAllCapabilities returns all of the capabilities +func GetAllCapabilities() []string { + output := make([]string, len(capabilityList)) + for i, capability := range capabilityList { + output[i] = capability.String() + } + return output +} + +// inSlice tests whether a string is contained in a slice of strings or not. +func inSlice(slice []string, s string) bool { + for _, ss := range slice { + if s == ss { + return true + } + } + return false +} + +const allCapabilities = "ALL" + +// 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 + + valids := GetAllCapabilities() + for _, c := range caps { + c = strings.ToUpper(c) + if c == allCapabilities { + normalized = append(normalized, c) + continue + } + 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 +} + +// 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 +}