Kubernetes Pod Security Contexts and Policies â Secure Container Hardening Guide
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
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
Forgetting to set
allowPrivilegeEscalation: false: By default, Kubernetes allows privilege escalation if the container runs as privileged or hasCAP_SYS_ADMIN. Without explicitly setting this tofalse, a compromised container can escalate to root even when started as a non-root user.ReadOnlyRootFilesystem without writable volume mounts: Many applications write temporary files to
/tmpor/var/run. SettingreadOnlyRootFilesystem: truewithout providingemptyDirvolumes for these paths causes the application to crash with "read-only file system" errors.Using
runAsUser: 0or omitting it: WhenrunAsUseris not specified, the container defaults to the user defined in the Dockerfile, which is often root (UID 0). Always setrunAsNonRoot: trueand specify a non-zerorunAsUseror rely on the container image's non-root user.Overly permissive seccomp or AppArmor profiles: Using
Unconfinedfor seccomp removes all system call filtering, negating one of the most effective container isolation layers. Always useRuntimeDefaultat minimum, and consider custom profiles for production.Mixing PSS enforcement levels across namespaces without testing: Enforcing
restrictedon a namespace where workloads require host network access, privileged containers, or specific capabilities causes all deployments to fail. Audit existing workloads withauditmode before switching toenforce.
Practice Questions
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, andseccompProfile. Container-level securityContext overrides the Pod-level for that specific container and adds settings likecapabilities,privileged, andallowPrivilegeEscalation.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_SERVICEfor binding to privileged ports) follows the principle of Least Privilege and reduces the attack surface if the container is compromised.What does
PodSecurityLevel: restrictedactually enforce? Answer: The restricted level enforces:runAsNonRoot: true,seccompProfile.typemust beRuntimeDefaultorLocalhost, 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.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.
Related Resources
| 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