Pod Security Context in CloudNativePG#
Overview#
CloudNativePG applies a default PodSecurityContext to every PostgreSQL instance pod. When users supply a custom spec.podSecurityContext, the operator uses a selective backfill strategy: it merges only RunAsUser, RunAsGroup, and SeccompProfile from defaults into the user-provided context, while leaving FSGroup and RunAsNonRoot untouched.
This behavior is implemented in GetPodSecurityContext in pkg/specs/pods.go and was introduced by PR #6614.
Default Security Context#
When spec.podSecurityContext is not set, the operator builds the following defaults :
| Field | Default Value | Source |
|---|---|---|
RunAsUser | 26 (UID for postgres) | GetPostgresUID() |
RunAsGroup | 26 (GID for postgres) | GetPostgresGID() |
FSGroup | 26 (same as GID) | inline default |
RunAsNonRoot | true | inline default |
SeccompProfile | RuntimeDefault | GetSeccompProfile() |
DefaultPostgresUID and DefaultPostgresGID are both 26, matching the UID/GID of the postgres system user in typical container images . These can be overridden via spec.postgresUID / spec.postgresGID in the Cluster spec.
On OpenShift, the entire PodSecurityContext is set to nil because security constraints are inherited from the restricted SecurityContextConstraints (SCC) .
Selective Backfill Behavior#
When spec.podSecurityContext is set, the operator deep-copies the user's context and backfills only three fields if they are nil :
RunAsUser← filled fromGetPostgresUID()if not setRunAsGroup← filled fromGetPostgresGID()if not setSeccompProfile← filled fromGetSeccompProfile()if not set
FSGroup and RunAsNonRoot are NOT backfilled. If the user provides a PodSecurityContext without setting these fields, they will remain nil in the resulting context .
Why FSGroup and RunAsNonRoot are excluded#
FSGroupis storage-specific. Different storage backends (and especially OpenShift's restricted SCC) may reject or conflict with a forced fsGroup value. Leaving it unset lets users—and the platform—control volume ownership explicitly.RunAsNonRootis a binary security policy decision. The operator does not silently enforce this policy when a user has opted into providing their own security context; they may have a valid reason to leave it unset (e.g., PSA policy enforcement at namespace level already covers this).
The deep-copy prevents mutation of the cluster spec object .
Test Coverage#
The behavior is explicitly validated in pkg/specs/pods_test.go:
- No custom context → all five default fields are set
- Partial custom context (only
RunAsUserset) →RunAsGroupandSeccompProfileare backfilled;FSGroupandRunAsNonRootremainnil - Custom
SeccompProfileonspec.seccompProfile→ honored whenpodSecurityContext.SeccompProfileisnil;FSGroupandRunAsNonRootstill remainnil - User explicitly sets
FSGroupandRunAsNonRoot→ preserved as-is
API Fields#
Defined in api/v1/cluster_types.go:
spec.podSecurityContext(*corev1.PodSecurityContext, optional) — overrides the operator's default pod-level security context. No effect when SecurityContextConstraints are present.spec.seccompProfile(*corev1.SeccompProfile, optional) — sets a custom seccomp profile for all pods and containers; defaults toRuntimeDefault. Consulted byGetSeccompProfile()when building both pod and container security contexts.spec.postgresUID/spec.postgresGID(int64, optional) — custom UID/GID used inRunAsUser,RunAsGroup, and (in the full-default path)FSGroup.
Container-level security context is handled separately via GetSecurityContext() (configured via spec.securityContext).
Key Source References#
| File | Purpose |
|---|---|
pkg/specs/pods.go:448-482 | GetPodSecurityContext — main merge logic |
pkg/specs/pods_test.go:68-160 | Unit tests for all backfill scenarios |
api/v1/cluster_funcs.go:1335-1344 | GetSeccompProfile() |
api/v1/cluster_funcs.go:1056-1072 | GetPostgresUID() / GetPostgresGID() |
api/v1/cluster_types.go | PodSecurityContext, SeccompProfile field definitions |
| PR #6614 | Original feature introduction |