diff --git a/Makefile b/Makefile index e6bf4d88f..6f5d0c883 100644 --- a/Makefile +++ b/Makefile @@ -76,6 +76,7 @@ $(GENERATED_CRDS): $(GENERATED) @$(SED) -i -e 's/listKind: PostgresqlList/listKind: postgresqlList/' manifests/postgresql.crd.yaml @hack/adjust_postgresql_crd.sh @mv manifests/acid.zalan.do_operatorconfigurations.yaml manifests/operatorconfiguration.crd.yaml + @hack/adjust_operatorconfiguration_crd.sh @mv manifests/acid.zalan.do_postgresteams.yaml manifests/postgresteam.crd.yaml @cp manifests/postgresql.crd.yaml pkg/apis/acid.zalan.do/v1/postgresql.crd.yaml @cp manifests/postgresql.crd.yaml charts/postgres-operator/crds/postgresqls.yaml diff --git a/charts/postgres-operator/crds/operatorconfigurations.yaml b/charts/postgres-operator/crds/operatorconfigurations.yaml index ebe4e7089..5bc0b5dab 100644 --- a/charts/postgres-operator/crds/operatorconfigurations.yaml +++ b/charts/postgres-operator/crds/operatorconfigurations.yaml @@ -119,9 +119,15 @@ spec: format: int32 minimum: 1 type: integer + connection_pooler_pod_security_context: + type: object + x-kubernetes-preserve-unknown-fields: true connection_pooler_schema: default: pooler type: string + connection_pooler_security_context: + type: object + x-kubernetes-preserve-unknown-fields: true connection_pooler_user: default: pooler type: string @@ -707,8 +713,7 @@ spec: type: object maintenance_windows: items: - pattern: ^\ *((Mon|Tue|Wed|Thu|Fri|Sat|Sun):(2[0-3]|[01]?\d):([0-5]?\d)|(2[0-3]|[01]?\d):([0-5]?\d))-((2[0-3]|[01]?\d):([0-5]?\d)|(2[0-3]|[01]?\d):([0-5]?\d))\ - *$ + pattern: '^\ *((Mon|Tue|Wed|Thu|Fri|Sat|Sun):(2[0-3]|[01]?\d):([0-5]?\d)|(2[0-3]|[01]?\d):([0-5]?\d))-((2[0-3]|[01]?\d):([0-5]?\d)|(2[0-3]|[01]?\d):([0-5]?\d))\ *$' type: string type: array major_version_upgrade: diff --git a/charts/postgres-operator/values.yaml b/charts/postgres-operator/values.yaml index 4ea5b6319..f3e3f6f92 100644 --- a/charts/postgres-operator/values.yaml +++ b/charts/postgres-operator/values.yaml @@ -479,6 +479,24 @@ configConnectionPooler: connection_pooler_default_memory_request: 100Mi connection_pooler_default_cpu_limit: "1" connection_pooler_default_memory_limit: 100Mi + # Override the pooler pod- and container-level securityContext. Useful for hardened + # images whose pgbouncer user/UID differs from the operator default (100/101) and which + # own /etc/pgbouncer, or to satisfy restricted Pod Security Standards. When unset the + # operator keeps its defaults (pod RunAsUser/RunAsGroup 100/101, container + # allowPrivilegeEscalation=false). + # connection_pooler_pod_security_context: + # runAsUser: 100 + # runAsGroup: 101 + # runAsNonRoot: true + # fsGroup: 101 + # seccompProfile: + # type: RuntimeDefault + # connection_pooler_security_context: + # allowPrivilegeEscalation: false + # readOnlyRootFilesystem: true + # capabilities: + # drop: + # - ALL configPatroni: # enable Patroni DCS failsafe_mode feature diff --git a/docs/reference/operator_parameters.md b/docs/reference/operator_parameters.md index 02c6db8d0..f30dedac3 100644 --- a/docs/reference/operator_parameters.md +++ b/docs/reference/operator_parameters.md @@ -1105,3 +1105,48 @@ operator being able to provide some reasonable defaults. **connection_pooler_default_cpu_limit** **connection_pooler_default_memory_limit** Default resource configuration for connection pooler deployment. + +* **connection_pooler_pod_security_context** + Pod-level `securityContext` applied to the connection pooler deployment. The + value is a standard Kubernetes + [PodSecurityContext](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.31/#podsecuritycontext-v1-core) + object. Only fields you set are overridden; unset fields keep the operator + defaults (`runAsUser: 100`, `runAsGroup: 101`, and the cluster's `fsGroup`). + Settable **only** via the `OperatorConfiguration` CRD (not the ConfigMap). + Defaults to not set. + +* **connection_pooler_security_context** + Container-level `securityContext` applied to the pgbouncer container. The + value is a standard Kubernetes + [SecurityContext](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.31/#securitycontext-v1-core) + object. Only fields you set are overridden; when `allowPrivilegeEscalation` + is unset it defaults to `false`. Settable **only** via the + `OperatorConfiguration` CRD (not the ConfigMap). Defaults to not set. + +These two parameters are useful when running a hardened pooler image whose +pgbouncer user/UID differs from the operator default (100/101), or to satisfy +restricted [Pod Security Standards](https://kubernetes.io/docs/concepts/security/pod-security-standards/). +Because they are object-valued they cannot be expressed in the flat operator +ConfigMap; configure them through the `OperatorConfiguration` CRD: + +```yaml +apiVersion: "acid.zalan.do/v1" +kind: OperatorConfiguration +metadata: + name: postgresql-operator-default-configuration +configuration: + connection_pooler: + connection_pooler_pod_security_context: + runAsUser: 100 + runAsGroup: 101 + runAsNonRoot: true + fsGroup: 101 + seccompProfile: + type: RuntimeDefault + connection_pooler_security_context: + allowPrivilegeEscalation: false + readOnlyRootFilesystem: true + capabilities: + drop: + - ALL +``` diff --git a/hack/adjust_operatorconfiguration_crd.sh b/hack/adjust_operatorconfiguration_crd.sh new file mode 100755 index 000000000..61fb4ac03 --- /dev/null +++ b/hack/adjust_operatorconfiguration_crd.sh @@ -0,0 +1,22 @@ +#!/usr/bin/env bash + +# Hack to adjust the generated operatorconfiguration CRD YAML file and add +# missing field settings which can not be expressed via kubebuilder markers. +# +# Injections: +# +# * type: string and pattern for the maintenance_windows items. +# The MaintenanceWindow type marshals to/from a string (see marshal.go) but +# the field is declared `+kubebuilder:validation:Schemaless` / +# `+kubebuilder:validation:Type=array`, so controller-gen emits a bare +# `type: array` without the required `items` schema. A structural CRD must +# specify `items` for every array, so the generated CRD is rejected with: +# spec.validation.openAPIV3Schema.properties[configuration] +# .properties[maintenance_windows].items: Required value: must be specified + +file="${1:-"manifests/operatorconfiguration.crd.yaml"}" + +sed -i '/^[[:space:]]*maintenance_windows:$/{ + # Capture the indentation + s/^\([[:space:]]*\)maintenance_windows:$/\1maintenance_windows:\n\1 items:\n\1 pattern: '\''^\\ *((Mon|Tue|Wed|Thu|Fri|Sat|Sun):(2[0-3]|[01]?\\d):([0-5]?\\d)|(2[0-3]|[01]?\\d):([0-5]?\\d))-((2[0-3]|[01]?\\d):([0-5]?\\d)|(2[0-3]|[01]?\\d):([0-5]?\\d))\\ *$'\''\n\1 type: string/ +}' "$file" diff --git a/manifests/operatorconfiguration.crd.yaml b/manifests/operatorconfiguration.crd.yaml index ebe4e7089..bdb6cc1eb 100644 --- a/manifests/operatorconfiguration.crd.yaml +++ b/manifests/operatorconfiguration.crd.yaml @@ -707,8 +707,7 @@ spec: type: object maintenance_windows: items: - pattern: ^\ *((Mon|Tue|Wed|Thu|Fri|Sat|Sun):(2[0-3]|[01]?\d):([0-5]?\d)|(2[0-3]|[01]?\d):([0-5]?\d))-((2[0-3]|[01]?\d):([0-5]?\d)|(2[0-3]|[01]?\d):([0-5]?\d))\ - *$ + pattern: '^\ *((Mon|Tue|Wed|Thu|Fri|Sat|Sun):(2[0-3]|[01]?\d):([0-5]?\d)|(2[0-3]|[01]?\d):([0-5]?\d))-((2[0-3]|[01]?\d):([0-5]?\d)|(2[0-3]|[01]?\d):([0-5]?\d))\ *$' type: string type: array major_version_upgrade: 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 046dd0761..e835dbf74 100644 --- a/pkg/apis/acid.zalan.do/v1/operator_configuration_type.go +++ b/pkg/apis/acid.zalan.do/v1/operator_configuration_type.go @@ -347,6 +347,14 @@ type ConnectionPoolerConfiguration struct { DefaultCPULimit string `json:"connection_pooler_default_cpu_limit,omitempty"` // +kubebuilder:validation:Pattern=`^(\d+(e\d+)?|\d+(\.\d+)?(e\d+)?[EPTGMK]i?)$` DefaultMemoryLimit string `json:"connection_pooler_default_memory_limit,omitempty"` + // +kubebuilder:validation:XPreserveUnknownFields + // +kubebuilder:validation:Type=object + // +kubebuilder:validation:Schemaless + PodSecurityContext *v1.PodSecurityContext `json:"connection_pooler_pod_security_context,omitempty"` + // +kubebuilder:validation:XPreserveUnknownFields + // +kubebuilder:validation:Type=object + // +kubebuilder:validation:Schemaless + SecurityContext *v1.SecurityContext `json:"connection_pooler_security_context,omitempty"` } // OperatorLogicalBackupConfiguration defines configuration for logical backup diff --git a/pkg/apis/acid.zalan.do/v1/operatorconfiguration.crd.yaml b/pkg/apis/acid.zalan.do/v1/operatorconfiguration.crd.yaml index ebe4e7089..bdb6cc1eb 100644 --- a/pkg/apis/acid.zalan.do/v1/operatorconfiguration.crd.yaml +++ b/pkg/apis/acid.zalan.do/v1/operatorconfiguration.crd.yaml @@ -707,8 +707,7 @@ spec: type: object maintenance_windows: items: - pattern: ^\ *((Mon|Tue|Wed|Thu|Fri|Sat|Sun):(2[0-3]|[01]?\d):([0-5]?\d)|(2[0-3]|[01]?\d):([0-5]?\d))-((2[0-3]|[01]?\d):([0-5]?\d)|(2[0-3]|[01]?\d):([0-5]?\d))\ - *$ + pattern: '^\ *((Mon|Tue|Wed|Thu|Fri|Sat|Sun):(2[0-3]|[01]?\d):([0-5]?\d)|(2[0-3]|[01]?\d):([0-5]?\d))-((2[0-3]|[01]?\d):([0-5]?\d)|(2[0-3]|[01]?\d):([0-5]?\d))\ *$' type: string type: array major_version_upgrade: 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 4601cf668..571f0eeb7 100644 --- a/pkg/apis/acid.zalan.do/v1/zz_generated.deepcopy.go +++ b/pkg/apis/acid.zalan.do/v1/zz_generated.deepcopy.go @@ -142,6 +142,16 @@ func (in *ConnectionPoolerConfiguration) DeepCopyInto(out *ConnectionPoolerConfi *out = new(int32) **out = **in } + if in.PodSecurityContext != nil { + in, out := &in.PodSecurityContext, &out.PodSecurityContext + *out = new(corev1.PodSecurityContext) + (*in).DeepCopyInto(*out) + } + if in.SecurityContext != nil { + in, out := &in.SecurityContext, &out.SecurityContext + *out = new(corev1.SecurityContext) + (*in).DeepCopyInto(*out) + } return } diff --git a/pkg/cluster/connection_pooler.go b/pkg/cluster/connection_pooler.go index 61cd9b041..ff4e5c5d2 100644 --- a/pkg/cluster/connection_pooler.go +++ b/pkg/cluster/connection_pooler.go @@ -29,6 +29,51 @@ import ( var poolerRunAsUser = int64(100) var poolerRunAsGroup = int64(101) +// generateConnectionPoolerPodSecurityContext returns the pod-level securityContext for the +// connection pooler. When the operator configuration provides one it is used as the base +// (deep-copied); otherwise an empty context is used. The historical defaults (RunAsUser/RunAsGroup +// 100/101) and the spilo FSGroup are applied only for fields the configuration leaves unset, so +// behavior is unchanged when no override is configured. +func (c *Cluster) generateConnectionPoolerPodSecurityContext(spec *acidv1.PostgresSpec) *v1.PodSecurityContext { + var securityContext v1.PodSecurityContext + if c.OpConfig.ConnectionPooler.PodSecurityContext != nil { + securityContext = *c.OpConfig.ConnectionPooler.PodSecurityContext.DeepCopy() + } + + if securityContext.RunAsUser == nil { + securityContext.RunAsUser = &poolerRunAsUser + } + if securityContext.RunAsGroup == nil { + securityContext.RunAsGroup = &poolerRunAsGroup + } + + if securityContext.FSGroup == nil { + effectiveFSGroup := c.OpConfig.Resources.SpiloFSGroup + if spec.SpiloFSGroup != nil { + effectiveFSGroup = spec.SpiloFSGroup + } + if effectiveFSGroup != nil { + securityContext.FSGroup = effectiveFSGroup + } + } + + return &securityContext +} + +// generateConnectionPoolerContainerSecurityContext returns the container-level securityContext for +// the pooler. A configured context is used as the base (deep-copied); AllowPrivilegeEscalation +// defaults to false when left unset, preserving prior behavior. +func (c *Cluster) generateConnectionPoolerContainerSecurityContext() *v1.SecurityContext { + var securityContext v1.SecurityContext + if c.OpConfig.ConnectionPooler.SecurityContext != nil { + securityContext = *c.OpConfig.ConnectionPooler.SecurityContext.DeepCopy() + } + if securityContext.AllowPrivilegeEscalation == nil { + securityContext.AllowPrivilegeEscalation = util.False() + } + return &securityContext +} + // ConnectionPoolerObjects K8s objects that are belong to connection pooler type ConnectionPoolerObjects struct { AuthSecret *v1.Secret @@ -383,9 +428,7 @@ func (c *Cluster) generateConnectionPoolerPodTemplate(role PostgresRole) ( }, }, }, - SecurityContext: &v1.SecurityContext{ - AllowPrivilegeEscalation: util.False(), - }, + SecurityContext: c.generateConnectionPoolerContainerSecurityContext(), } var poolerVolumes []v1.Volume @@ -444,19 +487,7 @@ func (c *Cluster) generateConnectionPoolerPodTemplate(role PostgresRole) ( poolerContainer.Env = envVars poolerContainer.VolumeMounts = volumeMounts tolerationsSpec := tolerations(&spec.Tolerations, c.OpConfig.PodToleration) - securityContext := v1.PodSecurityContext{} - - // determine the User, Group and FSGroup for the pooler pod - securityContext.RunAsUser = &poolerRunAsUser - securityContext.RunAsGroup = &poolerRunAsGroup - - effectiveFSGroup := c.OpConfig.Resources.SpiloFSGroup - if spec.SpiloFSGroup != nil { - effectiveFSGroup = spec.SpiloFSGroup - } - if effectiveFSGroup != nil { - securityContext.FSGroup = effectiveFSGroup - } + securityContext := c.generateConnectionPoolerPodSecurityContext(spec) podTemplate := &v1.PodTemplateSpec{ ObjectMeta: metav1.ObjectMeta{ @@ -469,7 +500,7 @@ func (c *Cluster) generateConnectionPoolerPodTemplate(role PostgresRole) ( Containers: []v1.Container{poolerContainer}, Tolerations: tolerationsSpec, Volumes: poolerVolumes, - SecurityContext: &securityContext, + SecurityContext: securityContext, ServiceAccountName: c.OpConfig.PodServiceAccountName, }, } diff --git a/pkg/cluster/connection_pooler_test.go b/pkg/cluster/connection_pooler_test.go index 1b41cbb02..9caf5aa60 100644 --- a/pkg/cluster/connection_pooler_test.go +++ b/pkg/cluster/connection_pooler_test.go @@ -1064,6 +1064,137 @@ func TestPoolerTLS(t *testing.T) { assert.Contains(t, poolerContainer.Env, v1.EnvVar{Name: "CONNECTION_POOLER_CLIENT_CA_FILE", Value: "/tls/ca.crt"}) } +// poolerSecurityContextOpConfig builds a minimal OpConfig usable for exercising the +// connection pooler securityContext behavior. Callers tweak the returned config's +// ConnectionPooler / Resources before passing it to syncedPoolerDeployment. +func poolerSecurityContextOpConfig() config.Config { + return config.Config{ + PodManagementPolicy: "ordered_ready", + ProtectedRoles: []string{"admin"}, + Auth: config.Auth{ + SuperUsername: superUserName, + ReplicationUsername: replicationUserName, + }, + Resources: config.Resources{ + ClusterLabels: map[string]string{"application": "spilo"}, + ClusterNameLabel: "cluster-name", + DefaultCPURequest: "300m", + DefaultCPULimit: "300m", + DefaultMemoryRequest: "300Mi", + DefaultMemoryLimit: "300Mi", + PodRoleLabel: "spilo-role", + }, + ConnectionPooler: config.ConnectionPooler{ + ConnectionPoolerDefaultCPURequest: "100m", + ConnectionPoolerDefaultCPULimit: "100m", + ConnectionPoolerDefaultMemoryRequest: "100Mi", + ConnectionPoolerDefaultMemoryLimit: "100Mi", + }, + PodServiceAccountName: "postgres-pod", + } +} + +// syncedPoolerDeployment provisions a pooler with the given OpConfig and returns the +// resulting master pooler Deployment. +func syncedPoolerDeployment(t *testing.T, opConfig config.Config) *appsv1.Deployment { + t.Helper() + client, _ := newFakeK8sPoolerTestClient() + clusterName := "acid-test-cluster" + namespace := "default" + + pg := acidv1.Postgresql{ + ObjectMeta: metav1.ObjectMeta{Name: clusterName, Namespace: namespace}, + Spec: acidv1.PostgresSpec{ + TeamID: "myapp", NumberOfInstances: 1, + EnableConnectionPooler: util.True(), + 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"}, + }, + } + + cluster := New(Config{OpConfig: opConfig}, client, pg, logger, eventRecorder) + + _, err := cluster.createStatefulSet() + assert.NoError(t, err) + + cluster.ConnectionPooler = map[PostgresRole]*ConnectionPoolerObjects{ + Master: { + Name: cluster.connectionPoolerName(Master), + ClusterName: clusterName, + Namespace: namespace, + Role: Master, + }, + } + + _, err = cluster.syncConnectionPoolerWorker(nil, &pg, Master) + assert.NoError(t, err) + + deploy, err := client.Deployments(namespace).Get(context.TODO(), cluster.connectionPoolerName(Master), metav1.GetOptions{}) + assert.NoError(t, err) + return deploy +} + +func TestConnectionPoolerDefaultSecurityContext(t *testing.T) { + deploy := syncedPoolerDeployment(t, poolerSecurityContextOpConfig()) + + podSc := deploy.Spec.Template.Spec.SecurityContext + assert.NotNil(t, podSc, "pod securityContext should be set") + assert.Equal(t, int64(100), *podSc.RunAsUser, "defaults to RunAsUser 100 for backward compatibility") + assert.Equal(t, int64(101), *podSc.RunAsGroup, "defaults to RunAsGroup 101 for backward compatibility") + + containerSc := deploy.Spec.Template.Spec.Containers[constants.ConnectionPoolerContainer].SecurityContext + assert.NotNil(t, containerSc, "container securityContext should be set") + assert.Equal(t, false, *containerSc.AllowPrivilegeEscalation, "defaults AllowPrivilegeEscalation to false") +} + +func TestConnectionPoolerPodSecurityContextOverride(t *testing.T) { + runAsUser := int64(1000001) + runAsGroup := int64(1000001) + spiloFSGroup := int64(103) + + opConfig := poolerSecurityContextOpConfig() + // FSGroup comes from the spilo config and must still apply when the override leaves it nil. + opConfig.Resources.SpiloFSGroup = &spiloFSGroup + opConfig.ConnectionPooler.PodSecurityContext = &v1.PodSecurityContext{ + RunAsUser: &runAsUser, + RunAsGroup: &runAsGroup, + RunAsNonRoot: util.True(), + SeccompProfile: &v1.SeccompProfile{ + Type: v1.SeccompProfileTypeRuntimeDefault, + }, + } + + deploy := syncedPoolerDeployment(t, opConfig) + podSc := deploy.Spec.Template.Spec.SecurityContext + + assert.Equal(t, runAsUser, *podSc.RunAsUser, "honors configured RunAsUser") + assert.Equal(t, runAsGroup, *podSc.RunAsGroup, "honors configured RunAsGroup") + assert.Equal(t, true, *podSc.RunAsNonRoot, "honors configured RunAsNonRoot") + assert.Equal(t, v1.SeccompProfileTypeRuntimeDefault, podSc.SeccompProfile.Type, "honors configured SeccompProfile") + assert.Equal(t, spiloFSGroup, *podSc.FSGroup, "still applies spilo FSGroup when override leaves it nil") +} + +func TestConnectionPoolerContainerSecurityContextOverride(t *testing.T) { + opConfig := poolerSecurityContextOpConfig() + opConfig.ConnectionPooler.SecurityContext = &v1.SecurityContext{ + AllowPrivilegeEscalation: util.False(), + ReadOnlyRootFilesystem: util.True(), + Capabilities: &v1.Capabilities{ + Drop: []v1.Capability{"ALL"}, + }, + } + + deploy := syncedPoolerDeployment(t, opConfig) + containerSc := deploy.Spec.Template.Spec.Containers[constants.ConnectionPoolerContainer].SecurityContext + + assert.Equal(t, true, *containerSc.ReadOnlyRootFilesystem, "honors configured ReadOnlyRootFilesystem") + assert.Equal(t, false, *containerSc.AllowPrivilegeEscalation, "keeps AllowPrivilegeEscalation false") + assert.Equal(t, []v1.Capability{"ALL"}, containerSc.Capabilities.Drop, "honors dropped capabilities") +} + func TestConnectionPoolerServiceSpec(t *testing.T) { testName := "Test connection pooler service spec generation" var cluster = New( diff --git a/pkg/controller/operator_config.go b/pkg/controller/operator_config.go index 667384e30..02c0651cd 100644 --- a/pkg/controller/operator_config.go +++ b/pkg/controller/operator_config.go @@ -287,5 +287,10 @@ func (c *Controller) importConfigurationFromCRD(fromCRD *acidv1.OperatorConfigur fromCRD.ConnectionPooler.MaxDBConnections, k8sutil.Int32ToPointer(constants.ConnectionPoolerMaxDBConnections)) + // Security contexts are nil unless explicitly configured; the cluster package falls back + // to the historical pooler defaults when they are not set. + result.ConnectionPooler.PodSecurityContext = fromCRD.ConnectionPooler.PodSecurityContext + result.ConnectionPooler.SecurityContext = fromCRD.ConnectionPooler.SecurityContext + return result } diff --git a/pkg/util/config/config.go b/pkg/util/config/config.go index 95324a747..e2baadbdb 100644 --- a/pkg/util/config/config.go +++ b/pkg/util/config/config.go @@ -165,6 +165,13 @@ type ConnectionPooler struct { ConnectionPoolerDefaultMemoryRequest string `name:"connection_pooler_default_memory_request"` ConnectionPoolerDefaultCPULimit string `name:"connection_pooler_default_cpu_limit"` ConnectionPoolerDefaultMemoryLimit string `name:"connection_pooler_default_memory_limit"` + // PodSecurityContext and SecurityContext let operators override the pooler + // pod- and container-level security contexts. They are populated only from the + // OperatorConfiguration CRD (name:"-" excludes them from the ConfigMap decoder, + // mirroring LivenessProbe). When nil, the operator falls back to the historical + // defaults (pod RunAsUser/RunAsGroup 100/101, container AllowPrivilegeEscalation=false). + PodSecurityContext *v1.PodSecurityContext `name:"-"` + SecurityContext *v1.SecurityContext `name:"-"` } // Config describes operator config