Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions charts/postgres-operator/crds/operatorconfigurations.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -196,6 +196,10 @@ spec:
items:
type: string
type: array
drop_pod_capabilities:
items:
type: string
type: array
enable_cross_namespace_secret:
type: boolean
enable_finalizers:
Expand Down Expand Up @@ -498,12 +502,17 @@ spec:
type: integer
spilo_privileged:
type: boolean
spilo_run_as_non_root:
type: boolean
spilo_runasgroup:
format: int64
type: integer
spilo_runasuser:
format: int64
type: integer
spilo_seccomp_profile:
pattern: ^(RuntimeDefault|Unconfined|Localhost/.+)?$
type: string
storage_resize_mode:
default: pvc
enum:
Expand Down
9 changes: 8 additions & 1 deletion charts/postgres-operator/values.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -229,8 +229,15 @@ configKubernetes:
# whether the Spilo container should run in privileged mode
spilo_privileged: false
# whether the Spilo container should run with additional permissions other than parent.
# required by cron which needs setuid
# required by cron which needs setuid when Spilo runs as root
spilo_allow_privilege_escalation: true
# refuse to start containers of the Spilo pods running as root
# spilo_run_as_non_root: true
# seccomp profile for the Spilo pods: RuntimeDefault, Unconfined or Localhost/<profile>
# spilo_seccomp_profile: RuntimeDefault
# list of capabilities to drop from the containers of the Spilo pods
# drop_pod_capabilities:
# - "ALL"

# liveness probe for the spilo pod
# liveness_probe:
Expand Down
41 changes: 41 additions & 0 deletions docs/administrator.md
Original file line number Diff line number Diff line change
Expand Up @@ -472,6 +472,47 @@ avoid sudden connection issues in pods which still use credentials of these
users in memory. You have to remove these child users manually or re-enable
password rotation with smaller interval so they get cleaned up.

## Pod Security Standards

To run Postgres clusters in namespaces enforcing the
[`restricted` Pod Security Standard](https://kubernetes.io/docs/concepts/security/pod-security-standards/#restricted),
configure Spilo to run as non-root and restrict the security context of all
containers of the Postgres pods, including sidecars, init containers and the
logical backup job:

```yaml
apiVersion: "acid.zalan.do/v1"
kind: OperatorConfiguration
metadata:
name: postgresql-configuration
configuration:
kubernetes:
spilo_runasuser: 101
spilo_runasgroup: 103
spilo_fsgroup: 103
spilo_run_as_non_root: true
spilo_allow_privilege_escalation: false
spilo_seccomp_profile: RuntimeDefault
drop_pod_capabilities:
- ALL
```

`spilo_runasgroup` must be the primary group of the `postgres` user in the
Spilo image (`103`), otherwise Spilo's cron jobs (e.g. WAL-G basebackups) are
not executed. Running as non-root, cron works without privilege escalation,
only the optional `renice` job is skipped. Settings defined in the security
context of a sidecar or init container take precedence, and privileged
containers are left untouched. With `spilo_run_as_non_root` enabled, clusters
must not override `spiloRunAsUser` with `0` in their manifest.

When these fields are enforced by mutating admission webhooks (e.g. Kyverno),
configure the same values in the operator. Otherwise the StatefulSet generated
by the operator differs from the mutated one and the operator triggers a
rolling update of the cluster on every sync.

Changing these options updates the pod template of every Postgres cluster,
which triggers a rolling update of all pods.

## Use taints and tolerations for dedicated PostgreSQL nodes

To ensure Postgres pods are running on nodes without any other application pods,
Expand Down
33 changes: 31 additions & 2 deletions docs/reference/operator_parameters.md
Original file line number Diff line number Diff line change
Expand Up @@ -544,15 +544,44 @@ configuration they are grouped under the `kubernetes` key.

* **spilo_allow_privilege_escalation**
Controls whether a process can gain more privileges than its parent
process. Required by cron which needs setuid. Without this parameter,
certification rotation & backups will not be done. The default is `true`.
process. Required by cron which needs setuid when Spilo runs as root. Without
this parameter, certification rotation & backups will not be done. Spilo
running as non-root (see `spilo_runasuser`) does not need it. When set to
`false`, `allowPrivilegeEscalation: false` is also set on sidecars, init
containers and the logical backup container, unless they define it themselves
or run privileged. The default is `true`.

* **additional_pod_capabilities**
list of additional capabilities to be added to the postgres container's
SecurityContext (e.g. SYS_NICE etc.). Please, make sure first that the
PodSecruityPolicy allows the capabilities listed here. Otherwise, the
container will not start. The default is empty.

* **drop_pod_capabilities**
list of capabilities to be dropped from the Spilo, sidecar, init and logical
backup containers, e.g. `ALL`. Containers that define their own dropped
capabilities or run privileged are left untouched. Capabilities listed in
`additional_pod_capabilities` are still added to the Spilo container. The
default is empty.

* **spilo_run_as_non_root**
sets `runAsNonRoot` in the pod security context of the Spilo and logical
backup pods, so the kubelet refuses to start any container running as root.
It is also set in the security context of the Spilo, sidecar, init and logical
backup containers, unless they define it themselves or run privileged. This
keeps the generated pod template in sync with admission webhooks (e.g.
Kyverno) enforcing `runAsNonRoot`, which would otherwise trigger a rolling
update on every sync. Requires a non-root `spilo_runasuser`: a cluster
manifest setting `spiloRunAsUser: 0` will fail to start its pods. The default
is empty (not set).

* **spilo_seccomp_profile**
seccomp profile set in the pod security context of the Spilo and logical
backup pods, applying to all their containers unless they define their own.
Allowed values are `RuntimeDefault`, `Unconfined` and
`Localhost/<profile path>`. On OpenShift the SecurityContextConstraints of
the pods must allow the profile. The default is empty (not set).

* **master_pod_move_timeout**
The period of time to wait for the success of migration of master pods from
an unschedulable node. The migration includes Patroni switchovers to
Expand Down
3 changes: 3 additions & 0 deletions manifests/configmap.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ metadata:
data:
# additional_owner_roles: "cron_admin"
# additional_pod_capabilities: "SYS_NICE"
# drop_pod_capabilities: "ALL"
# additional_secret_mount: "some-secret-name"
# additional_secret_mount_path: "/some/dir"
api_port: "8080"
Expand Down Expand Up @@ -160,6 +161,8 @@ data:
# spilo_runasuser: 101
# spilo_runasgroup: 103
# spilo_fsgroup: 103
# spilo_run_as_non_root: "true"
# spilo_seccomp_profile: "RuntimeDefault"
spilo_privileged: "false"
storage_resize_mode: "pvc"
super_username: postgres
Expand Down
9 changes: 9 additions & 0 deletions manifests/operatorconfiguration.crd.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -196,6 +196,10 @@ spec:
items:
type: string
type: array
drop_pod_capabilities:
items:
type: string
type: array
enable_cross_namespace_secret:
type: boolean
enable_finalizers:
Expand Down Expand Up @@ -498,12 +502,17 @@ spec:
type: integer
spilo_privileged:
type: boolean
spilo_run_as_non_root:
type: boolean
spilo_runasgroup:
format: int64
type: integer
spilo_runasuser:
format: int64
type: integer
spilo_seccomp_profile:
pattern: ^(RuntimeDefault|Unconfined|Localhost/.+)?$
type: string
storage_resize_mode:
default: pvc
enum:
Expand Down
4 changes: 4 additions & 0 deletions manifests/postgresql-operator-default-configuration.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,8 @@ configuration:
kubernetes:
# additional_pod_capabilities:
# - "SYS_NICE"
# drop_pod_capabilities:
# - "ALL"
cluster_domain: cluster.local
cluster_labels:
application: spilo
Expand Down Expand Up @@ -124,6 +126,8 @@ configuration:
# spilo_runasuser: 101
# spilo_runasgroup: 103
# spilo_fsgroup: 103
# spilo_run_as_non_root: true
# spilo_seccomp_profile: RuntimeDefault
spilo_privileged: false
storage_resize_mode: pvc
# toleration:
Expand Down
4 changes: 4 additions & 0 deletions pkg/apis/acid.zalan.do/v1/operator_configuration_type.go
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,11 @@ type KubernetesMetaConfiguration struct {
SpiloRunAsGroup *int64 `json:"spilo_runasgroup,omitempty"`
SpiloFSGroup *int64 `json:"spilo_fsgroup,omitempty"`
AdditionalPodCapabilities []string `json:"additional_pod_capabilities,omitempty"`
DropPodCapabilities []string `json:"drop_pod_capabilities,omitempty"`
SpiloRunAsNonRoot *bool `json:"spilo_run_as_non_root,omitempty"`
WatchedNamespace string `json:"watched_namespace,omitempty"`
// +kubebuilder:validation:Pattern=`^(RuntimeDefault|Unconfined|Localhost/.+)?$`
SpiloSeccompProfile string `json:"spilo_seccomp_profile,omitempty"`
// +kubebuilder:default="postgres-{cluster}-pdb"
// defines the template for PDB names
PDBNameFormat config.StringTemplate `json:"pdb_name_format,omitempty"`
Expand Down
9 changes: 9 additions & 0 deletions pkg/apis/acid.zalan.do/v1/operatorconfiguration.crd.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -196,6 +196,10 @@ spec:
items:
type: string
type: array
drop_pod_capabilities:
items:
type: string
type: array
enable_cross_namespace_secret:
type: boolean
enable_finalizers:
Expand Down Expand Up @@ -498,12 +502,17 @@ spec:
type: integer
spilo_privileged:
type: boolean
spilo_run_as_non_root:
type: boolean
spilo_runasgroup:
format: int64
type: integer
spilo_runasuser:
format: int64
type: integer
spilo_seccomp_profile:
pattern: ^(RuntimeDefault|Unconfined|Localhost/.+)?$
type: string
storage_resize_mode:
default: pvc
enum:
Expand Down
10 changes: 10 additions & 0 deletions pkg/apis/acid.zalan.do/v1/zz_generated.deepcopy.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

73 changes: 73 additions & 0 deletions pkg/cluster/k8sres.go
Original file line number Diff line number Diff line change
Expand Up @@ -506,6 +506,67 @@ func generateCapabilities(capabilities []string) *v1.Capabilities {
return nil
}

// applyContainerSecurityDefaults fills the container security context fields
// from the operator configuration: dropped capabilities and disallowed privilege
// escalation, which cannot be set on pod level, and runAsNonRoot, which is also
// set on pod level but repeated on the containers so the generated pod template
// matches what admission webhooks enforcing it inject. Fields a container
// already sets are kept and privileged containers are left untouched. The
// containers and their security contexts are copied, as they may be shared with
// the cluster manifest or the operator configuration.
func (c *Cluster) applyContainerSecurityDefaults(containers []v1.Container) []v1.Container {
dropCapabilities := make([]v1.Capability, 0, len(c.OpConfig.DropPodCapabilities))
for _, capability := range c.OpConfig.DropPodCapabilities {
dropCapabilities = append(dropCapabilities, v1.Capability(strings.ToUpper(capability)))
}
disallowPrivilegeEscalation := c.OpConfig.SpiloAllowPrivilegeEscalation != nil && !*c.OpConfig.SpiloAllowPrivilegeEscalation
runAsNonRoot := c.OpConfig.SpiloRunAsNonRoot

if len(dropCapabilities) == 0 && !disallowPrivilegeEscalation && runAsNonRoot == nil {
return containers
}

result := make([]v1.Container, len(containers))
for i, container := range containers {
result[i] = container
securityContext := &v1.SecurityContext{}
if container.SecurityContext != nil {
securityContext = container.SecurityContext.DeepCopy()
}
if securityContext.Privileged != nil && *securityContext.Privileged {
continue
}
if len(dropCapabilities) > 0 && (securityContext.Capabilities == nil || len(securityContext.Capabilities.Drop) == 0) {
if securityContext.Capabilities == nil {
securityContext.Capabilities = &v1.Capabilities{}
}
securityContext.Capabilities.Drop = slices.Clone(dropCapabilities)
}
// the API server rejects allowPrivilegeEscalation=false together with CAP_SYS_ADMIN
if disallowPrivilegeEscalation && securityContext.AllowPrivilegeEscalation == nil && !addsSysAdmin(securityContext.Capabilities) {
securityContext.AllowPrivilegeEscalation = util.False()
}
if runAsNonRoot != nil && securityContext.RunAsNonRoot == nil {
securityContext.RunAsNonRoot = new(bool)
*securityContext.RunAsNonRoot = *runAsNonRoot
}
result[i].SecurityContext = securityContext
}
return result
}

func addsSysAdmin(capabilities *v1.Capabilities) bool {
if capabilities == nil {
return false
}
for _, capability := range capabilities.Add {
if capability == "SYS_ADMIN" || capability == "CAP_SYS_ADMIN" {
return true
}
}
return false
}

func (c *Cluster) nodeAffinity(nodeReadinessLabel map[string]string, nodeAffinity *v1.NodeAffinity) *v1.Affinity {
if len(nodeReadinessLabel) == 0 && nodeAffinity == nil {
return nil
Expand Down Expand Up @@ -864,6 +925,18 @@ func (c *Cluster) generatePodTemplate(
securityContext.FSGroup = c.Spec.SpiloFSGroup
}

// pod level settings apply to all containers, including init containers,
// unless a container overrides them in its own security context
securityContext.RunAsNonRoot = c.OpConfig.Resources.SpiloRunAsNonRoot
seccompProfile, err := config.SeccompProfile(c.OpConfig.Resources.SpiloSeccompProfile)
if err != nil {
return nil, fmt.Errorf("could not generate pod security context: spilo_seccomp_profile: %v", err)
}
securityContext.SeccompProfile = seccompProfile

containers = c.applyContainerSecurityDefaults(containers)
initContainers = c.applyContainerSecurityDefaults(initContainers)

podSpec := v1.PodSpec{
ServiceAccountName: podServiceAccountName,
TerminationGracePeriodSeconds: &terminateGracePeriodSeconds,
Expand Down
Loading
Loading