diff --git a/pkg/domain/infra/abi/play.go b/pkg/domain/infra/abi/play.go index ec74751fd55..2e988bc651a 100644 --- a/pkg/domain/infra/abi/play.go +++ b/pkg/domain/infra/abi/play.go @@ -845,11 +845,6 @@ func (ic *ContainerEngine) playKubePod(ctx context.Context, podName string, podY } } - seccompPaths, err := kube.InitializeSeccompPaths(podYAML.ObjectMeta.Annotations, options.SeccompProfileRoot) - if err != nil { - return nil, nil, err - } - // Set the restart policy from the kube yaml at the pod level in podman switch podYAML.Spec.RestartPolicy { case v1.RestartPolicyAlways: @@ -994,7 +989,7 @@ func (ic *ContainerEngine) playKubePod(ctx context.Context, podName string, podY PodSecurityContext: podYAML.Spec.SecurityContext, ReadOnly: readOnly, RestartPolicy: define.RestartPolicyNo, - SeccompPaths: seccompPaths, + SeccompProfileRoot: options.SeccompProfileRoot, SecretsManager: secretsManager, UserNSIsHost: p.Userns.IsHost(), Volumes: volumes, @@ -1084,7 +1079,7 @@ func (ic *ContainerEngine) playKubePod(ctx context.Context, podName string, podY PodSecurityContext: podYAML.Spec.SecurityContext, RestartPolicy: podSpec.PodSpecGen.RestartPolicy, // pass the restart policy to the container (https://github.com/containers/podman/issues/20903) ReadOnly: readOnly, - SeccompPaths: seccompPaths, + SeccompProfileRoot: options.SeccompProfileRoot, SecretsManager: secretsManager, UserNSIsHost: p.Userns.IsHost(), Volumes: volumes, diff --git a/pkg/specgen/generate/kube/kube.go b/pkg/specgen/generate/kube/kube.go index 27f73d09c89..634579b4e98 100644 --- a/pkg/specgen/generate/kube/kube.go +++ b/pkg/specgen/generate/kube/kube.go @@ -11,6 +11,7 @@ import ( "math" "net" "os" + "path/filepath" "regexp" "runtime" "slices" @@ -27,6 +28,7 @@ import ( "go.podman.io/common/pkg/secrets" "go.podman.io/image/v5/manifest" itypes "go.podman.io/image/v5/types" + "go.podman.io/podman/v6/libpod" "go.podman.io/podman/v6/libpod/define" ann "go.podman.io/podman/v6/pkg/annotations" "go.podman.io/podman/v6/pkg/domain/entities" @@ -164,8 +166,8 @@ type CtrSpecGenOptions struct { PodInfraID string // ConfigMaps the configuration maps for environment variables ConfigMaps []v1.ConfigMap - // SeccompPaths for finding the seccomp profile path - SeccompPaths *KubeSeccompPaths + // SeccompProfileRoot is the base directory for Localhost seccomp profiles + SeccompProfileRoot string // ReadOnly make all containers root file system readonly ReadOnly itypes.OptionalBool // RestartPolicy defines the restart policy of the container @@ -287,7 +289,11 @@ func ToSpecGen(ctx context.Context, opts *CtrSpecGenOptions) (*specgen.SpecGener s.InitContainerType = opts.InitContainerType - setupSecurityContext(s, opts.Container.SecurityContext, opts.PodSecurityContext) + err = setupSecurityContext(s, opts.Container.SecurityContext, opts.PodSecurityContext, opts.SeccompProfileRoot) + if err != nil { + return nil, fmt.Errorf("failed to configure securityContext: %w", err) + } + err = setupLivenessProbe(s, opts.Container, opts.RestartPolicy) if err != nil { return nil, fmt.Errorf("failed to configure livenessProbe: %w", err) @@ -297,11 +303,6 @@ func ToSpecGen(ctx context.Context, opts *CtrSpecGenOptions) (*specgen.SpecGener return nil, fmt.Errorf("failed to configure startupProbe: %w", err) } - // Since we prefix the container name with pod name to work-around the uniqueness requirement, - // the seccomp profile should reference the actual container name from the YAML - // but apply to the containers with the prefixed name - s.SeccompProfilePath = opts.SeccompPaths.FindForContainer(opts.Container.Name) - setupContainerResources(s, opts.Container) err = setupContainerDevices(s, opts.Container) @@ -957,7 +958,7 @@ func setupContainerDevices(s *specgen.SpecGenerator, containerYAML v1.Container) return nil } -func setupSecurityContext(s *specgen.SpecGenerator, securityContext *v1.SecurityContext, podSecurityContext *v1.PodSecurityContext) { +func setupSecurityContext(s *specgen.SpecGenerator, securityContext *v1.SecurityContext, podSecurityContext *v1.PodSecurityContext, profileRoot string) error { if securityContext == nil { securityContext = &v1.SecurityContext{} } @@ -1002,6 +1003,34 @@ func setupSecurityContext(s *specgen.SpecGenerator, securityContext *v1.Security s.SelinuxOpts = append(s.SelinuxOpts, fmt.Sprintf("filetype:%s", seopt.FileType)) } } + + seccompProfile := securityContext.SeccompProfile + if seccompProfile == nil { + seccompProfile = podSecurityContext.SeccompProfile + } + if seccompProfile == nil { + seccompProfile = &v1.SeccompProfile{Type: v1.SeccompProfileTypeRuntimeDefault} + } + if seccompProfile != nil { + switch seccompProfile.Type { + case v1.SeccompProfileTypeRuntimeDefault: + seccompProfilePath, err := libpod.DefaultSeccompPath() + if err != nil { + return err + } + s.SeccompProfilePath = seccompProfilePath + case v1.SeccompProfileTypeUnconfined: + s.SeccompProfilePath = "unconfined" + case v1.SeccompProfileTypeLocalhost: + if seccompProfile.LocalhostProfile == nil || *seccompProfile.LocalhostProfile == "" { + return fmt.Errorf("seccomp profile type %q requires localhostProfile", seccompProfile.Type) + } + s.SeccompProfilePath = filepath.Join(profileRoot, *seccompProfile.LocalhostProfile) + default: + return fmt.Errorf("invalid seccomp profile type: %s", seccompProfile.Type) + } + } + if caps := securityContext.Capabilities; caps != nil { for _, capability := range caps.Add { s.CapAdd = append(s.CapAdd, string(capability)) @@ -1031,6 +1060,8 @@ func setupSecurityContext(s *specgen.SpecGenerator, securityContext *v1.Security for _, group := range podSecurityContext.SupplementalGroups { s.Groups = append(s.Groups, strconv.FormatInt(group, 10)) } + + return nil } func quantityToInt64(quantity *resource.Quantity) int64 { diff --git a/pkg/specgen/generate/kube/kube_test.go b/pkg/specgen/generate/kube/kube_test.go index a1b7655e024..73e0e37649f 100644 --- a/pkg/specgen/generate/kube/kube_test.go +++ b/pkg/specgen/generate/kube/kube_test.go @@ -3,12 +3,15 @@ package kube import ( + "path/filepath" "testing" "github.com/stretchr/testify/assert" + "go.podman.io/podman/v6/libpod" v1 "go.podman.io/podman/v6/pkg/k8s.io/api/core/v1" "go.podman.io/podman/v6/pkg/k8s.io/apimachinery/pkg/api/resource" "go.podman.io/podman/v6/pkg/k8s.io/apimachinery/pkg/util/intstr" + "go.podman.io/podman/v6/pkg/specgen" ) func testPropagation(t *testing.T, propagation v1.MountPropagationMode, expected string) { @@ -180,3 +183,69 @@ func TestQuantityToInt64(t *testing.T) { }) } } + +func seccompProfile(profileType v1.SeccompProfileType, localhostProfile *string) *v1.SeccompProfile { + return &v1.SeccompProfile{Type: profileType, LocalhostProfile: localhostProfile} +} + +func stringPtr(value string) *string { + return &value +} + +func TestSetupSecurityContextSeccompProfile(t *testing.T) { + profileRoot := t.TempDir() + defaultPath, err := libpod.DefaultSeccompPath() + assert.NoError(t, err) + + tests := []struct { + name string + ctr *v1.SecurityContext + pod *v1.PodSecurityContext + expected string + }{ + { + name: "container profile", + ctr: &v1.SecurityContext{ + SeccompProfile: seccompProfile(v1.SeccompProfileTypeUnconfined, nil), + }, + expected: "unconfined", + }, + { + name: "pod profile", + pod: &v1.PodSecurityContext{ + SeccompProfile: seccompProfile(v1.SeccompProfileTypeLocalhost, stringPtr("profiles/pod.json")), + }, + expected: filepath.Join(profileRoot, "profiles/pod.json"), + }, + { + name: "container overrides pod", + ctr: &v1.SecurityContext{ + SeccompProfile: seccompProfile(v1.SeccompProfileTypeLocalhost, stringPtr("profiles/container.json")), + }, + pod: &v1.PodSecurityContext{ + SeccompProfile: seccompProfile(v1.SeccompProfileTypeUnconfined, nil), + }, + expected: filepath.Join(profileRoot, "profiles/container.json"), + }, + { + name: "unset profile uses default", + expected: defaultPath, + }, + { + name: "RuntimeDefault profile", + ctr: &v1.SecurityContext{ + SeccompProfile: seccompProfile(v1.SeccompProfileTypeRuntimeDefault, nil), + }, + expected: defaultPath, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + s := &specgen.SpecGenerator{} + err := setupSecurityContext(s, tt.ctr, tt.pod, profileRoot) + assert.NoError(t, err) + assert.Equal(t, tt.expected, s.SeccompProfilePath) + }) + } +} diff --git a/pkg/specgen/generate/kube/seccomp.go b/pkg/specgen/generate/kube/seccomp.go deleted file mode 100644 index ac06592a956..00000000000 --- a/pkg/specgen/generate/kube/seccomp.go +++ /dev/null @@ -1,93 +0,0 @@ -//go:build !remote && (linux || freebsd) - -package kube - -import ( - "fmt" - "path/filepath" - "strings" - - "go.podman.io/podman/v6/libpod" - v1 "go.podman.io/podman/v6/pkg/k8s.io/api/core/v1" -) - -// KubeSeccompPaths holds information about a pod YAML's seccomp configuration -// it holds both container and pod seccomp paths -type KubeSeccompPaths struct { - containerPaths map[string]string - podPath string -} - -// FindForContainer checks whether a container has a seccomp path configured for it -// if not, it returns the podPath, which should always have a value -func (k *KubeSeccompPaths) FindForContainer(ctrName string) string { - if path, ok := k.containerPaths[ctrName]; ok { - return path - } - return k.podPath -} - -// InitializeSeccompPaths takes annotations from the pod object metadata and finds annotations pertaining to seccomp -// it parses both pod and container level -// if the annotation is of the form "localhost/%s", the seccomp profile will be set to profileRoot/%s -func InitializeSeccompPaths(annotations map[string]string, profileRoot string) (*KubeSeccompPaths, error) { - seccompPaths := &KubeSeccompPaths{containerPaths: make(map[string]string)} - var err error - if annotations != nil { - for annKeyValue, seccomp := range annotations { - // check if it is prefaced with container.seccomp.security.alpha.kubernetes.io/ - prefixAndCtr := strings.Split(annKeyValue, "/") - // FIXME: Rework for deprecation removal https://github.com/containers/podman/issues/27501 - //nolint:staticcheck // deprecated k8s annotation constant - if prefixAndCtr[0]+"/" != v1.SeccompContainerAnnotationKeyPrefix { - continue - } else if len(prefixAndCtr) != 2 { - // this could be caused by a user inputting either of - // container.seccomp.security.alpha.kubernetes.io{,/} - // both of which are invalid - return nil, fmt.Errorf("invalid seccomp path: %s", prefixAndCtr[0]) - } - - path, err := verifySeccompPath(seccomp, profileRoot) - if err != nil { - return nil, err - } - seccompPaths.containerPaths[prefixAndCtr[1]] = path - } - // FIXME: Rework for deprecation removal https://github.com/containers/podman/issues/27501 - //nolint:staticcheck // deprecated k8s annotation constant - podSeccomp, ok := annotations[v1.SeccompPodAnnotationKey] - if ok { - seccompPaths.podPath, err = verifySeccompPath(podSeccomp, profileRoot) - } else { - seccompPaths.podPath, err = libpod.DefaultSeccompPath() - } - if err != nil { - return nil, err - } - } - return seccompPaths, nil -} - -// verifySeccompPath takes a path and checks whether it is a default, unconfined, or a path -// the available options are parsed as defined in https://kubernetes.io/docs/concepts/policy/pod-security-policy/#seccomp -func verifySeccompPath(path string, profileRoot string) (string, error) { - switch path { - // FIXME: Rework for deprecation removal https://github.com/containers/podman/issues/27501 - //nolint:staticcheck // deprecated k8s seccomp constant - case v1.DeprecatedSeccompProfileDockerDefault: - fallthrough - // FIXME: Rework for deprecation removal https://github.com/containers/podman/issues/27501 - //nolint:staticcheck // deprecated k8s seccomp constant - case v1.SeccompProfileRuntimeDefault: - return libpod.DefaultSeccompPath() - case "unconfined": - return path, nil - default: - parts := strings.Split(path, "/") - if parts[0] == "localhost" { - return filepath.Join(profileRoot, parts[1]), nil - } - return "", fmt.Errorf("invalid seccomp path: %s", path) - } -} diff --git a/test/e2e/play_kube_test.go b/test/e2e/play_kube_test.go index 3f16aa35f94..c012d0807c9 100644 --- a/test/e2e/play_kube_test.go +++ b/test/e2e/play_kube_test.go @@ -31,10 +31,12 @@ import ( "go.podman.io/podman/v6/pkg/bindings" "go.podman.io/podman/v6/pkg/bindings/play" v1 "go.podman.io/podman/v6/pkg/k8s.io/api/core/v1" + metav1 "go.podman.io/podman/v6/pkg/k8s.io/apimachinery/pkg/apis/meta/v1" "go.podman.io/podman/v6/pkg/util" . "go.podman.io/podman/v6/test/utils" "go.podman.io/podman/v6/utils" "go.podman.io/storage/pkg/stringid" + "sigs.k8s.io/yaml" ) var secretYaml = ` @@ -1571,6 +1573,15 @@ func generateKubeYaml(kind string, object any, pathname string) error { return writeYaml(k, pathname) } +// generateNativeKubeYaml marshals a kubernetes object and writes it as a YAML document. +func generateNativeKubeYaml(object any, pathname string) error { + content, err := yaml.Marshal(object) + if err != nil { + return err + } + return writeYaml(string(content), pathname) +} + // generateMultiDocKubeYaml writes multiple kube objects in one Yaml document. func generateMultiDocKubeYaml(kubeObjects []string, pathname string) error { var multiKube strings.Builder @@ -3418,18 +3429,40 @@ var _ = Describe("Podman kube play", func() { It("seccomp container level", func() { SkipIfRemote("podman-remote does not support --seccomp-profile-root flag") - // expect kube play is expected to set a seccomp label if it's applied as an annotation jsonFile, err := podmanTest.CreateSeccompJSON(seccompLinkEPERM) if err != nil { GinkgoWriter.Println(err) Skip("Failed to prepare seccomp.json for test.") } - ctrAnnotation := "container.seccomp.security.alpha.kubernetes.io/" + defaultCtrName - ctr := getCtr(withCmd([]string{"ln"}), withArg([]string{"/etc/motd", "/noneShallPass"})) - - pod := getPod(withCtr(ctr), withAnnotation(ctrAnnotation, "localhost/"+filepath.Base(jsonFile))) - err = generateKubeYaml("pod", pod, kubeYaml) + profile := filepath.Base(jsonFile) + pod := &v1.Pod{ + TypeMeta: metav1.TypeMeta{ + APIVersion: "v1", + Kind: "Pod", + }, + ObjectMeta: metav1.ObjectMeta{ + Name: defaultPodName, + }, + Spec: v1.PodSpec{ + RestartPolicy: v1.RestartPolicyNever, + Containers: []v1.Container{ + { + Name: defaultCtrName, + Image: defaultCtrImage, + Command: []string{"ln"}, + Args: []string{"/etc/motd", "/noneShallPass"}, + SecurityContext: &v1.SecurityContext{ + SeccompProfile: &v1.SeccompProfile{ + Type: v1.SeccompProfileTypeLocalhost, + LocalhostProfile: &profile, + }, + }, + }, + }, + }, + } + err = generateNativeKubeYaml(pod, kubeYaml) Expect(err).ToNot(HaveOccurred()) // CreateSeccompJSON will put the profile into podmanTest.TempDir. Use --seccomp-profile-root to tell kube play where to look @@ -3437,7 +3470,7 @@ var _ = Describe("Podman kube play", func() { kube.WaitWithDefaultTimeout() Expect(kube).Should(ExitCleanly()) - ctrName := getCtrNameInPod(pod) + ctrName := defaultPodName + "-" + defaultCtrName wait := podmanTest.Podman([]string{"wait", ctrName}) wait.WaitWithDefaultTimeout() Expect(wait).Should(Exit(0), "podman wait %s", ctrName) @@ -3450,7 +3483,6 @@ var _ = Describe("Podman kube play", func() { It("seccomp pod level", func() { SkipIfRemote("podman-remote does not support --seccomp-profile-root flag") - // expect kube play is expected to set a seccomp label if it's applied as an annotation jsonFile, err := podmanTest.CreateSeccompJSON(seccompLinkEPERM) if err != nil { GinkgoWriter.Println(err) @@ -3458,10 +3490,34 @@ var _ = Describe("Podman kube play", func() { } defer os.Remove(jsonFile) - ctr := getCtr(withCmd([]string{"ln"}), withArg([]string{"/etc/motd", "/noPodsShallPass"})) - - pod := getPod(withCtr(ctr), withAnnotation("seccomp.security.alpha.kubernetes.io/pod", "localhost/"+filepath.Base(jsonFile))) - err = generateKubeYaml("pod", pod, kubeYaml) + profile := filepath.Base(jsonFile) + pod := &v1.Pod{ + TypeMeta: metav1.TypeMeta{ + APIVersion: "v1", + Kind: "Pod", + }, + ObjectMeta: metav1.ObjectMeta{ + Name: defaultPodName, + }, + Spec: v1.PodSpec{ + RestartPolicy: v1.RestartPolicyNever, + SecurityContext: &v1.PodSecurityContext{ + SeccompProfile: &v1.SeccompProfile{ + Type: v1.SeccompProfileTypeLocalhost, + LocalhostProfile: &profile, + }, + }, + Containers: []v1.Container{ + { + Name: defaultCtrName, + Image: defaultCtrImage, + Command: []string{"ln"}, + Args: []string{"/etc/motd", "/noPodsShallPass"}, + }, + }, + }, + } + err = generateNativeKubeYaml(pod, kubeYaml) Expect(err).ToNot(HaveOccurred()) // CreateSeccompJSON will put the profile into podmanTest.TempDir. Use --seccomp-profile-root to tell kube play where to look @@ -3469,7 +3525,7 @@ var _ = Describe("Podman kube play", func() { kube.WaitWithDefaultTimeout() Expect(kube).Should(ExitCleanly()) - podName := getCtrNameInPod(pod) + podName := defaultPodName + "-" + defaultCtrName wait := podmanTest.Podman([]string{"wait", podName}) wait.WaitWithDefaultTimeout() Expect(wait).Should(ExitCleanly())