Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

policy for seccomp-profile selection #4806

Merged
merged 2 commits into from
Jan 15, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions cmd/podman/common.go
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
30 changes: 23 additions & 7 deletions cmd/podman/shared/create.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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 == "" {
Expand Down Expand Up @@ -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()
Expand Down Expand Up @@ -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
}
Expand Down Expand Up @@ -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 {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As there is a return inside the if clause, you could happy path this.

secConfig.SeccompPolicy = policy
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't see where this is actually consumed - where are we trying to decode/use the image seccomp annotation

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We're doing that in pkg/spec.getSeccompConfig

}

config := &cc.CreateConfig{
Annotations: annotations,
BuiltinImgVolumes: ImageVolumes,
Expand Down
1 change: 1 addition & 0 deletions cmd/podman/shared/intermediate.go
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand Down
6 changes: 6 additions & 0 deletions docs/source/markdown/podman-create.1.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
6 changes: 6 additions & 0 deletions docs/source/markdown/podman-run.1.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
12 changes: 12 additions & 0 deletions pkg/spec/config_linux_cgo.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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)
Expand Down
67 changes: 56 additions & 11 deletions pkg/spec/createconfig.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package createconfig

import (
"os"
"sort"
"strconv"
"strings"
"syscall"
Expand Down Expand Up @@ -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
Expand Down
9 changes: 9 additions & 0 deletions test/e2e/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 changes: 70 additions & 0 deletions test/e2e/run_seccomp.go
Original file line number Diff line number Diff line change
@@ -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))
})
})