diff --git a/charts/postgres-operator/crds/operatorconfigurations.yaml b/charts/postgres-operator/crds/operatorconfigurations.yaml index 5f48d89ab..811dc2c28 100644 --- a/charts/postgres-operator/crds/operatorconfigurations.yaml +++ b/charts/postgres-operator/crds/operatorconfigurations.yaml @@ -498,6 +498,8 @@ spec: type: integer spilo_privileged: type: boolean + spilo_run_as_non_root: + type: boolean spilo_runasgroup: format: int64 type: integer diff --git a/charts/postgres-operator/crds/postgresqls.yaml b/charts/postgres-operator/crds/postgresqls.yaml index f3dcbab59..dd42406f8 100644 --- a/charts/postgres-operator/crds/postgresqls.yaml +++ b/charts/postgres-operator/crds/postgresqls.yaml @@ -2892,6 +2892,8 @@ spec: spiloRunAsGroup: format: int64 type: integer + spiloRunAsNonRoot: + type: boolean spiloRunAsUser: format: int64 type: integer diff --git a/docs/reference/cluster_manifest.md b/docs/reference/cluster_manifest.md index 7dddd5951..85fbe3365 100644 --- a/docs/reference/cluster_manifest.md +++ b/docs/reference/cluster_manifest.md @@ -85,6 +85,16 @@ These parameters are grouped directly under the `spec` key in the manifest. requires a custom Spilo image. Note the FSGroup of a Pod cannot be changed without recreating a new Pod. Optional. +* **spiloRunAsNonRoot** + sets `runAsNonRoot` in the pod and the Spilo container security context. + This will override the **spilo_run_as_non_root** operator parameter. Useful + in environments where Pod Security standards or mutating admission webhooks + enforce running as non-root: without this option such webhooks patch the + operator-generated StatefulSet after creation, so the operator detects + "pod changes" on every sync and keeps triggering rolling updates with + switchovers. Unset by default: `runAsNonRoot` is omitted from the generated + pod spec unless this option or the operator parameter is defined. Optional. + * **livenessProbe** Allows for adding a liveness probe to the Spilo container to detect if it's running properly. diff --git a/docs/reference/operator_parameters.md b/docs/reference/operator_parameters.md index 44a015cee..08b4285bf 100644 --- a/docs/reference/operator_parameters.md +++ b/docs/reference/operator_parameters.md @@ -537,6 +537,16 @@ configuration they are grouped under the `kubernetes` key. non-root process, but requires a custom Spilo image. Note the FSGroup of a Pod cannot be changed without recreating a new Pod. +* **spilo_run_as_non_root** + sets `runAsNonRoot` in the pod and the Spilo container security context. + Useful in environments where Pod Security standards or mutating admission + webhooks enforce running as non-root: without this option such webhooks + patch the operator-generated StatefulSet after creation, so the operator + detects "pod changes" on every sync and keeps triggering rolling updates + with switchovers. Unset by default: `runAsNonRoot` is omitted from the + generated pod spec unless this option (or the cluster manifest option + `spiloRunAsNonRoot`) is defined. + * **spilo_privileged** whether the Spilo container should run in privileged mode. Privileged mode is used for AWS volume resizing and not required if you don't need that diff --git a/manifests/operatorconfiguration.crd.yaml b/manifests/operatorconfiguration.crd.yaml index 5f48d89ab..811dc2c28 100644 --- a/manifests/operatorconfiguration.crd.yaml +++ b/manifests/operatorconfiguration.crd.yaml @@ -498,6 +498,8 @@ spec: type: integer spilo_privileged: type: boolean + spilo_run_as_non_root: + type: boolean spilo_runasgroup: format: int64 type: integer diff --git a/manifests/postgresql-operator-default-configuration.yaml b/manifests/postgresql-operator-default-configuration.yaml index 07af921f6..24e410f0f 100644 --- a/manifests/postgresql-operator-default-configuration.yaml +++ b/manifests/postgresql-operator-default-configuration.yaml @@ -124,6 +124,7 @@ configuration: # spilo_runasuser: 101 # spilo_runasgroup: 103 # spilo_fsgroup: 103 + # spilo_run_as_non_root: true spilo_privileged: false storage_resize_mode: pvc # toleration: diff --git a/manifests/postgresql.crd.yaml b/manifests/postgresql.crd.yaml index f3dcbab59..dd42406f8 100644 --- a/manifests/postgresql.crd.yaml +++ b/manifests/postgresql.crd.yaml @@ -2892,6 +2892,8 @@ spec: spiloRunAsGroup: format: int64 type: integer + spiloRunAsNonRoot: + type: boolean spiloRunAsUser: format: int64 type: integer diff --git a/pkg/apis/acid.zalan.do/v1/operator_configuration_type.go b/pkg/apis/acid.zalan.do/v1/operator_configuration_type.go index 5af67982e..20f89376a 100644 --- a/pkg/apis/acid.zalan.do/v1/operator_configuration_type.go +++ b/pkg/apis/acid.zalan.do/v1/operator_configuration_type.go @@ -85,6 +85,7 @@ type KubernetesMetaConfiguration struct { SpiloRunAsUser *int64 `json:"spilo_runasuser,omitempty"` SpiloRunAsGroup *int64 `json:"spilo_runasgroup,omitempty"` SpiloFSGroup *int64 `json:"spilo_fsgroup,omitempty"` + SpiloRunAsNonRoot *bool `json:"spilo_run_as_non_root,omitempty"` AdditionalPodCapabilities []string `json:"additional_pod_capabilities,omitempty"` WatchedNamespace string `json:"watched_namespace,omitempty"` // +kubebuilder:default="postgres-{cluster}-pdb" diff --git a/pkg/apis/acid.zalan.do/v1/operatorconfiguration.crd.yaml b/pkg/apis/acid.zalan.do/v1/operatorconfiguration.crd.yaml index 5f48d89ab..811dc2c28 100644 --- a/pkg/apis/acid.zalan.do/v1/operatorconfiguration.crd.yaml +++ b/pkg/apis/acid.zalan.do/v1/operatorconfiguration.crd.yaml @@ -498,6 +498,8 @@ spec: type: integer spilo_privileged: type: boolean + spilo_run_as_non_root: + type: boolean spilo_runasgroup: format: int64 type: integer diff --git a/pkg/apis/acid.zalan.do/v1/postgresql.crd.yaml b/pkg/apis/acid.zalan.do/v1/postgresql.crd.yaml index f3dcbab59..dd42406f8 100644 --- a/pkg/apis/acid.zalan.do/v1/postgresql.crd.yaml +++ b/pkg/apis/acid.zalan.do/v1/postgresql.crd.yaml @@ -2892,6 +2892,8 @@ spec: spiloRunAsGroup: format: int64 type: integer + spiloRunAsNonRoot: + type: boolean spiloRunAsUser: format: int64 type: integer diff --git a/pkg/apis/acid.zalan.do/v1/postgresql_type.go b/pkg/apis/acid.zalan.do/v1/postgresql_type.go index 2ea8c6ba8..bf267b730 100644 --- a/pkg/apis/acid.zalan.do/v1/postgresql_type.go +++ b/pkg/apis/acid.zalan.do/v1/postgresql_type.go @@ -53,9 +53,10 @@ type PostgresSpec struct { // deprecated field storing cluster name without teamId prefix ClusterName string `json:"-"` - SpiloRunAsUser *int64 `json:"spiloRunAsUser,omitempty"` - SpiloRunAsGroup *int64 `json:"spiloRunAsGroup,omitempty"` - SpiloFSGroup *int64 `json:"spiloFSGroup,omitempty"` + SpiloRunAsUser *int64 `json:"spiloRunAsUser,omitempty"` + SpiloRunAsGroup *int64 `json:"spiloRunAsGroup,omitempty"` + SpiloFSGroup *int64 `json:"spiloFSGroup,omitempty"` + SpiloRunAsNonRoot *bool `json:"spiloRunAsNonRoot,omitempty"` // vars that enable load balancers are pointers because it is important to know if any of them is omitted from the Postgres manifest // in that case the var evaluates to nil and the value is taken from the operator config diff --git a/pkg/apis/acid.zalan.do/v1/zz_generated.deepcopy.go b/pkg/apis/acid.zalan.do/v1/zz_generated.deepcopy.go index 9f5e93976..baaa20517 100644 --- a/pkg/apis/acid.zalan.do/v1/zz_generated.deepcopy.go +++ b/pkg/apis/acid.zalan.do/v1/zz_generated.deepcopy.go @@ -193,6 +193,11 @@ func (in *KubernetesMetaConfiguration) DeepCopyInto(out *KubernetesMetaConfigura *out = new(int64) **out = **in } + if in.SpiloRunAsNonRoot != nil { + in, out := &in.SpiloRunAsNonRoot, &out.SpiloRunAsNonRoot + *out = new(bool) + **out = **in + } if in.AdditionalPodCapabilities != nil { in, out := &in.AdditionalPodCapabilities, &out.AdditionalPodCapabilities *out = make([]string, len(*in)) @@ -788,6 +793,11 @@ func (in *PostgresSpec) DeepCopyInto(out *PostgresSpec) { *out = new(int64) **out = **in } + if in.SpiloRunAsNonRoot != nil { + in, out := &in.SpiloRunAsNonRoot, &out.SpiloRunAsNonRoot + *out = new(bool) + **out = **in + } if in.EnableMasterLoadBalancer != nil { in, out := &in.EnableMasterLoadBalancer, &out.EnableMasterLoadBalancer *out = new(bool) diff --git a/pkg/cluster/k8sres.go b/pkg/cluster/k8sres.go index 2353e464f..e8edc7e99 100644 --- a/pkg/cluster/k8sres.go +++ b/pkg/cluster/k8sres.go @@ -701,6 +701,7 @@ func generateContainer( volumeMounts []v1.VolumeMount, privilegedMode bool, privilegeEscalationMode *bool, + runAsNonRoot *bool, additionalPodCapabilities *v1.Capabilities, ) *v1.Container { return &v1.Container{ @@ -729,6 +730,7 @@ func generateContainer( AllowPrivilegeEscalation: privilegeEscalationMode, Privileged: &privilegedMode, ReadOnlyRootFilesystem: util.False(), + RunAsNonRoot: runAsNonRoot, Capabilities: additionalPodCapabilities, }, } @@ -847,9 +849,10 @@ func (c *Cluster) generatePodTemplate( containers := []v1.Container{*spiloContainer} containers = append(containers, sidecarContainers...) securityContext := v1.PodSecurityContext{ - RunAsUser: c.OpConfig.Resources.SpiloRunAsUser, - RunAsGroup: c.OpConfig.Resources.SpiloRunAsGroup, - FSGroup: c.OpConfig.Resources.SpiloFSGroup, + RunAsUser: c.OpConfig.Resources.SpiloRunAsUser, + RunAsGroup: c.OpConfig.Resources.SpiloRunAsGroup, + FSGroup: c.OpConfig.Resources.SpiloFSGroup, + RunAsNonRoot: c.OpConfig.Resources.SpiloRunAsNonRoot, } if c.Spec.SpiloRunAsUser != nil { @@ -864,6 +867,10 @@ func (c *Cluster) generatePodTemplate( securityContext.FSGroup = c.Spec.SpiloFSGroup } + if c.Spec.SpiloRunAsNonRoot != nil { + securityContext.RunAsNonRoot = c.Spec.SpiloRunAsNonRoot + } + podSpec := v1.PodSpec{ ServiceAccountName: podServiceAccountName, TerminationGracePeriodSeconds: &terminateGracePeriodSeconds, @@ -1395,6 +1402,7 @@ func (c *Cluster) generateStatefulSet(spec *acidv1.PostgresSpec) (*appsv1.Statef volumeMounts, c.OpConfig.Resources.SpiloPrivileged, c.OpConfig.Resources.SpiloAllowPrivilegeEscalation, + util.CoalesceBool(spec.SpiloRunAsNonRoot, c.OpConfig.Resources.SpiloRunAsNonRoot), generateCapabilities(c.OpConfig.AdditionalPodCapabilities), ) @@ -2410,6 +2418,7 @@ func (c *Cluster) generateLogicalBackupJob() (*batchv1.CronJob, error) { []v1.VolumeMount{}, c.OpConfig.SpiloPrivileged, // use same value as for normal DB pods c.OpConfig.SpiloAllowPrivilegeEscalation, + c.OpConfig.Resources.SpiloRunAsNonRoot, // use same value as for normal DB pods nil, ) diff --git a/pkg/cluster/k8sres_test.go b/pkg/cluster/k8sres_test.go index 3200dc2e7..000dd2765 100644 --- a/pkg/cluster/k8sres_test.go +++ b/pkg/cluster/k8sres_test.go @@ -1824,6 +1824,82 @@ func TestShmVolume(t *testing.T) { } } +func TestSpiloRunAsNonRoot(t *testing.T) { + superUserName := "superuser" + replicationUserName := "replicationuser" + + spiloRunAsUser := int64(101) + spiloRunAsGroup := int64(103) + spiloFSGroup := int64(103) + + createCluster := func(manifestRunAsNonRoot, optionRunAsNonRoot *bool) *Cluster { + pg := acidv1.Postgresql{ + ObjectMeta: metav1.ObjectMeta{ + Name: clusterName, + Namespace: namespace, + }, + Spec: acidv1.PostgresSpec{ + TeamID: "myapp", + NumberOfInstances: 1, + Volume: acidv1.Volume{ + Size: "5Gi", + }, + SpiloRunAsNonRoot: manifestRunAsNonRoot, + }, + } + + client, _ := newFakeK8sTestClient() + + return New( + Config{ + OpConfig: config.Config{ + PodManagementPolicy: "ordered_ready", + ProtectedRoles: []string{"admin"}, + Auth: config.Auth{ + SuperUsername: superUserName, + ReplicationUsername: replicationUserName, + }, + Resources: config.Resources{ + SpiloRunAsUser: &spiloRunAsUser, + SpiloRunAsGroup: &spiloRunAsGroup, + SpiloFSGroup: &spiloFSGroup, + SpiloRunAsNonRoot: optionRunAsNonRoot, + }, + }, + }, client, pg, logger, eventRecorder) + } + + t.Run("unset leaves runAsNonRoot out of the pod spec", func(t *testing.T) { + cluster := createCluster(nil, nil) + sts, err := cluster.createStatefulSet() + assert.NoError(t, err) + + assert.Nil(t, sts.Spec.Template.Spec.SecurityContext.RunAsNonRoot) + postgresContainer := getPostgresContainer(&sts.Spec.Template.Spec) + assert.Nil(t, postgresContainer.SecurityContext.RunAsNonRoot) + }) + + t.Run("operator option sets runAsNonRoot on pod and container level", func(t *testing.T) { + cluster := createCluster(nil, util.True()) + sts, err := cluster.createStatefulSet() + assert.NoError(t, err) + + assert.True(t, *sts.Spec.Template.Spec.SecurityContext.RunAsNonRoot) + postgresContainer := getPostgresContainer(&sts.Spec.Template.Spec) + assert.True(t, *postgresContainer.SecurityContext.RunAsNonRoot) + }) + + t.Run("cluster manifest overrides the operator option", func(t *testing.T) { + cluster := createCluster(util.False(), util.True()) + sts, err := cluster.createStatefulSet() + assert.NoError(t, err) + + assert.False(t, *sts.Spec.Template.Spec.SecurityContext.RunAsNonRoot) + postgresContainer := getPostgresContainer(&sts.Spec.Template.Spec) + assert.False(t, *postgresContainer.SecurityContext.RunAsNonRoot) + }) +} + func TestSecretVolume(t *testing.T) { tests := []struct { subTest string @@ -4660,6 +4736,7 @@ func TestGenerateContainerWithEnvFrom(t *testing.T) { false, util.False(), nil, + nil, ) if !reflect.DeepEqual(container.Env, envVars) { diff --git a/pkg/util/config/config.go b/pkg/util/config/config.go index 718b5ba23..7f98bc975 100644 --- a/pkg/util/config/config.go +++ b/pkg/util/config/config.go @@ -38,6 +38,7 @@ type Resources struct { ClusterDomain string `name:"cluster_domain" default:"cluster.local"` SpiloPrivileged bool `name:"spilo_privileged" default:"false"` SpiloAllowPrivilegeEscalation *bool `name:"spilo_allow_privilege_escalation" default:"true"` + SpiloRunAsNonRoot *bool `name:"spilo_run_as_non_root"` AdditionalPodCapabilities []string `name:"additional_pod_capabilities" default:""` ClusterLabels map[string]string `name:"cluster_labels" default:"application:spilo"` InheritedLabels []string `name:"inherited_labels" default:""`