policy for seccomp-profile selection

Implement a policy for selecting a seccomp profile.  In addition to the
default behaviour (default profile unless --security-opt seccomp is set)
add a second policy doing a lookup in the image annotation.

If the image has the "io.containers.seccomp.profile" set its value will be
interpreted as a seccomp profile.  The policy can be selected via the
new --seccomp-policy CLI flag.

Once the containers.conf support is merged into libpod, we can add an
option there as well.

Note that this feature is marked as experimental and may change in the
future.

Signed-off-by: Valentin Rothberg <rothberg@redhat.com>
This commit is contained in:
Valentin Rothberg
2020-01-07 16:48:07 +01:00
parent 7ccf412f70
commit f3f4c54f2a
9 changed files with 180 additions and 11 deletions

View File

@ -538,6 +538,10 @@ func getCreateFlags(c *cliconfig.PodmanCommand) {
"workdir", "w", "",
"Working directory inside the container",
)
createFlags.String(
"seccomp-policy", "default",
"Policy for selecting a seccomp profile (experimental)",
)
}
func getFormat(c *cliconfig.PodmanCommand) (string, error) {

View File

@ -31,6 +31,10 @@ import (
"github.com/sirupsen/logrus"
)
// seccompAnnotationKey is the key of the image annotation embedding a seccomp
// profile.
const seccompAnnotationKey = "io.containers.seccomp.profile"
func CreateContainer(ctx context.Context, c *GenericCLIResults, runtime *libpod.Runtime) (*libpod.Container, *cc.CreateConfig, error) {
var (
healthCheck *manifest.Schema2HealthConfig
@ -711,6 +715,18 @@ func ParseCreateOpts(ctx context.Context, c *GenericCLIResults, runtime *libpod.
return nil, err
}
// SECCOMP
if data != nil {
if value, exists := data.Annotations[seccompAnnotationKey]; exists {
secConfig.SeccompProfileFromImage = value
}
}
if policy, err := cc.LookupSeccompPolicy(c.String("seccomp-policy")); err != nil {
return nil, err
} else {
secConfig.SeccompPolicy = policy
}
config := &cc.CreateConfig{
Annotations: annotations,
BuiltinImgVolumes: ImageVolumes,

View File

@ -463,6 +463,7 @@ func NewIntermediateLayer(c *cliconfig.PodmanCommand, remote bool) GenericCLIRes
m["volume"] = newCRStringArray(c, "volume")
m["volumes-from"] = newCRStringSlice(c, "volumes-from")
m["workdir"] = newCRString(c, "workdir")
m["seccomp-policy"] = newCRString(c, "seccomp-policy")
// global flag
if !remote {
m["authfile"] = newCRString(c, "authfile")

View File

@ -663,6 +663,12 @@ If specified, the first argument refers to an exploded container on the file sys
This is useful to run a container without requiring any image management, the rootfs
of the container is assumed to be managed externally.
**--seccomp-policy**=*policy*
Specify the policy to select the seccomp profile. If set to *image*, Podman will look for a "io.podman.seccomp.profile" annotation in the container image and use its value as a seccomp profile. Otherwise, Podman will follow the *default* policy by applying the default profile unless specified otherwise via *--security-opt seccomp* as described below.
Note that this feature is experimental and may change in the future.
**--security-opt**=*option*
Security Options

View File

@ -685,6 +685,12 @@ of the container is assumed to be managed externally.
Note: On `SELinux` systems, the rootfs needs the correct label, which is by default
`unconfined_u:object_r:container_file_t`.
**--seccomp-policy**=*policy*
Specify the policy to select the seccomp profile. If set to *image*, Podman will look for a "io.podman.seccomp.profile" annotation in the container image and use its value as a seccomp profile. Otherwise, Podman will follow the *default* policy by applying the default profile unless specified otherwise via *--security-opt seccomp* as described below.
Note that this feature is experimental and may change in the future.
**--security-opt**=*option*
Security Options

View File

@ -8,13 +8,24 @@ import (
spec "github.com/opencontainers/runtime-spec/specs-go"
"github.com/pkg/errors"
seccomp "github.com/seccomp/containers-golang"
"github.com/sirupsen/logrus"
)
func getSeccompConfig(config *SecurityConfig, configSpec *spec.Spec) (*spec.LinuxSeccomp, error) {
var seccompConfig *spec.LinuxSeccomp
var err error
if config.SeccompPolicy == SeccompPolicyImage && config.SeccompProfileFromImage != "" {
logrus.Debug("Loading seccomp profile from the security config")
seccompConfig, err = seccomp.LoadProfile(config.SeccompProfileFromImage, configSpec)
if err != nil {
return nil, errors.Wrap(err, "loading seccomp profile failed")
}
return seccompConfig, nil
}
if config.SeccompProfilePath != "" {
logrus.Debugf("Loading seccomp profile from %q", config.SeccompProfilePath)
seccompProfile, err := ioutil.ReadFile(config.SeccompProfilePath)
if err != nil {
return nil, errors.Wrapf(err, "opening seccomp profile (%s) failed", config.SeccompProfilePath)
@ -24,6 +35,7 @@ func getSeccompConfig(config *SecurityConfig, configSpec *spec.Spec) (*spec.Linu
return nil, errors.Wrapf(err, "loading seccomp profile (%s) failed", config.SeccompProfilePath)
}
} else {
logrus.Debug("Loading default seccomp profile")
seccompConfig, err = seccomp.GetDefaultProfile(configSpec)
if err != nil {
return nil, errors.Wrapf(err, "loading seccomp profile (%s) failed", config.SeccompProfilePath)

View File

@ -2,6 +2,7 @@ package createconfig
import (
"os"
"sort"
"strconv"
"strings"
"syscall"
@ -106,19 +107,63 @@ type NetworkConfig struct {
PublishAll bool //publish-all
}
// SeccompPolicy determines which seccomp profile gets applied to the container.
type SeccompPolicy int
const (
// SeccompPolicyDefault - if set use SecurityConfig.SeccompProfilePath,
// otherwise use the default profile. The SeccompProfilePath might be
// explicitly set by the user.
SeccompPolicyDefault SeccompPolicy = iota
// SeccompPolicyImage - if set use SecurityConfig.SeccompProfileFromImage,
// otherwise follow SeccompPolicyDefault.
SeccompPolicyImage
)
// Map for easy lookups of supported policies.
var supportedSeccompPolicies = map[string]SeccompPolicy{
"": SeccompPolicyDefault,
"default": SeccompPolicyDefault,
"image": SeccompPolicyImage,
}
// LookupSeccompPolicy looksup the corresponding SeccompPolicy for the specified
// string. If none is found, an errors is returned including the list of
// supported policies.
// Note that an empty string resolved to SeccompPolicyDefault.
func LookupSeccompPolicy(s string) (SeccompPolicy, error) {
policy, exists := supportedSeccompPolicies[s]
if exists {
return policy, nil
}
// Sort the keys first as maps are non-deterministic.
keys := []string{}
for k := range supportedSeccompPolicies {
if k != "" {
keys = append(keys, k)
}
}
sort.Strings(keys)
return -1, errors.Errorf("invalid seccomp policy %q: valid policies are %+q", s, keys)
}
// SecurityConfig configures the security features for the container
type SecurityConfig struct {
CapAdd []string // cap-add
CapDrop []string // cap-drop
LabelOpts []string //SecurityOpts
NoNewPrivs bool //SecurityOpts
ApparmorProfile string //SecurityOpts
SeccompProfilePath string //SecurityOpts
SecurityOpts []string
Privileged bool //privileged
ReadOnlyRootfs bool //read-only
ReadOnlyTmpfs bool //read-only-tmpfs
Sysctl map[string]string //sysctl
CapAdd []string // cap-add
CapDrop []string // cap-drop
LabelOpts []string //SecurityOpts
NoNewPrivs bool //SecurityOpts
ApparmorProfile string //SecurityOpts
SeccompProfilePath string //SecurityOpts
SeccompProfileFromImage string // seccomp profile from the container image
SeccompPolicy SeccompPolicy
SecurityOpts []string
Privileged bool //privileged
ReadOnlyRootfs bool //read-only
ReadOnlyTmpfs bool //read-only-tmpfs
Sysctl map[string]string //sysctl
}
// CreateConfig is a pre OCI spec structure. It represents user input from varlink or the CLI

View File

@ -14,4 +14,13 @@ var (
BB = "docker.io/library/busybox:latest"
healthcheck = "docker.io/libpod/alpine_healthcheck:latest"
ImageCacheDir = "/tmp/podman/imagecachedir"
// This image has seccomp profiles that blocks all syscalls.
// The intention behind blocking all syscalls is to prevent
// regressions in the future. The required syscalls can vary
// depending on which runtime we're using.
alpineSeccomp = "docker.io/libpod/alpine-with-seccomp:latest"
// This image has a bogus/invalid seccomp profile which should
// yield a json error when being read.
alpineBogusSeccomp = "docker.io/libpod/alpine-with-bogus-seccomp:latest"
)

70
test/e2e/run_seccomp.go Normal file
View File

@ -0,0 +1,70 @@
// +build !remoteclient
package integration
import (
"os"
. "github.com/containers/libpod/test/utils"
. "github.com/onsi/ginkgo"
. "github.com/onsi/gomega"
)
var _ = Describe("Podman run", func() {
var (
tempdir string
err error
podmanTest *PodmanTestIntegration
)
BeforeEach(func() {
tempdir, err = CreateTempDirInTempDir()
if err != nil {
os.Exit(1)
}
podmanTest = PodmanTestCreate(tempdir)
podmanTest.Setup()
podmanTest.SeedImages()
})
AfterEach(func() {
podmanTest.Cleanup()
f := CurrentGinkgoTestDescription()
processTestResult(f)
})
It("podman run --seccomp-policy default", func() {
session := podmanTest.Podman([]string{"run", "--seccomp-policy", "default", alpineSeccomp, "ls"})
session.WaitWithDefaultTimeout()
Expect(session.ExitCode()).To(Equal(0))
})
It("podman run --seccomp-policy ''", func() {
// Empty string is interpreted as "default".
session := podmanTest.Podman([]string{"run", "--seccomp-policy", "", alpineSeccomp, "ls"})
session.WaitWithDefaultTimeout()
Expect(session.ExitCode()).To(Equal(0))
})
It("podman run --seccomp-policy invalid", func() {
session := podmanTest.Podman([]string{"run", "--seccomp-policy", "invalid", alpineSeccomp, "ls"})
session.WaitWithDefaultTimeout()
Expect(session.ExitCode()).ToNot(Equal(0))
})
It("podman run --seccomp-policy image (block all syscalls)", func() {
session := podmanTest.Podman([]string{"run", "--seccomp-policy", "image", alpineSeccomp, "ls"})
session.WaitWithDefaultTimeout()
// TODO: we're getting a "cannot start a container that has
// stopped" error which seems surprising. Investigate
// why that is so.
Expect(session.ExitCode()).ToNot(Equal(0))
})
It("podman run --seccomp-policy image (bogus profile)", func() {
session := podmanTest.Podman([]string{"run", "--seccomp-policy", "image", alpineBogusSeccomp, "ls"})
session.WaitWithDefaultTimeout()
Expect(session.ExitCode()).To(Equal(125))
})
})