Skip to content

Commit

Permalink
capabilities: WARN, not ERROR, for unknown / unavailable capabilities
Browse files Browse the repository at this point in the history
This updates handling of capabilities to match the updated runtime specification,
in opencontainers/runtime-spec#1094.

Prior to that change, the specification required runtimes to produce a (fatal)
error if a container configuration requested capabilities that could not be
granted (either the capability is "unknown" to the runtime, not supported by the
kernel version in use, or not available in the environment that the runtime
operates in).

This caused problems in situations where the runtime was running in a restricted
environment (for example, docker-in-docker), or if there is a mismatch between
the list of capabilities known by higher-level runtimes and the OCI runtime.

Some examples:

- Kernel 5.8 introduced CAP_PERFMON, CAP_BPF, and CAP_CHECKPOINT_RESTORE
  capabilities. Docker 20.10.0 ("higher level runtime") shipped with
  an updated list of capabilities, and when creating a "privileged" container,
  would determine what capabilities are known by the kernel in use, and request
  all those capabilities (by including them in the container config).
  However, runc did not yet have an updated list of capabilities, and therefore
  reject the container specification, producing an error because the new
  capabilities were "unknown".
- When running nested containers, for example, when running docker-in-docker,
  the "inner" container may be using a more recent version of docker than the
  "outer" container. In this situation, the "outer" container may be missing
  capabilities that the inner container expects to be supported (based on
  kernel version). However, starting the container would fail, because the OCI
  runtime could not grant those capabilities (them not being available in the
  environment it's running in).

WARN (but otherwise ignore) capabilities that cannot be granted
--------------------------------------------------------------------------------

This patch changes the handling to WARN (but otherwise ignore) capabilities that
are requested in the container config, but cannot be granted, alleviating higher
level runtimes to detect what capabilities are supported (by the kernel, and
in the current environment), as well as avoiding failures in situations where
the higher-level runtime is aware of capabilities that are not (yet) supported
by runc.

Impact on security
--------------------------------------------------------------------------------

Given that `capabilities` is an "allow-list", ignoring unknown capabilities does
not impose a security risk; worst case, a container does not get all requested
capabilities granted and, as a result, some actions may fail.

Backward-compatibility
--------------------------------------------------------------------------------

This change should be fully backward compatible. Higher-level runtimes that
already dynamically adjust the list of requested capabilities can continue to do
so. Runtimes that do not adjust will see an improvement (containers can start
even if some of the requested capabilities are not granted). Container processes
MAY fail (as described in "impact on security"), but users can debug this
situation either by looking at the warnings produces by the OCI runtime, or using
tools such as `capsh` / `libcap` to get the list of actual capabilities in the
container.

Signed-off-by: Sebastiaan van Stijn <[email protected]>
  • Loading branch information
thaJeztah committed Mar 15, 2021
1 parent 249bca0 commit 75fefcf
Show file tree
Hide file tree
Showing 4 changed files with 190 additions and 17 deletions.
56 changes: 39 additions & 17 deletions libcontainer/capabilities/capabilities.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,10 @@
package capabilities

import (
"fmt"
"strings"

"github.com/opencontainers/runc/libcontainer/configs"
"github.com/sirupsen/logrus"
"github.com/syndtr/gocapability/capability"
)

Expand All @@ -24,47 +24,69 @@ func init() {
}
}

// New creates a new Caps from the given Capabilities config.
// New creates a new Caps from the given Capabilities config. Unknown Capabilities
// or Capabilities that are unavailable in the current environment are ignored,
// printing a warning instead.
func New(capConfig *configs.Capabilities) (*Caps, error) {
var (
err error
caps Caps
err error
caps Caps
ignored []string
warnings = make([]string, 0)
)

if caps.bounding, err = capSlice(capConfig.Bounding); err != nil {
return nil, err
if caps.bounding, ignored = capSlice(capConfig.Bounding); len(ignored) > 0 {
warnings = append(warnings, ignored...)
}
if caps.effective, err = capSlice(capConfig.Effective); err != nil {
return nil, err
if caps.effective, ignored = capSlice(capConfig.Effective); len(ignored) > 0 {
warnings = append(warnings, ignored...)
}
if caps.inheritable, err = capSlice(capConfig.Inheritable); err != nil {
return nil, err
if caps.inheritable, ignored = capSlice(capConfig.Inheritable); len(ignored) > 0 {
warnings = append(warnings, ignored...)
}
if caps.permitted, err = capSlice(capConfig.Permitted); err != nil {
return nil, err
if caps.permitted, ignored = capSlice(capConfig.Permitted); len(ignored) > 0 {
warnings = append(warnings, ignored...)
}
if caps.ambient, err = capSlice(capConfig.Ambient); err != nil {
return nil, err
if caps.ambient, ignored = capSlice(capConfig.Ambient); len(ignored) > 0 {
warnings = append(warnings, ignored...)
}
if caps.pid, err = capability.NewPid2(0); err != nil {
return nil, err
}
if err = caps.pid.Load(); err != nil {
return nil, err
}
printWarnings(warnings)
return &caps, nil
}

func capSlice(caps []string) ([]capability.Cap, error) {
func printWarnings(warnings []string) {
if len(warnings) == 0 {
return
}
var unknownCaps = map[string]struct{}{}
for _, c := range warnings {
if _, ok := unknownCaps[c]; !ok {
logrus.WithField("capability", c).Warn("ignoring unknown or unavailable capability")
unknownCaps[c] = struct{}{}
}
}
}

// capSlice converts the given slice of capability names, and converts them
// to their numeric equivalent. Names of unknown or unavailable capabilities
// are returned and can be used by the caller to print as a warning.
func capSlice(caps []string) ([]capability.Cap, []string) {
out := make([]capability.Cap, len(caps))
ignored := make([]string, 0)
for i, c := range caps {
v, ok := capabilityMap[c]
if !ok {
return nil, fmt.Errorf("unknown capability %q", c)
ignored = append(ignored, c)
}
out[i] = v
}
return out, nil
return out, ignored
}

// Caps holds the capabilities for a container.
Expand Down
59 changes: 59 additions & 0 deletions libcontainer/capabilities/capabilities_linux_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
package capabilities

import (
"io/ioutil"
"os"
"testing"

"github.com/opencontainers/runc/libcontainer/configs"
"github.com/sirupsen/logrus"
"github.com/sirupsen/logrus/hooks/test"
)

func TestNewWarnings(t *testing.T) {
cs := []string{"CAP_CHOWN", "CAP_UNKNOWN"}
conf := configs.Capabilities{
Bounding: cs,
Effective: cs,
Inheritable: cs,
Permitted: cs,
Ambient: cs,
}

hook := test.NewGlobal()
defer hook.Reset()

logrus.SetOutput(ioutil.Discard)
_, err := New(&conf)
logrus.SetOutput(os.Stderr)

if err != nil {
t.Error(err)
}
e := hook.AllEntries()
if len(e) != 1 {
t.Errorf("expected 1 warning, got %d", len(e))
}

expected := logrus.Entry{
Data: logrus.Fields{"capability": "CAP_UNKNOWN"},
Level: logrus.WarnLevel,
Message: "ignoring unknown or unavailable capability",
}

l := hook.LastEntry()
if l == nil {
t.Fatal("expected a warning, but got none")
}
if l.Level != expected.Level {
t.Errorf("expected %s, got %s", expected.Level, l.Level)
}
if l.Data["capability"] != expected.Data["capability"] {
t.Errorf("expected %v, got %v", expected.Data["capability"], l.Data["capability"])
}
if l.Message != expected.Message {
t.Errorf("expected %s, got %s", expected.Message, l.Message)
}

hook.Reset()
}
91 changes: 91 additions & 0 deletions vendor/github.com/sirupsen/logrus/hooks/test/test.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions vendor/modules.txt
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@ github.com/shurcooL/sanitized_anchor_name
# github.com/sirupsen/logrus v1.7.0
## explicit
github.com/sirupsen/logrus
github.com/sirupsen/logrus/hooks/test
# github.com/syndtr/gocapability v0.0.0-20200815063812-42c35b437635
## explicit
github.com/syndtr/gocapability/capability
Expand Down

0 comments on commit 75fefcf

Please sign in to comment.