diff --git a/Makefile b/Makefile index e827c66..72edf5d 100644 --- a/Makefile +++ b/Makefile @@ -89,7 +89,7 @@ test: manifests generate envtest ## Run tests. KUBEBUILDER_ASSETS="$(shell $(ENVTEST) use $(ENVTEST_K8S_VERSION) --bin-dir $(LOCALBIN) -p path)" go test ./... -coverprofile cover.out GOLANGCI_LINT = $(shell pwd)/bin/golangci-lint -GOLANGCI_LINT_VERSION ?= v1.54.2 +GOLANGCI_LINT_VERSION ?= v2.10 golangci-lint: @[ -f $(GOLANGCI_LINT) ] || { \ set -e ;\ diff --git a/api/v1/store.go b/api/v1/store.go index 2a2698e..f36deeb 100644 --- a/api/v1/store.go +++ b/api/v1/store.go @@ -244,9 +244,11 @@ type ContainerSpec struct { VolumeMounts []corev1.VolumeMount `json:"volumeMounts,omitempty"` ImagePullSecrets []corev1.LocalObjectReference `json:"imagePullSecrets,omitempty"` RestartPolicy corev1.RestartPolicy `json:"restartPolicy,omitempty"` - SecurityContext *corev1.PodSecurityContext `json:"podSecurityContext,omitempty"` - ExtraContainers []corev1.Container `json:"extraContainers,omitempty"` - InitContainers []corev1.Container `json:"initContainers,omitempty"` + + // +kubebuilder:default={"fsGroup":82,"runAsGroup":82,"runAsNonRoot":true,"runAsUser":82,"seccompProfile":{"type":"RuntimeDefault"}} + SecurityContext *corev1.PodSecurityContext `json:"podSecurityContext,omitempty"` + ExtraContainers []corev1.Container `json:"extraContainers,omitempty"` + InitContainers []corev1.Container `json:"initContainers,omitempty"` // +kubebuilder:default=2 Replicas int32 `json:"replicas,omitempty"` diff --git a/internal/cronjob/scheduled_task.go b/internal/cronjob/scheduled_task.go index 45faba7..e16809c 100644 --- a/internal/cronjob/scheduled_task.go +++ b/internal/cronjob/scheduled_task.go @@ -59,11 +59,12 @@ func ScheduledTaskJob(store v1.Store) *batchv1.CronJob { annotations := util.GetDefaultContainerAnnotations(CONTAINER_NAME_SCHEDULED_JOB, store, store.Spec.SetupJobContainer.Annotations) - containers := append(store.Spec.Container.ExtraContainers, corev1.Container{ + containers := append(util.DefaultContainerSecurityContexts(store.Spec.Container.ExtraContainers), corev1.Container{ Name: CONTAINER_NAME_SCHEDULED_JOB, VolumeMounts: store.Spec.Container.VolumeMounts, ImagePullPolicy: store.Spec.Container.ImagePullPolicy, Image: store.Spec.Container.Image, + SecurityContext: util.RestrictedContainerSecurityContext(), Command: []string{"sh", "-c"}, Args: []string{store.Spec.ScheduledTask.Command}, Env: store.GetEnv(), @@ -111,9 +112,9 @@ func ScheduledTaskJob(store v1.Store) *batchv1.CronJob { EnableServiceLinks: store.Spec.Container.EnableServiceLinks, RestartPolicy: "Never", Containers: containers, - SecurityContext: store.Spec.Container.SecurityContext, + SecurityContext: util.DefaultPodSecurityContext(store.Spec.Container.SecurityContext), ServiceAccountName: sa, - InitContainers: store.Spec.Container.InitContainers, + InitContainers: util.DefaultContainerSecurityContexts(store.Spec.Container.InitContainers), }, }, }, diff --git a/internal/cronjob/scheduled_task_test.go b/internal/cronjob/scheduled_task_test.go new file mode 100644 index 0000000..0f012e4 --- /dev/null +++ b/internal/cronjob/scheduled_task_test.go @@ -0,0 +1,39 @@ +package cronjob_test + +import ( + "testing" + + v1 "github.com/shopware/shopware-operator/api/v1" + "github.com/shopware/shopware-operator/internal/cronjob" + "github.com/stretchr/testify/assert" + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +func TestScheduledTaskJobUsesRestrictedContainerSecurityContext(t *testing.T) { + store := v1.Store{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-store", + Namespace: "test", + }, + Spec: v1.StoreSpec{ + Container: v1.ContainerSpec{ + Image: "shopware:latest", + }, + ScheduledTask: v1.ScheduledTaskSpec{ + Schedule: "0 * * * *", + TimeZone: "Etc/UTC", + Command: "bin/console scheduled-task:run", + }, + }, + } + + result := cronjob.ScheduledTaskJob(store) + container := result.Spec.JobTemplate.Spec.Template.Spec.Containers[0] + + assert.NotNil(t, container.SecurityContext) + assert.NotNil(t, container.SecurityContext.AllowPrivilegeEscalation) + assert.False(t, *container.SecurityContext.AllowPrivilegeEscalation) + assert.NotNil(t, container.SecurityContext.Capabilities) + assert.Equal(t, []corev1.Capability{"ALL"}, container.SecurityContext.Capabilities.Drop) +} diff --git a/internal/deployment/admin.go b/internal/deployment/admin.go index 92cbd83..5562829 100644 --- a/internal/deployment/admin.go +++ b/internal/deployment/admin.go @@ -86,7 +86,7 @@ func AdminDeployment(store v1.Store) *appsv1.Deployment { } } - containers := append(containerSpec.ExtraContainers, corev1.Container{ + containers := append(util.DefaultContainerSecurityContexts(containerSpec.ExtraContainers), corev1.Container{ LivenessProbe: &corev1.Probe{ ProbeHandler: corev1.ProbeHandler{ HTTPGet: &corev1.HTTPGetAction{ @@ -116,6 +116,7 @@ func AdminDeployment(store v1.Store) *appsv1.Deployment { Name: appName, Image: containerSpec.Image, ImagePullPolicy: containerSpec.ImagePullPolicy, + SecurityContext: util.RestrictedContainerSecurityContext(), Env: envs, VolumeMounts: containerSpec.VolumeMounts, Ports: []corev1.ContainerPort{ @@ -170,8 +171,8 @@ func AdminDeployment(store v1.Store) *appsv1.Deployment { EnableServiceLinks: containerSpec.EnableServiceLinks, RestartPolicy: containerSpec.RestartPolicy, Containers: containers, - SecurityContext: containerSpec.SecurityContext, - InitContainers: containerSpec.InitContainers, + SecurityContext: util.DefaultPodSecurityContext(containerSpec.SecurityContext), + InitContainers: util.DefaultContainerSecurityContexts(containerSpec.InitContainers), }, }, }, diff --git a/internal/deployment/storefront.go b/internal/deployment/storefront.go index a9f81af..87893f8 100644 --- a/internal/deployment/storefront.go +++ b/internal/deployment/storefront.go @@ -91,7 +91,7 @@ func StorefrontDeployment(store v1.Store) *appsv1.Deployment { } } - containers := append(containerSpec.ExtraContainers, corev1.Container{ + containers := append(util.DefaultContainerSecurityContexts(containerSpec.ExtraContainers), corev1.Container{ Name: DEPLOYMENT_STOREFRONT_CONTAINER_NAME, LivenessProbe: &corev1.Probe{ ProbeHandler: corev1.ProbeHandler{ @@ -121,6 +121,7 @@ func StorefrontDeployment(store v1.Store) *appsv1.Deployment { }, Image: containerSpec.Image, ImagePullPolicy: containerSpec.ImagePullPolicy, + SecurityContext: util.RestrictedContainerSecurityContext(), Env: envs, VolumeMounts: containerSpec.VolumeMounts, Ports: []corev1.ContainerPort{ @@ -174,8 +175,8 @@ func StorefrontDeployment(store v1.Store) *appsv1.Deployment { EnableServiceLinks: containerSpec.EnableServiceLinks, RestartPolicy: containerSpec.RestartPolicy, Containers: containers, - SecurityContext: containerSpec.SecurityContext, - InitContainers: containerSpec.InitContainers, + SecurityContext: util.DefaultPodSecurityContext(containerSpec.SecurityContext), + InitContainers: util.DefaultContainerSecurityContexts(containerSpec.InitContainers), }, }, }, diff --git a/internal/deployment/storefront_test.go b/internal/deployment/storefront_test.go index 86f9c04..7b8310d 100644 --- a/internal/deployment/storefront_test.go +++ b/internal/deployment/storefront_test.go @@ -168,12 +168,36 @@ func TestStorefrontDeployment(t *testing.T) { result := deployment.StorefrontDeployment(store) - // Verify security context is overwritten + // Verify pod security context is overwritten. assert.NotNil(t, result.Spec.Template.Spec.SecurityContext) assert.Equal(t, int64(2000), *result.Spec.Template.Spec.SecurityContext.RunAsGroup) assert.Nil(t, result.Spec.Template.Spec.SecurityContext.RunAsUser) }) + t.Run("test storefront container security context is restricted", func(t *testing.T) { + store := v1.Store{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-store", + Namespace: "test", + }, + Spec: v1.StoreSpec{ + Container: v1.ContainerSpec{ + Image: "shopware:latest", + }, + SecretName: "store-secret", + }, + } + + result := deployment.StorefrontDeployment(store) + container := result.Spec.Template.Spec.Containers[0] + + assert.NotNil(t, container.SecurityContext) + assert.NotNil(t, container.SecurityContext.AllowPrivilegeEscalation) + assert.False(t, *container.SecurityContext.AllowPrivilegeEscalation) + assert.NotNil(t, container.SecurityContext.Capabilities) + assert.Equal(t, []corev1.Capability{"ALL"}, container.SecurityContext.Capabilities.Drop) + }) + t.Run("test service account merge", func(t *testing.T) { store := v1.Store{ ObjectMeta: metav1.ObjectMeta{ diff --git a/internal/deployment/worker.go b/internal/deployment/worker.go index 4bffca1..a213941 100644 --- a/internal/deployment/worker.go +++ b/internal/deployment/worker.go @@ -78,11 +78,12 @@ func WorkerDeployment(store v1.Store) *appsv1.Deployment { // Merge containerSpec.ExtraEnvs to override with merged values from WorkerDeploymentContainer envs := util.MergeEnv(store.GetEnv(), containerSpec.ExtraEnvs) - containers := append(containerSpec.ExtraContainers, corev1.Container{ + containers := append(util.DefaultContainerSecurityContexts(containerSpec.ExtraContainers), corev1.Container{ Name: appName, Image: containerSpec.Image, ImagePullPolicy: containerSpec.ImagePullPolicy, Env: envs, + SecurityContext: util.RestrictedContainerSecurityContext(), Command: []string{ "bin/console", }, @@ -145,8 +146,8 @@ func WorkerDeployment(store v1.Store) *appsv1.Deployment { EnableServiceLinks: containerSpec.EnableServiceLinks, RestartPolicy: containerSpec.RestartPolicy, Containers: containers, - SecurityContext: containerSpec.SecurityContext, - InitContainers: containerSpec.InitContainers, + SecurityContext: util.DefaultPodSecurityContext(containerSpec.SecurityContext), + InitContainers: util.DefaultContainerSecurityContexts(containerSpec.InitContainers), }, }, }, diff --git a/internal/job/command.go b/internal/job/command.go index 0fb66ab..088ba4b 100644 --- a/internal/job/command.go +++ b/internal/job/command.go @@ -116,11 +116,12 @@ func getJobSpec(store v1.Store, ex v1.StoreExec, labels map[string]string) batch envs := util.MergeEnv(store.GetEnv(), ex.Spec.ExtraEnvs) - containers := append(containerSpec.ExtraContainers, corev1.Container{ + containers := append(util.DefaultContainerSecurityContexts(containerSpec.ExtraContainers), corev1.Container{ Name: CONTAINER_NAME_COMMAND, VolumeMounts: containerSpec.VolumeMounts, ImagePullPolicy: containerSpec.ImagePullPolicy, Image: containerSpec.Image, + SecurityContext: util.RestrictedContainerSecurityContext(), Command: []string{"sh", "-c"}, Args: []string{ex.Spec.Script}, Env: envs, @@ -154,8 +155,8 @@ func getJobSpec(store v1.Store, ex v1.StoreExec, labels map[string]string) batch EnableServiceLinks: containerSpec.EnableServiceLinks, RestartPolicy: "Never", Containers: containers, - SecurityContext: containerSpec.SecurityContext, - InitContainers: containerSpec.InitContainers, + SecurityContext: util.DefaultPodSecurityContext(containerSpec.SecurityContext), + InitContainers: util.DefaultContainerSecurityContexts(containerSpec.InitContainers), }, }, } diff --git a/internal/job/command_test.go b/internal/job/command_test.go index 52c2632..b9b173a 100644 --- a/internal/job/command_test.go +++ b/internal/job/command_test.go @@ -105,6 +105,39 @@ func TestCommandJob(t *testing.T) { assert.Equal(t, []string{"bin/console cache:clear"}, container.Args) assert.Equal(t, "shopware-command", container.Name) }) + + t.Run("test container security context is restricted", func(t *testing.T) { + store := v1.Store{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-store", + Namespace: "test", + }, + Spec: v1.StoreSpec{ + Container: v1.ContainerSpec{ + Image: "shopware:latest", + }, + }, + } + + exec := v1.StoreExec{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-exec", + Namespace: "test", + }, + Spec: v1.StoreExecSpec{ + Script: "echo test", + }, + } + + result := job.CommandJob(store, exec) + container := result.Spec.Template.Spec.Containers[0] + + assert.NotNil(t, container.SecurityContext) + assert.NotNil(t, container.SecurityContext.AllowPrivilegeEscalation) + assert.False(t, *container.SecurityContext.AllowPrivilegeEscalation) + assert.NotNil(t, container.SecurityContext.Capabilities) + assert.Equal(t, []corev1.Capability{"ALL"}, container.SecurityContext.Capabilities.Drop) + }) } func TestCommandCronJob(t *testing.T) { @@ -144,4 +177,38 @@ func TestCommandCronJob(t *testing.T) { assert.Equal(t, "test-exec", result.Name) assert.Equal(t, "test", result.Namespace) }) + + t.Run("test cron container security context is restricted", func(t *testing.T) { + store := v1.Store{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-store", + Namespace: "test", + }, + Spec: v1.StoreSpec{ + Container: v1.ContainerSpec{ + Image: "shopware:latest", + }, + }, + } + + exec := v1.StoreExec{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-exec", + Namespace: "test", + }, + Spec: v1.StoreExecSpec{ + Script: "echo test", + CronSchedule: "*/5 * * * *", + }, + } + + result := job.CommandCronJob(store, exec) + container := result.Spec.JobTemplate.Spec.Template.Spec.Containers[0] + + assert.NotNil(t, container.SecurityContext) + assert.NotNil(t, container.SecurityContext.AllowPrivilegeEscalation) + assert.False(t, *container.SecurityContext.AllowPrivilegeEscalation) + assert.NotNil(t, container.SecurityContext.Capabilities) + assert.Equal(t, []corev1.Capability{"ALL"}, container.SecurityContext.Capabilities.Drop) + }) } diff --git a/internal/job/migration.go b/internal/job/migration.go index 3683e14..d3ea1db 100644 --- a/internal/job/migration.go +++ b/internal/job/migration.go @@ -53,11 +53,12 @@ func MigrationJob(store v1.Store) *batchv1.Job { // Merge containerSpec.ExtraEnvs to override with merged values from MigrationJobContainer envs := util.MergeEnv(store.GetEnv(), containerSpec.ExtraEnvs) - containers := append(containerSpec.ExtraContainers, corev1.Container{ + containers := append(util.DefaultContainerSecurityContexts(containerSpec.ExtraContainers), corev1.Container{ Name: CONTAINER_NAME_MIGRATION_JOB, VolumeMounts: containerSpec.VolumeMounts, ImagePullPolicy: containerSpec.ImagePullPolicy, Image: containerSpec.Image, + SecurityContext: util.RestrictedContainerSecurityContext(), Command: []string{"sh", "-c"}, Args: []string{store.Spec.MigrationScript}, Env: envs, @@ -93,8 +94,8 @@ func MigrationJob(store v1.Store) *batchv1.Job { EnableServiceLinks: containerSpec.EnableServiceLinks, RestartPolicy: "Never", Containers: containers, - SecurityContext: containerSpec.SecurityContext, - InitContainers: containerSpec.InitContainers, + SecurityContext: util.DefaultPodSecurityContext(containerSpec.SecurityContext), + InitContainers: util.DefaultContainerSecurityContexts(containerSpec.InitContainers), }, }, }, diff --git a/internal/job/migration_test.go b/internal/job/migration_test.go index 6e85fa0..14fd90f 100644 --- a/internal/job/migration_test.go +++ b/internal/job/migration_test.go @@ -205,4 +205,32 @@ func TestMigrationJob(t *testing.T) { // Verify service account is overwritten assert.Equal(t, "migration-sa", result.Spec.Template.Spec.ServiceAccountName) }) + + t.Run("test container security context is restricted", func(t *testing.T) { + store := v1.Store{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-store", + Namespace: "test", + }, + Spec: v1.StoreSpec{ + Container: v1.ContainerSpec{ + Image: "shopware:latest", + }, + MigrationScript: "/migrate.sh", + SecretName: "store-secret", + }, + Status: v1.StoreStatus{ + CurrentImageTag: "shopware:old", + }, + } + + result := job.MigrationJob(store) + container := result.Spec.Template.Spec.Containers[0] + + assert.NotNil(t, container.SecurityContext) + assert.NotNil(t, container.SecurityContext.AllowPrivilegeEscalation) + assert.False(t, *container.SecurityContext.AllowPrivilegeEscalation) + assert.NotNil(t, container.SecurityContext.Capabilities) + assert.Equal(t, []corev1.Capability{"ALL"}, container.SecurityContext.Capabilities.Drop) + }) } diff --git a/internal/job/setup.go b/internal/job/setup.go index 53456c8..9cba1e4 100644 --- a/internal/job/setup.go +++ b/internal/job/setup.go @@ -62,11 +62,12 @@ func SetupJob(store v1.Store) *batchv1.Job { // Merge containerSpec.ExtraEnvs to override with merged values from SetupJobContainer envs = util.MergeEnv(envs, containerSpec.ExtraEnvs) - containers := append(containerSpec.ExtraContainers, corev1.Container{ + containers := append(util.DefaultContainerSecurityContexts(containerSpec.ExtraContainers), corev1.Container{ Name: CONTAINER_NAME_SETUP_JOB, VolumeMounts: containerSpec.VolumeMounts, ImagePullPolicy: containerSpec.ImagePullPolicy, Image: containerSpec.Image, + SecurityContext: util.RestrictedContainerSecurityContext(), Command: []string{"sh", "-c"}, Args: []string{store.Spec.SetupScript}, Env: envs, @@ -102,8 +103,8 @@ func SetupJob(store v1.Store) *batchv1.Job { EnableServiceLinks: containerSpec.EnableServiceLinks, RestartPolicy: "Never", Containers: containers, - SecurityContext: containerSpec.SecurityContext, - InitContainers: containerSpec.InitContainers, + SecurityContext: util.DefaultPodSecurityContext(containerSpec.SecurityContext), + InitContainers: util.DefaultContainerSecurityContexts(containerSpec.InitContainers), }, }, }, diff --git a/internal/job/setup_test.go b/internal/job/setup_test.go index a273d04..2d9c6db 100644 --- a/internal/job/setup_test.go +++ b/internal/job/setup_test.go @@ -280,4 +280,32 @@ func TestSetupJob(t *testing.T) { assert.NotNil(t, result.Spec.Template.Spec.EnableServiceLinks) assert.False(t, *result.Spec.Template.Spec.EnableServiceLinks) }) + + t.Run("test container security context is restricted", func(t *testing.T) { + store := v1.Store{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-store", + Namespace: "test", + }, + Spec: v1.StoreSpec{ + Container: v1.ContainerSpec{ + Image: "shopware:latest", + }, + SetupScript: "/setup.sh", + AdminCredentials: v1.Credentials{ + Username: "admin", + }, + SecretName: "store-secret", + }, + } + + result := job.SetupJob(store) + container := result.Spec.Template.Spec.Containers[0] + + assert.NotNil(t, container.SecurityContext) + assert.NotNil(t, container.SecurityContext.AllowPrivilegeEscalation) + assert.False(t, *container.SecurityContext.AllowPrivilegeEscalation) + assert.NotNil(t, container.SecurityContext.Capabilities) + assert.Equal(t, []corev1.Capability{"ALL"}, container.SecurityContext.Capabilities.Drop) + }) } diff --git a/internal/job/snapshot.go b/internal/job/snapshot.go index b4f7733..2ae5cc1 100644 --- a/internal/job/snapshot.go +++ b/internal/job/snapshot.go @@ -112,11 +112,12 @@ func snapshotJob(store v1.Store, meta metav1.ObjectMeta, snapshot v1.StoreSnapsh } } - containers := append(snapshot.Container.ExtraContainers, corev1.Container{ + containers := append(util.DefaultContainerSecurityContexts(snapshot.Container.ExtraContainers), corev1.Container{ Name: CONTAINER_NAME_SNAPSHOT, VolumeMounts: vm, ImagePullPolicy: snapshot.Container.ImagePullPolicy, Image: snapshot.Container.Image, + SecurityContext: util.RestrictedContainerSecurityContext(), Args: args, Env: snapshot.GetEnv(store), Resources: snapshot.Container.Resources, @@ -166,8 +167,8 @@ func snapshotJob(store v1.Store, meta metav1.ObjectMeta, snapshot v1.StoreSnapsh EnableServiceLinks: snapshot.Container.EnableServiceLinks, RestartPolicy: "Never", Containers: containers, - SecurityContext: snapshot.Container.SecurityContext, - InitContainers: snapshot.Container.InitContainers, + SecurityContext: util.DefaultPodSecurityContext(snapshot.Container.SecurityContext), + InitContainers: util.DefaultContainerSecurityContexts(snapshot.Container.InitContainers), }, }, }, diff --git a/internal/job/snapshot_test.go b/internal/job/snapshot_test.go new file mode 100644 index 0000000..bbce352 --- /dev/null +++ b/internal/job/snapshot_test.go @@ -0,0 +1,53 @@ +package job_test + +import ( + "testing" + + v1 "github.com/shopware/shopware-operator/api/v1" + "github.com/shopware/shopware-operator/internal/job" + "github.com/stretchr/testify/assert" + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +func TestSnapshotCreateJobUsesRestrictedContainerSecurityContext(t *testing.T) { + store := v1.Store{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-store", + Namespace: "test", + }, + Spec: v1.StoreSpec{ + SecretName: "store-secret", + Database: v1.DatabaseSpec{ + Name: "shopware", + }, + S3Storage: v1.S3Storage{ + EndpointURL: "https://s3.example.com", + PrivateBucketName: "private", + PublicBucketName: "public", + }, + }, + } + + snapshot := v1.StoreSnapshotCreate{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-snapshot", + Namespace: "test", + }, + Spec: v1.StoreSnapshotSpec{ + Path: "/tmp/snapshot.zip", + Container: v1.ContainerSpec{ + Image: "shopware:snapshot", + }, + }, + } + + result := job.SnapshotCreateJob(store, snapshot) + container := result.Spec.Template.Spec.Containers[0] + + assert.NotNil(t, container.SecurityContext) + assert.NotNil(t, container.SecurityContext.AllowPrivilegeEscalation) + assert.False(t, *container.SecurityContext.AllowPrivilegeEscalation) + assert.NotNil(t, container.SecurityContext.Capabilities) + assert.Equal(t, []corev1.Capability{"ALL"}, container.SecurityContext.Capabilities.Drop) +} diff --git a/internal/pod/debug.go b/internal/pod/debug.go index c88b16e..f29abbd 100644 --- a/internal/pod/debug.go +++ b/internal/pod/debug.go @@ -55,7 +55,7 @@ func DebugPod(store v1.Store, storeDebugInstance v1.StoreDebugInstance) *corev1. } ports = append(ports, storeDebugInstance.Spec.ExtraContainerPorts...) - containers := append(store.Spec.Container.ExtraContainers, corev1.Container{ + containers := append(util.DefaultContainerSecurityContexts(store.Spec.Container.ExtraContainers), corev1.Container{ Name: deployment.DEPLOYMENT_STOREFRONT_CONTAINER_NAME, // we don't need the liveness and readiness probe to make sure that the container always starts Image: containerImage, // Use custom image if provided @@ -64,6 +64,7 @@ func DebugPod(store v1.Store, storeDebugInstance v1.StoreDebugInstance) *corev1. VolumeMounts: store.Spec.Container.VolumeMounts, Ports: ports, Resources: store.Spec.Container.Resources, + SecurityContext: util.RestrictedContainerSecurityContext(), }) podSpec.Spec.Containers = containers @@ -72,8 +73,8 @@ func DebugPod(store v1.Store, storeDebugInstance v1.StoreDebugInstance) *corev1. podSpec.Spec.NodeSelector = store.Spec.Container.NodeSelector podSpec.Spec.ImagePullSecrets = store.Spec.Container.ImagePullSecrets podSpec.Spec.EnableServiceLinks = store.Spec.Container.EnableServiceLinks - podSpec.Spec.SecurityContext = store.Spec.Container.SecurityContext - podSpec.Spec.InitContainers = store.Spec.Container.InitContainers + podSpec.Spec.SecurityContext = util.DefaultPodSecurityContext(store.Spec.Container.SecurityContext) + podSpec.Spec.InitContainers = util.DefaultContainerSecurityContexts(store.Spec.Container.InitContainers) if store.Spec.ServiceAccountName != "" { podSpec.Spec.ServiceAccountName = store.Spec.ServiceAccountName diff --git a/internal/util/security.go b/internal/util/security.go new file mode 100644 index 0000000..5470810 --- /dev/null +++ b/internal/util/security.go @@ -0,0 +1,64 @@ +package util + +import corev1 "k8s.io/api/core/v1" + +// RestrictedContainerSecurityContext returns the fixed container-level settings +// required by the restricted Pod Security Standard. The CRD default only covers +// podSecurityContext, but allowPrivilegeEscalation and capabilities live on each +// container's securityContext. +func RestrictedContainerSecurityContext() *corev1.SecurityContext { + allowPrivilegeEscalation := false + + return &corev1.SecurityContext{ + AllowPrivilegeEscalation: &allowPrivilegeEscalation, + Capabilities: &corev1.Capabilities{ + Drop: []corev1.Capability{ + "ALL", + }, + }, + } +} + +// DefaultContainerSecurityContexts keeps user-provided container security +// contexts unchanged and only fills missing ones with the restricted defaults. +func DefaultContainerSecurityContexts(containers []corev1.Container) []corev1.Container { + if containers == nil { + return nil + } + + defaulted := make([]corev1.Container, len(containers)) + copy(defaulted, containers) + for i := range defaulted { + if defaulted[i].SecurityContext == nil { + defaulted[i].SecurityContext = RestrictedContainerSecurityContext() + } + } + + return defaulted +} + +// RestrictedPodSecurityContext mirrors the podSecurityContext default from the +// CRD for resources that were not API-server defaulted before reconciliation. +func RestrictedPodSecurityContext() *corev1.PodSecurityContext { + runAsNonRoot := true + + return &corev1.PodSecurityContext{ + FSGroup: Int64(82), + RunAsGroup: Int64(82), + RunAsNonRoot: &runAsNonRoot, + RunAsUser: Int64(82), + SeccompProfile: &corev1.SeccompProfile{ + Type: corev1.SeccompProfileTypeRuntimeDefault, + }, + } +} + +// DefaultPodSecurityContext keeps CRD-provided values unchanged and only falls +// back to the restricted defaults when podSecurityContext is missing. +func DefaultPodSecurityContext(securityContext *corev1.PodSecurityContext) *corev1.PodSecurityContext { + if securityContext != nil { + return securityContext + } + + return RestrictedPodSecurityContext() +} diff --git a/internal/util/security_test.go b/internal/util/security_test.go new file mode 100644 index 0000000..42ac9f0 --- /dev/null +++ b/internal/util/security_test.go @@ -0,0 +1,64 @@ +package util_test + +import ( + "testing" + + "github.com/shopware/shopware-operator/internal/util" + "github.com/stretchr/testify/assert" + corev1 "k8s.io/api/core/v1" +) + +func TestDefaultPodSecurityContextUsesRestrictedDefaultsWhenMissing(t *testing.T) { + securityContext := util.DefaultPodSecurityContext(nil) + + assert.NotNil(t, securityContext) + assert.NotNil(t, securityContext.FSGroup) + assert.Equal(t, int64(82), *securityContext.FSGroup) + assert.NotNil(t, securityContext.RunAsGroup) + assert.Equal(t, int64(82), *securityContext.RunAsGroup) + assert.NotNil(t, securityContext.RunAsNonRoot) + assert.True(t, *securityContext.RunAsNonRoot) + assert.NotNil(t, securityContext.RunAsUser) + assert.Equal(t, int64(82), *securityContext.RunAsUser) + assert.NotNil(t, securityContext.SeccompProfile) + assert.Equal(t, corev1.SeccompProfileTypeRuntimeDefault, securityContext.SeccompProfile.Type) +} + +func TestDefaultPodSecurityContextKeepsProvidedContext(t *testing.T) { + securityContext := &corev1.PodSecurityContext{ + RunAsUser: util.Int64(1000), + } + + assert.Same(t, securityContext, util.DefaultPodSecurityContext(securityContext)) +} + +func TestDefaultContainerSecurityContextsUsesRestrictedDefaultsWhenMissing(t *testing.T) { + containers := []corev1.Container{ + {Name: "sidecar"}, + } + + defaulted := util.DefaultContainerSecurityContexts(containers) + + assert.Nil(t, containers[0].SecurityContext) + assert.NotNil(t, defaulted[0].SecurityContext) + assert.NotNil(t, defaulted[0].SecurityContext.AllowPrivilegeEscalation) + assert.False(t, *defaulted[0].SecurityContext.AllowPrivilegeEscalation) + assert.NotNil(t, defaulted[0].SecurityContext.Capabilities) + assert.Equal(t, []corev1.Capability{"ALL"}, defaulted[0].SecurityContext.Capabilities.Drop) +} + +func TestDefaultContainerSecurityContextsKeepsProvidedContext(t *testing.T) { + securityContext := &corev1.SecurityContext{ + RunAsUser: util.Int64(1000), + } + containers := []corev1.Container{ + { + Name: "sidecar", + SecurityContext: securityContext, + }, + } + + defaulted := util.DefaultContainerSecurityContexts(containers) + + assert.Same(t, securityContext, defaulted[0].SecurityContext) +}