Safeguard
Containers

k8s securityContext: How to Lock Down Kubernetes Pods and Containers

The k8s securityContext is your first real control over what a container can do at runtime. A field-by-field guide to a hardened, non-root pod spec.

Karan Patel
Platform Engineer
6 min read

The k8s securityContext is the block in a Pod or container spec that controls the runtime privileges of the workload, and setting it correctly is the difference between a container breakout being a full node compromise and being a non-event. By default Kubernetes runs containers with far more privilege than most workloads need: often as root, with a broad set of Linux capabilities, and a writable filesystem. The securityContext is where you claw that back to least privilege.

If you have been deploying without one, this is the highest-leverage hardening you can apply, and it costs a few lines of YAML.

securityContext Lives at Two Levels

There are two securityContext blocks, and they do different things. The Pod-level spec.securityContext sets defaults that apply to all containers and controls a few Pod-wide settings like the filesystem group. The container-level spec.containers[].securityContext applies to a single container and overrides the Pod-level values where they overlap.

The general pattern is to set the safe defaults at the Pod level and only override per-container when one container genuinely needs something different. Here is a hardened baseline:

apiVersion: v1
kind: Pod
metadata:
  name: api
spec:
  securityContext:
    runAsNonRoot: true
    runAsUser: 10001
    runAsGroup: 10001
    fsGroup: 10001
    seccompProfile:
      type: RuntimeDefault
  containers:
    - name: api
      image: registry.example.com/api@sha256:...
      securityContext:
        allowPrivilegeEscalation: false
        readOnlyRootFilesystem: true
        capabilities:
          drop:
            - ALL

Every field in that spec is closing a specific door. Let me go through the ones that matter most.

Run as Non-Root

runAsNonRoot: true tells the kubelet to refuse to start the container if its image would run as UID 0. runAsUser and runAsGroup pin the exact identity. This is the most important single setting, because a process running as root inside the container is root on the host kernel if it ever escapes the namespace. A great many container breakout techniques depend on being root inside the container first.

There is a catch worth knowing: runAsNonRoot validates the running user but relies on the image being built for a non-root UID. If your Dockerfile does not set a USER, you have to supply runAsUser explicitly, and the application has to actually work without root, which means not binding to privileged ports below 1024 and not writing to root-owned paths. Building images non-root from the start makes this painless.

Drop Capabilities and Block Privilege Escalation

Linux capabilities slice up root's powers into discrete privileges. Containers get a default set that is broader than almost any application needs. The right move is to drop everything and add back only what is required:

capabilities:
  drop:
    - ALL
  add:
    - NET_BIND_SERVICE   # only if you truly must bind a low port

Most workloads need nothing added. Pair this with allowPrivilegeEscalation: false, which sets the no_new_privs flag so a process cannot gain more privileges than its parent through setuid binaries. That single flag defeats a whole family of local escalation tricks.

And to be explicit about the opposite: never set privileged: true unless you are running something like a node-level agent that genuinely needs it. A privileged container effectively disables the isolation between it and the host.

Read-Only Root Filesystem

readOnlyRootFilesystem: true mounts the container's root filesystem read-only. This is one of the most underused settings and one of the most effective, because a huge amount of post-exploitation depends on writing to disk: dropping a tool, modifying a binary, persisting a foothold. If the filesystem is read-only, that playbook stops working.

Applications that need scratch space still work; you give them a writable emptyDir volume mounted at the specific path they need, such as /tmp, and leave the rest read-only. It takes a little discovery to find which paths an app writes to, but the result is a container that is far harder to tamper with at runtime.

seccomp and Filtering Syscalls

seccompProfile: { type: RuntimeDefault } applies the container runtime's default seccomp profile, which blocks a set of dangerous and rarely-needed syscalls. This has been a sane default for years, yet plenty of clusters still run with seccomp unconfined because nobody set the field. Turning on RuntimeDefault is nearly free and shrinks the kernel attack surface the container can reach. For sensitive workloads you can go further with a custom Localhost profile tuned to the syscalls your app actually issues.

Enforcing It Across the Cluster

Setting securityContext on one Pod is good; guaranteeing every Pod is compliant is the real goal, because the one deployment that forgot it is the one that gets exploited. Kubernetes ships Pod Security Standards with three levels (privileged, baseline, and restricted) enforced by the built-in Pod Security Admission controller. Labeling a namespace to enforce the restricted profile rejects Pods that run as root, allow privilege escalation, or fail to drop capabilities:

apiVersion: v1
kind: Namespace
metadata:
  name: prod
  labels:
    pod-security.kubernetes.io/enforce: restricted
    pod-security.kubernetes.io/enforce-version: latest

For finer-grained policy than the built-in standards offer, admission tools like Kyverno or OPA Gatekeeper let you write and enforce custom rules, such as requiring a specific fsGroup range or forbidding certain images. The principle either way is the same: make the secure configuration mandatory, not a thing developers remember to add.

Where the Image Fits In

A locked-down securityContext constrains what a container can do, but it does not fix a container that ships known-vulnerable packages. Runtime hardening and image hygiene are two halves of the same job: the securityContext limits blast radius, while scanning limits how likely an initial foothold is in the first place. An SCA and container-scanning tool such as Safeguard can flag vulnerable layers and dependencies before an image is deployed, so the workload you are hardening is not carrying a known exploit in the first place. Our security academy covers the container hardening lifecycle if you want the fuller picture.

FAQ

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

Pod-level securityContext sets defaults for all containers and controls Pod-wide settings like fsGroup. Container-level securityContext applies to a single container and overrides the Pod-level values where they overlap. Set safe defaults at the Pod level and override sparingly.

What is the most important securityContext setting?

runAsNonRoot: true (with an explicit runAsUser) is the highest-impact single setting, because root inside a container becomes root on the host kernel if the container is ever escaped. Combine it with dropping all capabilities and allowPrivilegeEscalation: false.

Does readOnlyRootFilesystem break applications?

Only if they write to the root filesystem. Mount a writable emptyDir volume at the specific paths an app needs, such as /tmp, and keep the rest read-only. Most well-behaved applications work fine with a small amount of scratch space.

How do I enforce securityContext across a whole cluster?

Use Kubernetes Pod Security Admission to enforce the restricted Pod Security Standard on namespaces, which rejects non-compliant Pods. For custom rules, add an admission policy engine such as Kyverno or OPA Gatekeeper.

Never miss an update

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