diff options
-rw-r--r-- | cmd/podman/common.go | 4 | ||||
-rw-r--r-- | cmd/podman/shared/create.go | 30 | ||||
-rw-r--r-- | cmd/podman/shared/intermediate.go | 1 | ||||
-rw-r--r-- | docs/source/markdown/podman-create.1.md | 6 | ||||
-rw-r--r-- | docs/source/markdown/podman-run.1.md | 6 | ||||
-rw-r--r-- | pkg/spec/config_linux_cgo.go | 12 | ||||
-rw-r--r-- | pkg/spec/createconfig.go | 67 | ||||
-rw-r--r-- | test/e2e/config.go | 9 | ||||
-rw-r--r-- | test/e2e/run_seccomp.go | 70 |
9 files changed, 187 insertions, 18 deletions
diff --git a/cmd/podman/common.go b/cmd/podman/common.go index dc7590590..9064ec219 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 05a3f5598..50a64b01c 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 @@ -67,7 +71,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 +103,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 +175,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 } @@ -712,6 +716,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 e985e4dc0..d1f0e602e 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 abde899bf..81fae0b25 100644 --- a/docs/source/markdown/podman-create.1.md +++ b/docs/source/markdown/podman-create.1.md @@ -676,6 +676,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 9484ad347..af5bb814d 100644 --- a/docs/source/markdown/podman-run.1.md +++ b/docs/source/markdown/podman-run.1.md @@ -697,6 +697,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 6d058229b..fb222083b 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)) + }) +}) |