Safeguard
Containers

Kubernetes securityContext: A Practical Hardening Guide

The securityContext in Kubernetes is where most pod hardening actually happens. A field-by-field guide to running non-root, dropping capabilities, and read-only roots.

Marcus Chen
DevSecOps Engineer
5 min read

The securityContext in Kubernetes is the block that controls the privilege and access level of a pod or container, and getting five fields right, non-root user, dropped capabilities, no privilege escalation, read-only root filesystem, and a seccomp profile, eliminates most container-escape risk in a typical deployment. It is set at two levels: spec.securityContext applies to the whole pod, and spec.containers[].securityContext applies to one container and overrides the pod level. Most teams either ignore it entirely or copy a snippet without understanding which field does what. This guide fixes that.

The Fields That Matter Most

Here is a hardened container securityContext in a Kubernetes deployment, annotated:

apiVersion: apps/v1
kind: Deployment
metadata:
  name: api
spec:
  template:
    spec:
      securityContext:
        runAsNonRoot: true
        runAsUser: 10001
        runAsGroup: 10001
        fsGroup: 10001
        seccompProfile:
          type: RuntimeDefault
      containers:
        - name: api
          image: myorg/api:2.3.1
          securityContext:
            allowPrivilegeEscalation: false
            readOnlyRootFilesystem: true
            capabilities:
              drop: ["ALL"]

Field by field:

  • runAsNonRoot: true makes the kubelet refuse to start the container if its image would run as UID 0. This is the single highest-value setting. If an attacker gets code execution in a root container, they start with far more inside the container to work with.
  • runAsUser / runAsGroup pin a specific non-zero UID/GID. Set these because many images default to root even when they do not need it.
  • allowPrivilegeEscalation: false blocks a process from gaining more privileges than its parent via setuid binaries or file capabilities. It sets the no_new_privs process flag.
  • readOnlyRootFilesystem: true mounts the container's root filesystem read-only. Attackers can no longer drop tools or modify binaries. Where the app genuinely needs to write (temp files, caches), mount an emptyDir volume at that path.
  • capabilities.drop: ["ALL"] removes every Linux capability, then you add back only what the process needs (rarely anything for a typical web service). A web server binding port 80 needs NET_BIND_SERVICE, but binding a high port and letting the Service map it avoids even that.
  • seccompProfile.type: RuntimeDefault applies the container runtime's default syscall filter. It is off unless you ask for it, which surprises people.

Pod-Level vs Container-Level

The distinction trips up newcomers. fsGroup, runAsNonRoot, and runAsUser at the pod level set defaults for all containers, and fsGroup additionally controls group ownership of mounted volumes so a non-root process can read them. Container-level settings override pod-level ones and are the only place for readOnlyRootFilesystem, allowPrivilegeEscalation, and capabilities. Set the safe baseline at the pod level, then override per container only where necessary.

The readOnlyRootFilesystem Reality

This is the field teams enable, watch the pod crash-loop, and then disable in frustration. The fix is almost always writable volumes for the specific paths that need them:

          volumeMounts:
            - name: tmp
              mountPath: /tmp
            - name: cache
              mountPath: /var/cache/app
      volumes:
        - name: tmp
          emptyDir: {}
        - name: cache
          emptyDir: {}

Find the write paths by running the app once without the read-only flag and watching for permission errors, or check the application's documentation. It is a one-time cost that buys real hardening.

From Pod Security Policy to Pod Security Admission

If you learned Kubernetes a few years ago you knew PodSecurityPolicy (PSP). PSP was deprecated in Kubernetes 1.21 and removed in 1.25. Its replacement is Pod Security Admission (PSA), a built-in admission controller that enforces three standard profiles at the namespace level:

  • privileged — no restrictions.
  • baseline — blocks the obviously dangerous (privileged pods, host namespaces, most capabilities beyond a small default set).
  • restricted — the hardened profile, requiring runAsNonRoot, allowPrivilegeEscalation: false, dropped ALL capabilities, a seccomp profile, and more.

You apply it with namespace labels:

apiVersion: v1
kind: Namespace
metadata:
  name: production
  labels:
    pod-security.kubernetes.io/enforce: restricted
    pod-security.kubernetes.io/warn: restricted

The restricted profile is essentially a checklist of the securityContext fields above. If your manifests satisfy restricted, PSA is a formality; if they do not, PSA tells you exactly which field is missing. For finer-grained rules than the three built-in profiles, teams reach for Kyverno or OPA Gatekeeper, but start with PSA because it ships in the box. Older references to "pod security policy in Kubernetes" almost always mean this PSA model now.

Rolling It Out Without an Outage

Do not flip enforce: restricted on a live namespace. The migration path:

  1. Add warn and audit labels first. Kubernetes logs violations without blocking anything, giving you an inventory of non-compliant workloads.
  2. Fix manifests one deployment at a time. Add the securityContext block, rebuild images to run as non-root where needed, add writable volumes.
  3. Only then set enforce. Because you already fixed everything, nothing breaks.

Bake the checks earlier too. Scanning Kubernetes manifests in CI catches a missing runAsNonRoot or a privileged: true before it reaches the cluster, and a container and IaC scanning step reports these as misconfiguration findings alongside image CVEs. The Academy has a longer walkthrough if you want the full progression from baseline to restricted.

FAQ

What is securityContext in Kubernetes?

It is the configuration block that controls the privilege and access level of a pod or container: which user it runs as, whether it can escalate privileges, which Linux capabilities it holds, whether its root filesystem is writable, and which seccomp profile applies.

What is the difference between pod-level and container-level securityContext?

Pod-level securityContext sets defaults for all containers and controls volume ownership via fsGroup. Container-level securityContext overrides pod-level values and is the only place for readOnlyRootFilesystem, allowPrivilegeEscalation, and capabilities.

Is PodSecurityPolicy still available in Kubernetes?

No. PodSecurityPolicy was deprecated in Kubernetes 1.21 and removed in 1.25. The built-in replacement is Pod Security Admission, which enforces privileged, baseline, and restricted profiles per namespace via labels.

What are the most important securityContext settings?

Set runAsNonRoot: true, pin a non-zero runAsUser, use allowPrivilegeEscalation: false, drop all capabilities, enable readOnlyRootFilesystem with writable volumes for the paths that need them, and apply seccompProfile: RuntimeDefault.

Never miss an update

Weekly insights on software supply chain security, delivered to your inbox.