Merge pull request #38380 from olljanat/capabilities-support

Add support for exact list of capabilities + capAdd / capDrop refactor
This commit is contained in:
Sebastiaan van Stijn 2019-01-28 16:36:03 +01:00 committed by GitHub
commit 5801c04345
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
12 changed files with 281 additions and 84 deletions

View file

@ -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,

View file

@ -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:

View file

@ -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

View file

@ -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 {

View file

@ -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 {

View file

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

View file

@ -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

View file

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

View file

@ -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")

View file

@ -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
}

View file

@ -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{},

View file

@ -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