Skip to content

Commit

Permalink
Add support for OCI artifact seccomp profiles
Browse files Browse the repository at this point in the history
Signed-off-by: Sascha Grunert <[email protected]>
  • Loading branch information
saschagrunert committed Feb 7, 2024
1 parent e9febd3 commit b20d06c
Show file tree
Hide file tree
Showing 19 changed files with 666 additions and 9 deletions.
9 changes: 8 additions & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -331,7 331,8 @@ mockgen: \
mock-lib-config \
mock-oci \
mock-image-types \
mock-ocicni-types
mock-ocicni-types \
mock-ociartifact-types

mock-containereventserver: ${MOCKGEN}
${MOCKGEN} \
Expand Down Expand Up @@ -381,6 382,12 @@ mock-ocicni-types: ${MOCKGEN}
-destination ${MOCK_PATH}/ocicni/types.go \
github.com/cri-o/ocicni/pkg/ocicni CNIPlugin

mock-ociartifact-types: ${MOCKGEN}
${BUILD_BIN_PATH}/mockgen \
-package ociartifactmock \
-destination ${MOCK_PATH}/ociartifact/ociartifact.go \
github.com/cri-o/cri-o/internal/config/ociartifact Impl

codecov: SHELL := $(shell which bash)
codecov:
bash <(curl -s https://codecov.io/bash) -f ${COVERAGE_PATH}/coverprofile
Expand Down
1 change: 1 addition & 0 deletions contrib/test/ci/integration.yml
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 63,7 @@
['namespaces.bats'] | product(kata_skip_namespaces_tests) \
['network.bats'] | product(kata_skip_network_tests) \
['pod.bats'] | product(kata_skip_pod_tests) \
['seccomp_oci_artifacts.bats'] | product(kata_skip_seccomp_oci_artifacts_tests) \
| list }}"

- name: Disable SELinux
Expand Down
3 changes: 3 additions & 0 deletions contrib/test/ci/vars.yml
Original file line number Diff line number Diff line change
Expand Up @@ -156,5 156,8 @@ kata_skip_pod_tests:
- 'test "restart crio and still get pod status"'
- 'test "systemd cgroup_parent correctly set"'
- 'test "kubernetes pod terminationGracePeriod passthru"'
kata_skip_seccomp_oci_artifacts_tests:
- 'test "seccomp OCI artifact with pod annotation"'
- 'test "seccomp OCI artifact with container annotation"'

runc_git_version: main
2 changes: 2 additions & 0 deletions docs/crio.conf.5.md
Original file line number Diff line number Diff line change
Expand Up @@ -352,6 352,7 @@ The "crio.runtime.runtimes" table defines a list of OCI compatible runtimes. Th
"io.kubernetes.cri-o.ShmSize" for configuring the size of /dev/shm.
"io.kubernetes.cri-o.UnifiedCgroup.$CTR_NAME" for configuring the cgroup v2 unified block for a container.
"io.containers.trace-syscall" for tracing syscalls via the OCI seccomp BPF hook.
"io.kubernetes.cri-o.seccompProfile" for setting the seccomp profile for a specific container, pod or whole image.

**platform_runtime_paths**={}
A mapping of platforms to the corresponding runtime executable paths for the runtime handler.
Expand Down Expand Up @@ -379,6 380,7 @@ A workload is chosen for a pod based on whether the workload's **activation_anno
"io.kubernetes.cri-o.seccompNotifierAction" for enabling the seccomp notifier feature.
"io.kubernetes.cri-o.umask" for setting the umask for container init process.
"io.kubernetes.cri.rdt-class" for setting the RDT class of a container
"io.kubernetes.cri-o.seccompProfile" for setting the seccomp profile for a specific container, pod or whole image.

#### Using the seccomp notifier feature:

Expand Down
77 changes: 77 additions & 0 deletions internal/config/ociartifact/ociartifact.go
Original file line number Diff line number Diff line change
@@ -0,0 1,77 @@
package ociartifact

import (
"context"
"fmt"

"github.com/containers/common/libimage"
"github.com/containers/common/pkg/config"
"github.com/containers/image/v5/types"
"github.com/containers/storage"

"github.com/cri-o/cri-o/internal/log"
)

// Impl is the main implementation interface of this package.
type Impl interface {
Pull(context.Context, *types.SystemContext, string) (*Artifact, error)
}

// New returns a new OCI artifact implementation.
func New() Impl {
return &defaultImpl{}
}

// Artifact can be used to manage OCI artifacts.
type Artifact struct {
// MountPath is the local path containing the artifact data.
MountPath string

// Cleanup has to be called if the artifact is not used any more.
Cleanup func()
}

// defaultImpl is the default implementation for the OCI artifact handling.
type defaultImpl struct{}

// Pull downloads and mounts the artifact content by using the provided ref.
func (*defaultImpl) Pull(ctx context.Context, sys *types.SystemContext, ref string) (*Artifact, error) {
log.Infof(ctx, "Pulling OCI artifact from ref: %s", ref)

storeOpts, err := storage.DefaultStoreOptions(false, 0)
if err != nil {
return nil, fmt.Errorf("get default storage options: %w", err)
}

store, err := storage.GetStore(storeOpts)
if err != nil {
return nil, fmt.Errorf("get container storage: %w", err)
}

runtime, err := libimage.RuntimeFromStore(store, &libimage.RuntimeOptions{SystemContext: sys})
if err != nil {
return nil, fmt.Errorf("create libimage runtime: %w", err)
}

images, err := runtime.Pull(ctx, ref, config.PullPolicyAlways, &libimage.PullOptions{})
if err != nil {
return nil, fmt.Errorf("pull OCI artifact: %w", err)
}
image := images[0]

mountPath, err := image.Mount(ctx, nil, "")
if err != nil {
return nil, fmt.Errorf("mount OCI artifact: %w", err)
}

cleanup := func() {
if err := image.Unmount(true); err != nil {
log.Warnf(ctx, "Unable to unmount OCI artifact path %s: %v", mountPath, err)
}
}

return &Artifact{
MountPath: mountPath,
Cleanup: cleanup,
}, nil
}
56 changes: 48 additions & 8 deletions internal/config/seccomp/seccomp.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 13,8 @@ import (
"sync"

"github.com/containers/common/pkg/seccomp"
imagetypes "github.com/containers/image/v5/types"
"github.com/cri-o/cri-o/internal/config/seccomp/seccompociartifact"
"github.com/cri-o/cri-o/internal/log"
json "github.com/json-iterator/go"
"github.com/opencontainers/runtime-tools/generate"
Expand Down Expand Up @@ -177,16 179,35 @@ func (c *Config) Profile() *seccomp.Seccomp {
// Setup can be used to setup the seccomp profile.
func (c *Config) Setup(
ctx context.Context,
sys *imagetypes.SystemContext,
msgChan chan Notification,
containerID string,
annotations map[string]string,
containerID, containerName string,
sandboxAnnotations, imageAnnotations map[string]string,
specGenerator *generate.Generator,
profileField *types.SecurityProfile,
) (*Notifier, string, error) {
ctx, span := log.StartSpan(ctx)
defer span.End()
log.Debugf(ctx, "Setup seccomp from profile field: % v", profileField)

// Specifically set profile fields always have a higher priority than OCI artifact annotations
// TODO(sgrunert): allow merging OCI artifact profiles with security context ones.
if profileField == nil || profileField.ProfileType == types.SecurityProfile_Unconfined {
ociArtifactProfile, err := seccompociartifact.New().TryPull(ctx, sys, containerName, sandboxAnnotations, imageAnnotations)
if err != nil {
return nil, "", fmt.Errorf("try to pull OCI artifact seccomp profile: %w", err)
}

if ociArtifactProfile != nil {
notifier, err := c.applyProfileFromBytes(ctx, ociArtifactProfile, msgChan, containerID, sandboxAnnotations, specGenerator)
if err != nil {
return nil, "", fmt.Errorf("apply profile from bytes: %w", err)
}

return notifier, "", nil
}
}

if profileField == nil {
if !c.UseDefaultWhenEmpty() {
// running w/o seccomp, aka unconfined
Expand Down Expand Up @@ -226,7 247,7 @@ func (c *Config) Setup(
if err != nil {
return nil, "", fmt.Errorf("load default profile: %w", err)
}
notifier, err := c.injectNotifier(ctx, msgChan, containerID, annotations, linuxSpecs)
notifier, err := c.injectNotifier(ctx, msgChan, containerID, sandboxAnnotations, linuxSpecs)
if err != nil {
return nil, "", fmt.Errorf("inject notifier: %w", err)
}
Expand All @@ -243,14 264,33 @@ func (c *Config) Setup(
)
}

linuxSpecs, err := seccomp.LoadProfileFromBytes(file, specGenerator.Config)
notifier, err := c.applyProfileFromBytes(ctx, file, msgChan, containerID, sandboxAnnotations, specGenerator)
if err != nil {
return nil, "", fmt.Errorf("load local profile: %w", err)
return nil, "", fmt.Errorf("apply profile from bytes: %w", err)
}
notifier, err := c.injectNotifier(ctx, msgChan, containerID, annotations, linuxSpecs)

return notifier, localhostRef, nil
}

// Setup can be used to setup the seccomp profile.
func (c *Config) applyProfileFromBytes(
ctx context.Context,
fileBytes []byte,
msgChan chan Notification,
containerID string,
sandboxAnnotations map[string]string,
specGenerator *generate.Generator,
) (*Notifier, error) {
linuxSpecs, err := seccomp.LoadProfileFromBytes(fileBytes, specGenerator.Config)
if err != nil {
return nil, "", fmt.Errorf("inject notifier: %w", err)
return nil, fmt.Errorf("load local profile: %w", err)
}

notifier, err := c.injectNotifier(ctx, msgChan, containerID, sandboxAnnotations, linuxSpecs)
if err != nil {
return nil, fmt.Errorf("inject notifier: %w", err)
}

specGenerator.Config.Linux.Seccomp = linuxSpecs
return notifier, localhostRef, nil
return notifier, nil
}
9 changes: 9 additions & 0 deletions internal/config/seccomp/seccomp_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -102,8 102,11 @@ var _ = t.Describe("Config", func() {
_, ref, err := sut.Setup(
context.Background(),
nil,
nil,
"",
"",
nil,
nil,
&generator,
field,
)
Expand All @@ -127,8 130,11 @@ var _ = t.Describe("Config", func() {
_, ref, err := sut.Setup(
context.Background(),
nil,
nil,
"",
"",
nil,
nil,
&generator,
field,
)
Expand All @@ -151,7 157,10 @@ var _ = t.Describe("Config", func() {
_, _, err = sut.Setup(
context.Background(),
nil,
nil,
"",
"",
nil,
nil,
&generator,
field,
Expand Down
93 changes: 93 additions & 0 deletions internal/config/seccomp/seccompociartifact/seccompociartifact.go
Original file line number Diff line number Diff line change
@@ -0,0 1,93 @@
package seccompociartifact

import (
"context"
"fmt"
"io/fs"
"os"
"path/filepath"

"github.com/containers/image/v5/types"

"github.com/cri-o/cri-o/internal/config/ociartifact"
"github.com/cri-o/cri-o/internal/log"
"github.com/cri-o/cri-o/pkg/annotations"
)

// SeccompOCIArtifact is the main structure for handling seccomp related OCI
// artifacts.
type SeccompOCIArtifact struct {
ociArtifactImpl ociartifact.Impl
}

// New creates a new seccomp OCI artifact handler.
func New() *SeccompOCIArtifact {
return &SeccompOCIArtifact{
ociArtifactImpl: ociartifact.New(),
}
}

// TryPull tries to pull the OCI artifact seccomp profile while evaluating
// the provided annotations.
func (s *SeccompOCIArtifact) TryPull(
ctx context.Context,
sys *types.SystemContext,
containerName string,
podAnnotations, imageAnnotations map[string]string,
) (profile []byte, err error) {
log.Debugf(ctx, "Evaluating seccomp annotations")

profileRef := ""
containerKey := fmt.Sprintf("%s/%s", annotations.SeccompProfileAnnotation, containerName)
if val, ok := podAnnotations[containerKey]; ok {
log.Infof(ctx, "Found container specific seccomp profile annotation: %s=%s", containerKey, val)
profileRef = val
} else if val, ok := podAnnotations[annotations.SeccompProfileAnnotation]; ok {
log.Infof(ctx, "Found pod specific seccomp profile annotation: %s=%s", annotations.SeccompProfileAnnotation, val)
profileRef = val
} else if val, ok := imageAnnotations[annotations.SeccompProfileAnnotation]; ok {
log.Infof(ctx, "Found image specific seccomp profile annotation: %s=%s", annotations.SeccompProfileAnnotation, val)
profileRef = val
}

if profileRef == "" {
return nil, nil
}

artifact, err := s.ociArtifactImpl.Pull(ctx, sys, profileRef)
if err != nil {
return nil, fmt.Errorf("pull OCI artifact: %w", err)
}
defer artifact.Cleanup()

const jsonExt = ".json"
seccompProfilePath := ""
if err := filepath.Walk(artifact.MountPath,
func(p string, info os.FileInfo, err error) error {
if err != nil {
return err
}

if info.IsDir() ||
info.Mode()&os.ModeSymlink == os.ModeSymlink ||
filepath.Ext(info.Name()) != jsonExt {
return nil
}

seccompProfilePath = p

// TODO(sgrunert): allow merging profiles, not just choosing the first one
return fs.SkipAll
}); err != nil {
return nil, fmt.Errorf("walk %s: %w", artifact.MountPath, err)
}

log.Infof(ctx, "Trying to read profile from: %s", seccompProfilePath)
profileContent, err := os.ReadFile(seccompProfilePath)
if err != nil {
return nil, fmt.Errorf("read %s from file store: %w", seccompProfilePath, err)
}

log.Infof(ctx, "Retrieved OCI artifact seccomp profile of len: %d", len(profileContent))
return profileContent, nil
}
Loading

0 comments on commit b20d06c

Please sign in to comment.