diff --git a/charts/postgres-operator/crds/operatorconfigurations.yaml b/charts/postgres-operator/crds/operatorconfigurations.yaml index c6da405a5..245d61e48 100644 --- a/charts/postgres-operator/crds/operatorconfigurations.yaml +++ b/charts/postgres-operator/crds/operatorconfigurations.yaml @@ -354,6 +354,15 @@ spec: type: integer spilo_fsgroup: type: integer + spilo_runasnonroot: + type: boolean + spilo_seccompprofile: + type: object + properties: + localhostProfile: + type: string + type: + type: string spilo_privileged: type: boolean default: false diff --git a/charts/postgres-operator/crds/postgresqls.yaml b/charts/postgres-operator/crds/postgresqls.yaml index 8083e5e1d..31a17ef46 100644 --- a/charts/postgres-operator/crds/postgresqls.yaml +++ b/charts/postgres-operator/crds/postgresqls.yaml @@ -483,6 +483,15 @@ spec: type: integer spiloFSGroup: type: integer + spiloRunAsNonRoot: + type: boolean + spiloSeccompProfile: + type: object + properties: + localhostProfile: + type: string + type: + type: string standby: type: object properties: diff --git a/charts/postgres-operator/values.yaml b/charts/postgres-operator/values.yaml index d66aa5608..d19486155 100644 --- a/charts/postgres-operator/values.yaml +++ b/charts/postgres-operator/values.yaml @@ -216,6 +216,14 @@ configKubernetes: # group ID with write-access to volumes (required to run Spilo as non-root process) # spilo_fsgroup: 103 + # sets runAsNonRoot in the security context. If this is set you also must set spilo_runasuser. + # spilo_runasnonroot: true + + # sets seccompProfile in the security context + # spilo_seccompprofile: + # type: Localhost + # localhostProfile: profiles/audit.json + # whether the Spilo container should run in privileged mode spilo_privileged: false # whether the Spilo container should run with additional permissions other than parent. diff --git a/docs/reference/cluster_manifest.md b/docs/reference/cluster_manifest.md index ab0353202..ac4b91079 100644 --- a/docs/reference/cluster_manifest.md +++ b/docs/reference/cluster_manifest.md @@ -85,6 +85,13 @@ 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** + boolean flag to set `runAsNonRoot` in the pod security context. If this is set + then `spiloRunAsUser` must also be set. Optional. + +* **spiloSeccompProfile** + sets the `seccompProfile` in the pod security context. Optional. + * **enableMasterLoadBalancer** boolean flag to override the operator defaults (set by the `enable_master_load_balancer` parameter) to define whether to enable the load diff --git a/docs/reference/operator_parameters.md b/docs/reference/operator_parameters.md index 7e7cbeaf0..d0006bc4c 100644 --- a/docs/reference/operator_parameters.md +++ b/docs/reference/operator_parameters.md @@ -509,6 +509,13 @@ 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_runasnonroot** + boolean flag to set `runAsNonRoot` in the Spilo pod security context. If this + is set then `spilo_runasuser` must also be set. Optional. + +* **spilo_seccompprofile** + sets the `seccompProfile` in the Spilo pod security context. Optional. + * **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/postgresql-operator-default-configuration.yaml b/manifests/postgresql-operator-default-configuration.yaml index 389d9325a..153811ca7 100644 --- a/manifests/postgresql-operator-default-configuration.yaml +++ b/manifests/postgresql-operator-default-configuration.yaml @@ -109,6 +109,10 @@ configuration: # spilo_runasuser: 101 # spilo_runasgroup: 103 # spilo_fsgroup: 103 + # spilo_runasnonroot: true + # spilo_seccompprofile: + # type: Localhost + # localhostProfile: profiles/audit.json spilo_privileged: false storage_resize_mode: pvc # toleration: diff --git a/manifests/postgresql.crd.yaml b/manifests/postgresql.crd.yaml index 39d751cef..a53b91c1a 100644 --- a/manifests/postgresql.crd.yaml +++ b/manifests/postgresql.crd.yaml @@ -481,6 +481,15 @@ spec: type: integer spiloFSGroup: type: integer + spiloRunAsNonRoot: + type: boolean + spiloSeccompProfile: + type: object + properties: + localhostProfile: + type: string + type: + type: string standby: type: object properties: diff --git a/pkg/apis/acid.zalan.do/v1/crds.go b/pkg/apis/acid.zalan.do/v1/crds.go index 3f6bf25d9..8c7a1ab6d 100644 --- a/pkg/apis/acid.zalan.do/v1/crds.go +++ b/pkg/apis/acid.zalan.do/v1/crds.go @@ -751,6 +751,20 @@ var PostgresCRDResourceValidation = apiextv1.CustomResourceValidation{ "spiloFSGroup": { Type: "integer", }, + "spiloRunAsNonRoot": { + Type: "boolean", + }, + "spiloSeccompProfile": { + Type: "object", + Properties: map[string]apiextv1.JSONSchemaProps{ + "localhostProfile": { + Type: "string", + }, + "type": { + Type: "string", + }, + }, + }, "standby": { Type: "object", Properties: map[string]apiextv1.JSONSchemaProps{ @@ -1525,6 +1539,20 @@ var OperatorConfigCRDResourceValidation = apiextv1.CustomResourceValidation{ "spilo_fsgroup": { Type: "integer", }, + "spilo_runasnonroot": { + Type: "boolean", + }, + "spilo_seccompprofile": { + Type: "object", + Properties: map[string]apiextv1.JSONSchemaProps{ + "localhostProfile": { + Type: "string", + }, + "type": { + Type: "string", + }, + }, + }, "spilo_privileged": { Type: "boolean", }, 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 cd11b9173..08899083f 100644 --- a/pkg/apis/acid.zalan.do/v1/operator_configuration_type.go +++ b/pkg/apis/acid.zalan.do/v1/operator_configuration_type.go @@ -66,6 +66,8 @@ 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_runasnonroot,omitempty"` + SpiloSeccompProfile *v1.SeccompProfile `json:"spilo_seccompprofile,omitempty"` AdditionalPodCapabilities []string `json:"additional_pod_capabilities,omitempty"` WatchedNamespace string `json:"watched_namespace,omitempty"` PDBNameFormat config.StringTemplate `json:"pdb_name_format,omitempty"` diff --git a/pkg/apis/acid.zalan.do/v1/postgresql_type.go b/pkg/apis/acid.zalan.do/v1/postgresql_type.go index ef6dfe7ff..c03b5c455 100644 --- a/pkg/apis/acid.zalan.do/v1/postgresql_type.go +++ b/pkg/apis/acid.zalan.do/v1/postgresql_type.go @@ -43,6 +43,9 @@ type PostgresSpec struct { SpiloRunAsGroup *int64 `json:"spiloRunAsGroup,omitempty"` SpiloFSGroup *int64 `json:"spiloFSGroup,omitempty"` + SpiloRunAsNonRoot *bool `json:"spiloRunAsNonRoot,omitempty"` + SpiloSeccompProfile *v1.SeccompProfile `json:"spiloSeccompProfile,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 EnableMasterLoadBalancer *bool `json:"enableMasterLoadBalancer,omitempty"` 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 5d0a5b341..245dfd04c 100644 --- a/pkg/apis/acid.zalan.do/v1/zz_generated.deepcopy.go +++ b/pkg/apis/acid.zalan.do/v1/zz_generated.deepcopy.go @@ -183,6 +183,16 @@ 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.SpiloSeccompProfile != nil { + in, out := &in.SpiloSeccompProfile, &out.SpiloSeccompProfile + *out = new(corev1.SeccompProfile) + (*in).DeepCopyInto(*out) + } if in.AdditionalPodCapabilities != nil { in, out := &in.AdditionalPodCapabilities, &out.AdditionalPodCapabilities *out = make([]string, len(*in)) @@ -688,6 +698,16 @@ 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.SpiloSeccompProfile != nil { + in, out := &in.SpiloSeccompProfile, &out.SpiloSeccompProfile + *out = new(corev1.SeccompProfile) + (*in).DeepCopyInto(*out) + } 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 e05a54553..52978c7cc 100644 --- a/pkg/cluster/k8sres.go +++ b/pkg/cluster/k8sres.go @@ -813,6 +813,8 @@ func (c *Cluster) generatePodTemplate( spiloRunAsUser *int64, spiloRunAsGroup *int64, spiloFSGroup *int64, + spiloRunAsNonRoot *bool, + spiloSeccompProfile *v1.SeccompProfile, nodeAffinity *v1.Affinity, schedulerName *string, terminateGracePeriod int64, @@ -845,6 +847,14 @@ func (c *Cluster) generatePodTemplate( securityContext.FSGroup = spiloFSGroup } + if spiloRunAsNonRoot != nil { + securityContext.RunAsNonRoot = spiloRunAsNonRoot + } + + if spiloSeccompProfile != nil { + securityContext.SeccompProfile = spiloSeccompProfile + } + podSpec := v1.PodSpec{ ServiceAccountName: podServiceAccountName, TerminationGracePeriodSeconds: &terminateGracePeriodSeconds, @@ -1357,6 +1367,16 @@ func (c *Cluster) generateStatefulSet(spec *acidv1.PostgresSpec) (*appsv1.Statef effectiveFSGroup = spec.SpiloFSGroup } + effectiveRunAsNonRoot := c.OpConfig.Resources.SpiloRunAsNonRoot + if spec.SpiloRunAsNonRoot != nil { + effectiveRunAsNonRoot = spec.SpiloRunAsNonRoot + } + + effectiveSeccompProfile := c.OpConfig.Resources.SpiloSeccompProfile + if spec.SpiloSeccompProfile != nil { + effectiveSeccompProfile = spec.SpiloSeccompProfile + } + volumeMounts := generateVolumeMounts(spec.Volume) // configure TLS with a custom secret volume @@ -1473,6 +1493,8 @@ func (c *Cluster) generateStatefulSet(spec *acidv1.PostgresSpec) (*appsv1.Statef effectiveRunAsUser, effectiveRunAsGroup, effectiveFSGroup, + effectiveRunAsNonRoot, + effectiveSeccompProfile, c.nodeAffinity(c.OpConfig.NodeReadinessLabel, spec.NodeAffinity), spec.SchedulerName, int64(c.OpConfig.PodTerminateGracePeriod.Seconds()), @@ -2361,6 +2383,8 @@ func (c *Cluster) generateLogicalBackupJob() (*batchv1.CronJob, error) { nil, nil, nil, + nil, + nil, c.nodeAffinity(c.OpConfig.NodeReadinessLabel, nil), nil, int64(c.OpConfig.PodTerminateGracePeriod.Seconds()), diff --git a/pkg/cluster/k8sres_test.go b/pkg/cluster/k8sres_test.go index 137c24081..e22ae99ad 100644 --- a/pkg/cluster/k8sres_test.go +++ b/pkg/cluster/k8sres_test.go @@ -3984,3 +3984,123 @@ func TestGenerateCapabilities(t *testing.T) { } } } + +func TestGenerateSeccompProfile(t *testing.T) { + mockClient, _ := newFakeK8sTestClient() + + spiloSeccompProfile := v1.SeccompProfile{ + Type: "Localhost", + LocalhostProfile: k8sutil.StringToPointer("profiles/audit.json"), + } + + postgresql := acidv1.Postgresql{ + ObjectMeta: metav1.ObjectMeta{ + Name: "acid-test-cluster", + Namespace: "default", + }, + Spec: acidv1.PostgresSpec{ + TeamID: "myapp", + NumberOfInstances: 1, + Resources: &acidv1.Resources{ + ResourceRequests: acidv1.ResourceDescription{CPU: k8sutil.StringToPointer("1"), Memory: k8sutil.StringToPointer("10")}, + ResourceLimits: acidv1.ResourceDescription{CPU: k8sutil.StringToPointer("1"), Memory: k8sutil.StringToPointer("10")}, + }, + Volume: acidv1.Volume{ + Size: "1G", + }, + }, + } + + testCluster := New( + Config{ + OpConfig: config.Config{ + PodManagementPolicy: "ordered_ready", + Resources: config.Resources{ + SpiloSeccompProfile: &spiloSeccompProfile, + }, + }, + }, mockClient, postgresql, logger, eventRecorder) + + // create a statefulset + sts, err := testCluster.createStatefulSet() + assert.NoError(t, err) + + assert.Equal(t, spiloSeccompProfile.Type, sts.Spec.Template.Spec.SecurityContext.SeccompProfile.Type, "SeccompProfile.Type matches") + assert.Equal(t, *spiloSeccompProfile.LocalhostProfile, *sts.Spec.Template.Spec.SecurityContext.SeccompProfile.LocalhostProfile, "SeccompProfile.LocalhostProfile matches") +} + +func TestGenerateEmptySeccompProfile(t *testing.T) { + mockClient, _ := newFakeK8sTestClient() + + postgresql := acidv1.Postgresql{ + ObjectMeta: metav1.ObjectMeta{ + Name: "acid-test-cluster", + Namespace: "default", + }, + Spec: acidv1.PostgresSpec{ + TeamID: "myapp", + NumberOfInstances: 1, + Resources: &acidv1.Resources{ + ResourceRequests: acidv1.ResourceDescription{CPU: k8sutil.StringToPointer("1"), Memory: k8sutil.StringToPointer("10")}, + ResourceLimits: acidv1.ResourceDescription{CPU: k8sutil.StringToPointer("1"), Memory: k8sutil.StringToPointer("10")}, + }, + Volume: acidv1.Volume{ + Size: "1G", + }, + }, + } + + testCluster := New( + Config{ + OpConfig: config.Config{ + PodManagementPolicy: "ordered_ready", + Resources: config.Resources{}, + }, + }, mockClient, postgresql, logger, eventRecorder) + + // create a statefulset + sts, err := testCluster.createStatefulSet() + assert.NoError(t, err) + + assert.Nil(t, sts.Spec.Template.Spec.SecurityContext.SeccompProfile, "SeccompProfile not set") +} + +func TestGenerateRunAsNonRoot(t *testing.T) { + mockClient, _ := newFakeK8sTestClient() + + postgresql := acidv1.Postgresql{ + ObjectMeta: metav1.ObjectMeta{ + Name: "acid-test-cluster", + Namespace: "default", + }, + Spec: acidv1.PostgresSpec{ + TeamID: "myapp", + NumberOfInstances: 1, + Resources: &acidv1.Resources{ + ResourceRequests: acidv1.ResourceDescription{CPU: k8sutil.StringToPointer("1"), Memory: k8sutil.StringToPointer("10")}, + ResourceLimits: acidv1.ResourceDescription{CPU: k8sutil.StringToPointer("1"), Memory: k8sutil.StringToPointer("10")}, + }, + Volume: acidv1.Volume{ + Size: "1G", + }, + }, + } + + runAsNonRoot := true + + testCluster := New( + Config{ + OpConfig: config.Config{ + PodManagementPolicy: "ordered_ready", + Resources: config.Resources{ + SpiloRunAsNonRoot: &runAsNonRoot, + }, + }, + }, mockClient, postgresql, logger, eventRecorder) + + // create a statefulset + sts, err := testCluster.createStatefulSet() + assert.NoError(t, err) + + assert.Equal(t, true, *sts.Spec.Template.Spec.SecurityContext.RunAsNonRoot, "RunAsNonRoot set") +} diff --git a/pkg/controller/operator_config.go b/pkg/controller/operator_config.go index 5cf1d7e40..766390f45 100644 --- a/pkg/controller/operator_config.go +++ b/pkg/controller/operator_config.go @@ -79,6 +79,8 @@ func (c *Controller) importConfigurationFromCRD(fromCRD *acidv1.OperatorConfigur result.SpiloRunAsUser = fromCRD.Kubernetes.SpiloRunAsUser result.SpiloRunAsGroup = fromCRD.Kubernetes.SpiloRunAsGroup result.SpiloFSGroup = fromCRD.Kubernetes.SpiloFSGroup + result.SpiloRunAsNonRoot = fromCRD.Kubernetes.SpiloRunAsNonRoot + result.SpiloSeccompProfile = fromCRD.Kubernetes.SpiloSeccompProfile result.AdditionalPodCapabilities = fromCRD.Kubernetes.AdditionalPodCapabilities result.ClusterDomain = util.Coalesce(fromCRD.Kubernetes.ClusterDomain, "cluster.local") result.WatchedNamespace = fromCRD.Kubernetes.WatchedNamespace diff --git a/pkg/util/config/config.go b/pkg/util/config/config.go index 9fadd6a5b..1328eb348 100644 --- a/pkg/util/config/config.go +++ b/pkg/util/config/config.go @@ -34,6 +34,8 @@ type Resources struct { SpiloRunAsUser *int64 `name:"spilo_runasuser"` SpiloRunAsGroup *int64 `name:"spilo_runasgroup"` SpiloFSGroup *int64 `name:"spilo_fsgroup"` + SpiloRunAsNonRoot *bool `name:"spilo_runasnonroot"` + SpiloSeccompProfile *v1.SeccompProfile `name:"spilo_seccompprofile"` PodPriorityClassName string `name:"pod_priority_class_name"` ClusterDomain string `name:"cluster_domain" default:"cluster.local"` SpiloPrivileged bool `name:"spilo_privileged" default:"false"`