Kubernetes Pod Security Standards: PodSecurityPolicy vervangen na 1.25

PodSecurityPolicy is verwijderd in Kubernetes 1.25. De vervanger, Pod Security Admission (PSA) met Pod Security Standards (PSS), is ingebouwd en standaard ingeschakeld op elk cluster, maar doet niets tenzij je namespaces labelt. Deze gids loopt door de drie profielen, de drie handhavingsmodi, het migratiepad vanuit PSP, en de cluster-level configuratie die voorkomt dat nieuwe namespaces onbeschermd draaien.

Inhoudsopgave

Doel

Na het doorlopen van deze gids heb je je cluster gemigreerd van PodSecurityPolicy naar Pod Security Admission, met gelabelde namespaces, een cluster-level vangnet, en geen gat in de pod security enforcement.

Vereisten

  • Een Kubernetes-cluster op versie 1.25 of nieuwer (PSA is GA sinds 1.25)
  • kubectl-toegang met rechten om namespaces te labelen, PSPs te bekijken en RBAC-bindings te beheren
  • Draai je nog op Kubernetes 1.22 tot en met 1.24, dan is PSA beschikbaar als beta admission plugin; de labelsyntax is identiek maar de AdmissionConfiguration API-versie verschilt (zie stap 6)
  • Bekendheid met RBAC, want namespace-labelpermissies bepalen wie de enforcement kan verzwakken

De PSP-verwijderingstijdlijn

PodSecurityPolicy werd deprecated in Kubernetes 1.21 (april 2021) en volledig verwijderd in 1.25 (augustus 2022). Dat was niet zomaar een labeltje. PSP had fundamentele ontwerpfouten die niet opgelost konden worden zonder breaking changes: een verwarrend toepassingsmechanisme waardoor je onbedoeld te brede rechten kon verlenen, geen audit-modus voor veilige uitrol, en een mutatie-eigenschap die het onmogelijk maakte om te achterhalen wat er precies met een pod spec was gebeurd.

Wanneer PSP verdwijnt (door verwijdering of een clusterupgrade naar 1.25), heeft elke namespace zonder PSA-labels helemaal geen pod security enforcement meer. Pods kunnen als root draaien, hostpaden mounten en willekeurige capabilities aanvragen. Dit is het gevaarlijkste moment in de hele migratie.

PSP-verwijdering raakt securityContext niet. Het securityContext-veld in pod specs is de security-configuratie op podniveau; het staat los van het admission-beleid dat het valideert.

Drie profielen: privileged, baseline, restricted

Pod Security Standards definiëren drie profielen die cumulatief strenger worden:

Privileged is volledig onbeperkt. Geen securitybeperkingen. Gebruik dit voor infrastructuur-namespaces (kube-system, monitoring, CNI-plugins) waar workloads legitiem hosttoegang nodig hebben.

Baseline blokkeert bekende privilege-escalatiepaden terwijl het adopteerbaar blijft voor de meeste applicatieworkloads. Het verbiedt hostNetwork, hostPID, hostIPC, privileged containers, hostPath-volumes, en beperkt capabilities tot een veilige subset (NET_BIND_SERVICE, CHOWN, SETUID, SETGID en andere). Seccomp mag niet Unconfined zijn. SELinux-types zijn beperkt tot container_t, container_init_t en container_kvm_t.

Restricted voegt alles van Baseline toe plus: runAsNonRoot moet true zijn, allowPrivilegeEscalation moet false zijn, capabilities moeten ALL droppen (alleen NET_BIND_SERVICE mag weer worden toegevoegd), en een seccompProfile van RuntimeDefault of Localhost is verplicht.

Elke container in de pod moet voldoen. Als een enkele init-container de validatie niet haalt, wordt de hele pod geweigerd.

Welk profiel voor welke namespace

Type namespace Aanbevolen profiel
Applicatieworkloads (gehard) restricted
Applicatieworkloads (standaard) baseline
Infrastructuur (CNI, logging, monitoring, ingress) privileged
kube-system, kube-public, kube-node-lease Exempt via AdmissionConfiguration

Het Restricted-profiel faalt voor het overgrote deel van de workloads die niet expliciet gehard zijn. Standaard nginx-images draaien als root. Calico, Flannel en Cilium hebben NET_ADMIN, SYS_ADMIN en host-namespaces nodig. Logshippers hebben hostPath-volumes nodig om /var/log te lezen. Begin enforcement op Baseline voor applicatie-namespaces en verscherp naar Restricted nadat je de securityContext-velden van je workloads hebt gefixed.

Drie handhavingsmodi: enforce, audit, warn

PSA heeft drie modi die je onafhankelijk per namespace instelt:

Modus Blokkeert pods? Wat het rapporteert Geldt voor
enforce Ja Rejection-fout bij pod-creatie Alleen pod-objecten
audit Nee Annotatie in Kubernetes audit log Workload-resources + pods
warn Nee Warning:-melding in kubectl-output Workload-resources + pods

De asymmetrie tussen enforce en de andere twee modi is het meest verkeerd begrepen gedrag. enforce evalueert alleen het resulterende Pod-object, niet de Deployment of StatefulSet die het aanmaakte. Je kunt gewoon kubectl apply doen op een Deployment met een violating pod template en het lukt. De blokkade komt pas wanneer de controller de daadwerkelijke Pod aanmaakt. De Deployment staat dan in je cluster terwijl het ReplicaSet pods in een loop faalt.

audit en warn evalueren workload-resources (Deployments, Jobs, StatefulSets) op templateniveau en vangen violations dus al op voordat pods worden aangemaakt. Daarom is audit en warn eerst draaien niet optioneel; het is de enige manier om template-level problemen te vangen voordat enforce pods blokkeert.

warn blokkeert niks. Het stuurt een Warning:-prefix naar de kubectl-client. Operators zien soms warn-output en nemen aan dat hun workloads beschermd zijn. Dat zijn ze pas wanneer enforce is ingeschakeld.

Stap 1: huidige PSPs inventariseren

Bepaal voordat je migreert wat je PSPs daadwerkelijk afdwingen. PSA kan niet alle PSP-functionaliteit repliceren. PSA is namelijk non-mutating: het valideert maar wijzigt nooit pod specs. Als je leunde op PSP om defaults in te stellen (zoals defaultAllowPrivilegeEscalation of runAsGroup: MustRunAs), heb je een mutating admission webhook of een policy engine zoals Kyverno nodig als aanvulling.

Bekijk je huidige PSPs en identificeer muterende velden:

# Alle PSPs bekijken (werkt alleen voor 1.25)
kubectl get psp

# Exporteren voor analyse
kubectl get psp -o yaml > current-psps.yaml

Verwijder deze mutatie-velden uit je analyse (PSA heeft geen equivalent):

  • defaultAllowPrivilegeEscalation
  • defaultAddCapabilities
  • runtimeClass.defaultRuntimeClassName
  • Seccomp- en AppArmor-default-profielannotaties

Let ook op velden die PSA's drie vaste profielen niet valideren: allowedHostPaths (padspecifieke restricties), allowedFlexVolumes, allowedCSIDrivers, forbiddenSysctls, runAsGroup (MustRunAs-strategie), supplementalGroups en fsGroup. Als je security-posture van deze velden afhangt, is PSA alleen niet genoeg.

Stap 2: namespaces labelen met audit en warn

Pas niet-blokkerende labels toe op alle namespaces om violations te observeren zonder workloads te verstoren:

apiVersion: v1
kind: Namespace
metadata:
  name: production
  labels:
    pod-security.kubernetes.io/audit: restricted
    pod-security.kubernetes.io/audit-version: latest    # vooruitkijkend
    pod-security.kubernetes.io/warn: restricted
    pod-security.kubernetes.io/warn-version: latest

Toepassen op alle namespaces tegelijk:

# Alle niet-systeem namespaces labelen met audit + warn op restricted-niveau
for ns in $(kubectl get ns -o jsonpath='{.items[*].metadata.name}'); do
  kubectl label --overwrite ns "$ns" \
    pod-security.kubernetes.io/audit=restricted \
    pod-security.kubernetes.io/audit-version=latest \
    pod-security.kubernetes.io/warn=restricted \
    pod-security.kubernetes.io/warn-version=latest
done

Verwachte output: elke kubectl label-opdracht print namespace/<naam> labeled.

Stap 3: workload-violations fixen

Rol je workloads opnieuw uit om evaluatie te triggeren tegen de nieuwe labels. Let op Warning:-meldingen in de kubectl-output:

kubectl rollout restart deployment -n production
# Verwacht: Warning: would violate PodSecurity "restricted:latest":
# allowPrivilegeEscalation != false, ...

Controleer het Kubernetes audit log op pod-security-annotaties die violations in audit-modus identificeren.

De typische fixes bestaan uit het toevoegen of aanscherpen van securityContext op zowel pod- als containerniveau:

apiVersion: apps/v1
kind: Deployment
metadata:
  name: web-app
  namespace: production
spec:
  template:
    spec:
      securityContext:
        runAsNonRoot: true             # podniveau
        seccompProfile:
          type: RuntimeDefault         # vereist door restricted
      containers:
      - name: web
        securityContext:
          allowPrivilegeEscalation: false
          readOnlyRootFilesystem: true
          capabilities:
            drop: ["ALL"]              # alles droppen, dan toevoegen wat nodig is
            add: ["NET_BIND_SERVICE"]  # alleen als de container bindt onder 1024

Itereer totdat kubectl apply geen warnings meer produceert. Systeem-namespaces (waar CNI, logging en monitoring draaien) zullen wel warnings tonen onder het restricted-profiel. Dat is verwacht; die namespaces krijgen het privileged-profiel in de volgende stap.

Stap 4: enforce mode inschakelen per namespace

Zodra audit en warn geen violations meer tonen voor een namespace, pas je het enforce-label toe:

kubectl label --overwrite ns production \
  pod-security.kubernetes.io/enforce=baseline \
  pod-security.kubernetes.io/enforce-version=v1.31

Pin enforce-version op een specifieke Kubernetes-minor-versie. Dit voorkomt dat een clusterupgrade nieuwe beleidschecks introduceert die eerder conforme pods plotseling blokkeren. Gebruik latest voor audit- en warn-versies zodat je vroegtijdig zicht hebt op toekomstige vereisten.

Het aanbevolen patroon voor applicatie-namespaces die volledig gehard zijn:

labels:
  pod-security.kubernetes.io/enforce: restricted
  pod-security.kubernetes.io/enforce-version: v1.31    # vastgezet
  pod-security.kubernetes.io/audit: restricted
  pod-security.kubernetes.io/audit-version: latest      # vooruitkijkend
  pod-security.kubernetes.io/warn: restricted
  pod-security.kubernetes.io/warn-version: latest

Voor namespaces die nog niet volledig gehard zijn: enforce baseline en audit/warn restricted. Rol namespace voor namespace uit. Zet niet alles in een keer op enforce.

Stap 5: PSP-objecten en RBAC-bindings verwijderen

Nadat PSA enforce actief is op alle namespaces, ruim je PSP-artefacten op:

# Controleer op resterende PSP RBAC-dependencies
kubectl get clusterrolebinding -o json | jq '.items[] | select(.roleRef.name | test("psp")) | .metadata.name'
kubectl get rolebinding -A -o json | jq '.items[] | select(.roleRef.name | test("psp")) | {ns: .metadata.namespace, name: .metadata.name}'

# Verwijder PSP RBAC-bindings
kubectl delete clusterrolebinding <psp-binding-naam>
kubectl delete rolebinding <psp-binding-naam> -n <namespace>

# Verwijder PSPs (alleen op clusters nog onder 1.25)
kubectl delete psp --all

Op Kubernetes 1.25+ bestaan PSP-objecten niet meer. Het risico is dat de upgrade stilletjes PSP-enforcement verwijderde terwijl je namespaces geen PSA-labels hadden. Lees je dit na een upgrade, controleer dan of je namespaces gelabeld zijn. Ongelabelde namespaces hebben geen enforcement.

Stap 6: cluster-level defaults instellen

Namespace-labels beschermen gelabelde namespaces. Nieuwe namespaces zonder labels hebben geen bescherming. Een cluster-level AdmissionConfiguration vangt dit gat op:

# /etc/kubernetes/psa-config.yaml
apiVersion: apiserver.config.k8s.io/v1       # v1 voor K8s 1.25+
kind: AdmissionConfiguration
plugins:
- name: PodSecurity
  configuration:
    apiVersion: pod-security.admission.config.k8s.io/v1
    kind: PodSecurityConfiguration
    defaults:
      enforce: "baseline"
      enforce-version: "latest"
      audit: "restricted"
      audit-version: "latest"
      warn: "restricted"
      warn-version: "latest"
    exemptions:
      usernames: []
      runtimeClasses: []
      namespaces:
        - kube-system
        - kube-public
        - kube-node-lease

Verwijs naar dit bestand in de API-serverconfiguratie:

# Voor kubeadm-clusters, toevoegen aan ClusterConfiguration:
# apiServer:
#   extraArgs:
#     admission-control-config-file: /etc/kubernetes/psa-config.yaml
#   extraVolumes:
#   - name: psa-config
#     hostPath: /etc/kubernetes/psa-config.yaml
#     mountPath: /etc/kubernetes/psa-config.yaml
#     readOnly: true

De AdmissionConfiguration API-versie hangt af van je Kubernetes-versie: v1 voor 1.25+, v1beta1 voor 1.23 tot en met 1.24, v1alpha1 voor 1.22.

Op managed clusters (EKS, GKE, AKS): alle drie de providers schakelen PSA standaard in maar zetten de cluster-level default op privileged voor alle modi. Dat betekent dat er geen restricties gelden tenzij je expliciet namespaces labelt. De AdmissionConfiguration kun je op managed clusters doorgaans niet direct aanpassen. Namespace-labels zijn dan je primaire configuratie-oppervlak.

Stap 7: automatisering bijwerken voor nieuwe namespaces

Elk namespace-provisioning-tool (Helm-charts, Kustomize-bases, Terraform-modules, CI/CD-templates) moet PSA-labels toepassen op nieuwe namespaces. Zonder automatisering draait de eerstvolgende namespace die iemand via kubectl create ns aanmaakt zonder bescherming.

Gebruik je een GitOps-tool zoals ArgoCD, voeg de labels dan toe aan je namespace-manifesten in de Git-repository. Gebruik je Terraform met de Kubernetes-provider, voeg de labels toe aan je kubernetes_namespace-resources.

Eindresultaat verifiëren

Controleer of alle namespaces enforcement-labels hebben:

kubectl get ns --show-labels | grep pod-security

Verwachte output: elke namespace toont pod-security.kubernetes.io/enforce met waarde baseline of restricted. Systeem-namespaces tonen of privileged of staan in de AdmissionConfiguration-exemptions.

Test dat enforcement daadwerkelijk violations blokkeert:

kubectl run test-privileged --image=nginx \
  --overrides='{"spec":{"containers":[{"name":"nginx","image":"nginx","securityContext":{"privileged":true}}]}}' \
  -n production --dry-run=server
# Verwacht: Error from server (Forbidden): ... violates PodSecurity "baseline:..."

Als de testpod wordt geaccepteerd, ontbreken de namespace-labels of staan ze op privileged.

Exemptions en de risico's ervan

Exemptions worden geconfigureerd in de AdmissionConfiguration en omzeilen alle PSA-modi (enforce, audit en warn). Er zijn drie dimensies:

  1. Namespaces (meestal kube-system)
  2. Usernames (bijvoorbeeld CI-systeemaccounts)
  3. RuntimeClassNames (pods die specifieke runtimeklassen gebruiken)

Exempt geen controller-service-accounts zoals system:serviceaccount:kube-system:replicaset-controller. Een controller exempten exempt in feite elke gebruiker die de bijbehorende workload-resource kan aanmaken, want de pod wordt uiteindelijk door de identiteit van de controller aangemaakt.

Iedereen met update- of patch-rechten op namespace-objecten kan PSA-labels verwijderen en daarmee enforcement uitschakelen. Vergrendel namespace-labelpermissies via RBAC of voeg een validating admission webhook toe om ongeautoriseerde labelwijzigingen te voorkomen.

Wanneer PSA niet genoeg is

PSA's drie vaste profielen dekken de meeste situaties. Je hebt een third-party policy engine nodig wanneer je het volgende vereist:

  • Per-ServiceAccount beleid binnen een namespace (PSA is alleen namespace-scoped)
  • Mutatie (automatisch security-defaults instellen op pods die ze niet zelf zetten)
  • Beleid op niet-pod-resources (NetworkPolicies, ConfigMaps, custom resources)
  • Gedetailleerde aangepaste regels die verder gaan dan de drie vaste profielen

Kyverno (CNCF Incubating) schrijft beleid in YAML en ondersteunt validatie, mutatie en generatie. OPA/Gatekeeper (CNCF Graduate) gebruikt de Rego-taal en is flexibeler maar heeft een steilere leercurve.

De aanbevolen aanpak: gebruik PSA als ingebouwde baseline (geen dependencies, altijd ingeschakeld) en voeg Kyverno of Gatekeeper toe voor regels die PSA niet kan uitdrukken. Vervang PSA niet door een policy engine; bouw erop voort.

Veelvoorkomende problemen

Pods geweigerd maar de Deployment is wel geaccepteerd. Dit is de enforce-modus-asymmetrie die hierboven beschreven staat. Controleer de ReplicaSet-events: kubectl describe rs <replicaset-naam> -n <namespace>. De events tonen de rejection op podniveau. Fix de securityContext van de pod template.

Geen warnings na het labelen. De warn- en audit-labels evalueren workloads op het moment van apply. Bestaande pods worden niet opnieuw geëvalueerd. Herstart of herinstalleer je workloads: kubectl rollout restart deployment -n <namespace>.

Workloads in kube-system breken na labelen. Systeemcomponenten hebben het privileged-profiel nodig. Label kube-system als privileged of exempt het in de AdmissionConfiguration.

Pods halen warn-modus maar falen op enforce-modus. Dit kan gebeuren wanneer de pod template en de daadwerkelijke pod verschillen (admission webhooks die sidecars injecteren, bijvoorbeeld). De geïnjecteerde sidecar kan het beleid schenden ook al haalde de template de check wel. Bekijk de daadwerkelijke pod spec, niet alleen de Deployment-template.

Wanneer escaleren

Als pods geweigerd worden en je kunt niet identificeren welk veld het beleid schendt, verzamel dan:

  • De volledige foutmelding uit kubectl describe op het ReplicaSet of de pod
  • De namespace-labels: kubectl get ns <namespace> --show-labels
  • De pod spec: kubectl get pod <pod-naam> -n <namespace> -o yaml
  • Kubernetes-versie: kubectl version
  • Of er admission webhooks zijn die pod specs muteren: kubectl get mutatingwebhookconfigurations
  • De cluster-level AdmissionConfiguration (als je toegang hebt tot de API-serverconfiguratie)

Terugkerende server- of deploymentproblemen?

Ik help teams productie betrouwbaar maken met CI/CD, Kubernetes en cloud—zodat fixes blijven en deploys geen stress meer zijn.

Bekijk DevOps consultancy

Doorzoek deze site

Begin met typen om te zoeken, of blader door de kennisbank en blog.