From 7ccf412f7068acac16aa5aecf059df19c239f7cc Mon Sep 17 00:00:00 2001 From: Valentin Rothberg Date: Tue, 7 Jan 2020 13:04:42 +0100 Subject: shared/create.go: s/data/imageData/ Rename `data` to `imageData` to make it more obvious which kind of data the variable refers to. Signed-off-by: Valentin Rothberg --- cmd/podman/shared/create.go | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) (limited to 'cmd') diff --git a/cmd/podman/shared/create.go b/cmd/podman/shared/create.go index 58cf56eea..f0fb28eb6 100644 --- a/cmd/podman/shared/create.go +++ b/cmd/podman/shared/create.go @@ -67,7 +67,7 @@ func CreateContainer(ctx context.Context, c *GenericCLIResults, runtime *libpod. } imageName := "" - var data *inspect.ImageData = nil + var imageData *inspect.ImageData = nil // Set the storage if there is no rootfs specified if rootfs == "" { @@ -99,17 +99,17 @@ func CreateContainer(ctx context.Context, c *GenericCLIResults, runtime *libpod. if err != nil { return nil, nil, err } - data, err = newImage.Inspect(ctx) + imageData, err = newImage.Inspect(ctx) if err != nil { return nil, nil, err } - if overrideOS == "" && data.Os != goruntime.GOOS { - return nil, nil, errors.Errorf("incompatible image OS %q on %q host", data.Os, goruntime.GOOS) + if overrideOS == "" && imageData.Os != goruntime.GOOS { + return nil, nil, errors.Errorf("incompatible image OS %q on %q host", imageData.Os, goruntime.GOOS) } - if overrideArch == "" && data.Architecture != goruntime.GOARCH { - return nil, nil, errors.Errorf("incompatible image architecture %q on %q host", data.Architecture, goruntime.GOARCH) + if overrideArch == "" && imageData.Architecture != goruntime.GOARCH { + return nil, nil, errors.Errorf("incompatible image architecture %q on %q host", imageData.Architecture, goruntime.GOARCH) } names := newImage.Names() @@ -171,7 +171,7 @@ func CreateContainer(ctx context.Context, c *GenericCLIResults, runtime *libpod. } } - createConfig, err := ParseCreateOpts(ctx, c, runtime, imageName, data) + createConfig, err := ParseCreateOpts(ctx, c, runtime, imageName, imageData) if err != nil { return nil, nil, err } -- cgit v1.2.3-54-g00ecf From f3f4c54f2abc341cee1e7b83e9538d91a3c627e3 Mon Sep 17 00:00:00 2001 From: Valentin Rothberg Date: Tue, 7 Jan 2020 16:48:07 +0100 Subject: 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 --- cmd/podman/common.go | 4 ++ cmd/podman/shared/create.go | 16 ++++++++ cmd/podman/shared/intermediate.go | 1 + docs/source/markdown/podman-create.1.md | 6 +++ docs/source/markdown/podman-run.1.md | 6 +++ pkg/spec/config_linux_cgo.go | 12 ++++++ pkg/spec/createconfig.go | 67 +++++++++++++++++++++++++------ test/e2e/config.go | 9 +++++ test/e2e/run_seccomp.go | 70 +++++++++++++++++++++++++++++++++ 9 files changed, 180 insertions(+), 11 deletions(-) create mode 100644 test/e2e/run_seccomp.go (limited to 'cmd') diff --git a/cmd/podman/common.go b/cmd/podman/common.go index 69365201e..5c117f6b3 100644 --- a/cmd/podman/common.go +++ b/cmd/podman/common.go @@ -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) { diff --git a/cmd/podman/shared/create.go b/cmd/podman/shared/create.go index f0fb28eb6..2aca53ac9 100644 --- a/cmd/podman/shared/create.go +++ b/cmd/podman/shared/create.go @@ -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, diff --git a/cmd/podman/shared/intermediate.go b/cmd/podman/shared/intermediate.go index bc12bd2a5..55826625b 100644 --- a/cmd/podman/shared/intermediate.go +++ b/cmd/podman/shared/intermediate.go @@ -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") diff --git a/docs/source/markdown/podman-create.1.md b/docs/source/markdown/podman-create.1.md index 85aa81553..814805e19 100644 --- a/docs/source/markdown/podman-create.1.md +++ b/docs/source/markdown/podman-create.1.md @@ -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 diff --git a/docs/source/markdown/podman-run.1.md b/docs/source/markdown/podman-run.1.md index e8744de35..ceb97dbb7 100644 --- a/docs/source/markdown/podman-run.1.md +++ b/docs/source/markdown/podman-run.1.md @@ -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 diff --git a/pkg/spec/config_linux_cgo.go b/pkg/spec/config_linux_cgo.go index c47156456..ae83c9d52 100644 --- a/pkg/spec/config_linux_cgo.go +++ b/pkg/spec/config_linux_cgo.go @@ -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) diff --git a/pkg/spec/createconfig.go b/pkg/spec/createconfig.go index 244a8d1cd..827caacd6 100644 --- a/pkg/spec/createconfig.go +++ b/pkg/spec/createconfig.go @@ -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 diff --git a/test/e2e/config.go b/test/e2e/config.go index aeb7affee..12d0e545e 100644 --- a/test/e2e/config.go +++ b/test/e2e/config.go @@ -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" ) diff --git a/test/e2e/run_seccomp.go b/test/e2e/run_seccomp.go new file mode 100644 index 000000000..dcf938ad6 --- /dev/null +++ b/test/e2e/run_seccomp.go @@ -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)) + }) +}) -- cgit v1.2.3-54-g00ecf