Skip to content

Kubernetes Pod Security Contexts and Policies — Secure Container Hardening Guide

DodaTech Updated 2026-06-22 7 min read

In this tutorial, you'll learn about Kubernetes Pod Security Contexts and Policies. We cover key concepts, practical examples, and best practices to help you understand and apply this topic effectively.

Kubernetes Pod security contexts define privilege and access control settings at the Pod and container level, enforcing read-only filesystems, non-root users, capability drops, and system call filtering to harden workloads against container escapes.

What You'll Learn

Why It Matters

Containers run as root by default inside the container namespace. If an attacker exploits an application vulnerability, root access inside the container can lead to a container escape that compromises the host node. Security contexts prevent this by dropping unnecessary capabilities, enforcing non-root users, making the root filesystem read-only, and restricting system calls. Without these controls, a single application vulnerability can escalate into a full cluster compromise.

Real-World Use

DodaTech enforces security contexts across all Durga Antivirus Pro deployments -- API containers run as non-root user ID 1001, with all capabilities dropped except NET_BIND_SERVICE, root filesystem set to read-only, and a seccomp profile that blocks unneeded syscalls. These policies caught two container escape attempts during Penetration Testing.

flowchart TD
    A["Pod Spec"] --> B["Pod SecurityContext"]
    B --> C["RunAsNonRoot: true"]
    B --> D["FSGroup: 1001"]
    B --> E["SeccompProfile: RuntimeDefault"]
    A --> F["Container SecurityContext"]
    F --> G["RunAsUser: 1001"]
    F --> H["Capabilities: DROP ALL"]
    F --> I["ReadOnlyRootFilesystem: true"]
    F --> J["Privileged: false"]
    F --> K["AllowPrivilegeEscalation: false"]
    G --> L["Secure Container"]
    H --> L
    I --> L
    K --> L
    style L fill:#326CE5,color:#fff
â„šī¸ Info

Prerequisites: Basic Kubernetes knowledge, a running cluster, and familiarity with Linux security primitives (users, capabilities, syscalls).

Pod-Level Security Context

Settings applied at the Pod level propagate to all containers unless overridden by container-level settings.

# pod-security-context.yaml
apiVersion: v1
kind: Pod
metadata:
  name: secure-pod
spec:
  securityContext:
    runAsUser: 1001
    runAsGroup: 3001
    fsGroup: 2001
    fsGroupChangePolicy: OnRootMismatch
    supplementalGroups: [4001, 5001]
    seccompProfile:
      type: RuntimeDefault
  containers:
    - name: app
      image: nginx:1.25-alpine
      command: ["sleep", "3600"]

Expected behavior: The Pod runs all containers as user ID 1001, group 3001. Volume mounts are owned by fsGroup 2001. The RuntimeDefault seccomp profile is applied, blocking approximately 40% of system calls that are not needed by containers.

# Apply and verify
kubectl apply -f pod-security-context.yaml
kubectl exec secure-pod -- id

# Expected output:
# uid=1001 gid=3001 groups=3001,2001,4001,5001

kubectl exec secure-pod -- cat /proc/1/status | grep Cap

# Expected output (all caps are dropped):
# CapInh: 0000000000000000
# CapPrm: 0000000000000000
# CapEff: 0000000000000000
# CapBnd: 0000000000000000

Container-Level Security Context

Container-level settings override Pod-level settings for specific containers. The most important fields for security hardening are allowPrivilegeEscalation, privileged, and capabilities.

# container-security-context.yaml
apiVersion: v1
kind: Pod
metadata:
  name: hardened-app
spec:
  containers:
    - name: api
      image: myapp/api:1.0.0
      securityContext:
        allowPrivilegeEscalation: false
        privileged: false
        readOnlyRootFilesystem: true
        runAsNonRoot: true
        runAsUser: 1001
        capabilities:
          drop: ["ALL"]
          add: ["NET_BIND_SERVICE"]
        seccompProfile:
          type: RuntimeDefault
      volumeMounts:
        - name: tmp
          mountPath: /tmp
      ports:
        - containerPort: 8080
  volumes:
    - name: tmp
      emptyDir: {}

Expected behavior: The container cannot escalate privileges, runs as non-root user 1001, has all Linux capabilities dropped except NET_BIND_SERVICE (needed to bind to ports under 1024), and the root filesystem is read-only. The /tmp directory is writable through an emptyDir volume mounted explicitly.

# Test the read-only filesystem
kubectl exec hardened-app -- touch /test

# Expected output:
# touch: /test: Read-only file system

# Test privilege escalation
kubectl exec hardened-app -- sysctl -w net.ipv4.ip_forward=1

# Expected output:
# sysctl: error setting key 'net.ipv4.ip_forward': Permission denied

Pod Security Standards (PSS)

Kubernetes provides three built-in security levels enforced through labels or admission controllers.

Standard Description Key Restrictions
Privileged No restrictions Unrestricted
Baseline Minimal restrictions, prevents known escapes No privileged containers, no hostPID, no ALL capabilities
Restricted Hardened, follows Pod hardening best practices RunAsNonRoot, readOnlyRootFilesystem, all caps dropped, seccomp required
# namespace-pss.yaml
apiVersion: v1
kind: Namespace
metadata:
  name: production
  labels:
    pod-security.kubernetes.io/enforce: restricted
    pod-security.kubernetes.io/enforce-version: latest
    pod-security.kubernetes.io/audit: restricted
    pod-security.kubernetes.io/warn: restricted

Expected behavior: Any Pod created in the production namespace that violates the restricted PSS level is rejected. The audit label logs violations without blocking, and warn sends warnings to the user.

# Attempt to create a privileged Pod in the restricted namespace
kubectl run bad-pod --image=nginx --privileged -n production

# Expected output:
# Error: container nginx has securityContext.privileged=true,
# but the pod must not set privileged=true or other escalated
# privileges as enforced by the PodSecurity annotation
# 'pod-security.kubernetes.io/enforce: restricted'

Custom Security Policies with OPA Gatekeeper

For fine-grained control beyond the built-in PSS levels, use OPA Gatekeeper with ConstraintTemplates.

# k8srequiredlabels.yaml
apiVersion: templates.gatekeeper.sh/v1
kind: ConstraintTemplate
metadata:
  name: k8srequiredlabels
spec:
  crd:
    spec:
      names:
        kind: K8sRequiredLabels
  targets:
    - target: admission.k8s.gatekeeper.sh
      rego: |
        package k8srequiredlabels
        violation[{"msg": msg}] {
          input.request.kind.kind == "Pod"
          not input.request.object.metadata.labels.team
          msg := "All Pods must have a 'team' label"
        }
# require-runasnonroot.yaml
apiVersion: templates.gatekeeper.sh/v1
kind: ConstraintTemplate
metadata:
  name: k8srunasnonroot
spec:
  crd:
    spec:
      names:
        kind: K8sRunAsNonRoot
  targets:
    - target: admission.k8s.gatekeeper.sh
      rego: |
        package k8srunasnonroot
        violation[{"msg": msg}] {
          container := input.request.object.spec.containers[_]
          not container.securityContext.runAsNonRoot
          msg := sprintf("Container %v must set runAsNonRoot: true", [container.name])
        }

Expected behavior: Gatekeeper evaluates all admission requests against these constraints. Any Pod lacking the team label or missing runAsNonRoot: true is rejected before the resource is persisted.

Common Errors

  1. Forgetting to set allowPrivilegeEscalation: false: By default, Kubernetes allows privilege escalation if the container runs as privileged or has CAP_SYS_ADMIN. Without explicitly setting this to false, a compromised container can escalate to root even when started as a non-root user.

  2. ReadOnlyRootFilesystem without writable volume mounts: Many applications write temporary files to /tmp or /var/run. Setting readOnlyRootFilesystem: true without providing emptyDir volumes for these paths causes the application to crash with "read-only file system" errors.

  3. Using runAsUser: 0 or omitting it: When runAsUser is not specified, the container defaults to the user defined in the Dockerfile, which is often root (UID 0). Always set runAsNonRoot: true and specify a non-zero runAsUser or rely on the container image's non-root user.

  4. Overly permissive seccomp or AppArmor profiles: Using Unconfined for seccomp removes all system call filtering, negating one of the most effective container isolation layers. Always use RuntimeDefault at minimum, and consider custom profiles for production.

  5. Mixing PSS enforcement levels across namespaces without testing: Enforcing restricted on a namespace where workloads require host network access, privileged containers, or specific capabilities causes all deployments to fail. Audit existing workloads with audit mode before switching to enforce.

Practice Questions

  1. What is the difference between Pod-level and container-level securityContext? Answer: Pod-level securityContext applies to all containers in the Pod and covers settings like runAsUser, fsGroup, and seccompProfile. Container-level securityContext overrides the Pod-level for that specific container and adds settings like capabilities, privileged, and allowPrivilegeEscalation.

  2. Why is it important to drop all capabilities and add only required ones? Answer: Each Linux capability grants specific privileged operations. Dropping all capabilities and adding only those explicitly needed (like NET_BIND_SERVICE for binding to privileged ports) follows the principle of Least Privilege and reduces the attack surface if the container is compromised.

  3. What does PodSecurityLevel: restricted actually enforce? Answer: The restricted level enforces: runAsNonRoot: true, seccompProfile.type must be RuntimeDefault or Localhost, all capabilities dropped, allowPrivilegeEscalation: false, no privileged containers, no host network/PID/IPC access, and read-only root filesystem. It is the most secure built-in standard.

  4. How does OPA Gatekeeper differ from Pod Security Standards? Answer: PSS provides three fixed levels (Privileged, Baseline, Restricted) enforced via namespace labels. OPA Gatekeeper uses custom ConstraintTemplates written in Rego to enforce arbitrary policies, giving teams fine-grained control beyond what PSS offers.

Challenge

Create a Gatekeeper ConstraintTemplate that enforces: all containers must drop all capabilities, allowPrivilegeEscalation must be false, runAsNonRoot must be true, and the Pod must have a security-tier label. Apply the constraint to the production namespace. Verify that a Pod without these settings is rejected and one with them is accepted. Write a second constraint that requires all images to come from a trusted registry (e.g., myregistry.io/*).

Mini Project

Harden an existing Kubernetes Deployment for a Node.js application: add Pod security context with runAsNonRoot: true and fsGroup: 1001, add container security context with allowPrivilegeEscalation: false, all capabilities dropped, readOnlyRootFilesystem: true, and runAsUser: 1001, mount an emptyDir volume at /tmp, add a RuntimeDefault seccomp profile, verify the application still works with all probes passing, apply the restricted PSS label to the namespace, run kubectl audit to check for violations, and document any exceptions needed.

Resource Description
Kubernetes Pods Pod lifecycle and configuration
Container Security Broader container security topics
DevSecOps Pipeline Integrating security into CI/CD
Secret Management Securing sensitive data in Kubernetes

Built by the developers of Doda Browser, DodaZIP, and Durga Antivirus Pro.

Built by the developers of DodaTech

Doda Browser, DodaZIP & Durga Antivirus Pro