Skip to content
Security context

Security context

By default a container runs as root, with a broad set of Linux capabilities, and writes wherever it likes in its own filesystem. That is convenient for building images and dangerous for running them.

A security context is where you take those defaults away. Most of the benefit comes from three settings, and they cost nothing when your image is built for them.

Reference

Terminology

  • runAsNonRoot: true — Refuses to start the container if the image would run as root. A refusal at admission is much better than a surprise at runtime.
  • runAsUser / runAsGroup — The numeric UID and GID to run as. Numbers, not names: the kubelet cannot resolve users from /etc/passwd in an image it has not started.
  • fsGroup — The group applied to mounted volumes, so a non-root process can write to them. This is the usual reason a non-root container fails to start on a volume.
  • readOnlyRootFilesystem: true — The container’s own filesystem becomes read-only. Anything that needs to write must use a mounted volume.
  • Capabilities — The fine-grained privileges that make up “root”. Dropping ALL and adding back only what is needed — NET_BIND_SERVICE for a port below 1024, for example — is the tightest common configuration.
  • allowPrivilegeEscalation: false — Blocks setuid binaries and similar routes to more privilege than the process started with.
  • seccompProfile — Restricts the system calls the container may make. RuntimeDefault is the safe, portable choice.

Pod Level Versus Container Level

securityContext appears in two places, and settings do not always exist in both:

spec:
  securityContext:            # applies to the Pod
    runAsNonRoot: true
    runAsUser: 10001
    fsGroup: 10001
    seccompProfile:
      type: RuntimeDefault
  containers:
    - name: app
      image: docker.io/library/nginx:1.27
      securityContext:        # applies to this container
        allowPrivilegeEscalation: false
        readOnlyRootFilesystem: true
        capabilities:
          drop: ["ALL"]

Pod-level settings apply to every container as a default; container-level settings win where they overlap. fsGroup and runAsNonRoot are Pod-level; capabilities and readOnlyRootFilesystem are container-level. Setting one in the wrong place is silently ignored, which is worth knowing before you conclude that a setting “does not work”.

Exercises

  1. See the default. Run a container and look at who you are:

    kubectl run whoami --image=docker.io/library/busybox --restart=Never -- sleep 3600
    kubectl exec whoami -- id

    uid=0(root). This is what runs in most clusters unless someone changes it.

  2. Make it non-root.

    spec:
      securityContext:
        runAsNonRoot: true
        runAsUser: 10001

    The container now reports a non-zero uid — unless the image’s own USER instruction conflicts, in which case runAsNonRoot refuses to start it and the error says so. That error is the feature.

  3. Break an application with a read-only root filesystem, then fix it. Set readOnlyRootFilesystem: true on something that writes to its own directory. It fails. Now give it somewhere to write:

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

    This is the whole pattern: a read-only root plus explicit writable volumes is both more secure and a precise description of what the application actually needs to write.

  4. Drop capabilities and keep one. Drop ALL, then add NET_BIND_SERVICE back and run a container listening on port 80. Remove it and watch the bind fail.

Pod Security Admission

Enforcing this by hand across every workload does not scale, so clusters apply Pod Security Admission with labels on the namespace:

Label valueWhat it enforces
privilegedNothing. The default before anyone thinks about it
baselineBlocks the obvious dangers: host networking, host paths, privileged containers
restrictedThe hardened set: non-root, no privilege escalation, dropped capabilities, seccomp
metadata:
  labels:
    pod-security.kubernetes.io/enforce: restricted
    pod-security.kubernetes.io/warn: restricted

warn lets you see what would break before enforce breaks it, which is how you roll this out to an existing cluster without a maintenance window. This is covered properly on Day 4.

Gotchas Worth Knowing

  • runAsNonRoot with no runAsUser makes the kubelet inspect the image’s USER; if it cannot tell, the Pod is rejected. Being explicit is more predictable.
  • fsGroup is usually required for a non-root process to write to a mounted volume — the volume arrives owned by root until you say otherwise.
  • Ports below 1024 need a capability, unless you set net.ipv4.ip_unprivileged_port_start or run as root. NET_BIND_SERVICE is the narrow fix.
  • A setting in the wrong scope is ignored without an error. Check the API reference for where each field lives.
  • privileged: true is not a security context tweak — it is a container with the host’s capabilities. Treat it as a red flag in review.

What to Take Away

  • The default is root with broad capabilities. Say what you want instead, explicitly.
  • runAsNonRoot, readOnlyRootFilesystem and capabilities: drop: [ALL] are the three that pay for themselves immediately.
  • fsGroup and writable emptyDir mounts are what make hardened containers actually work.
  • Enforce it with Pod Security Admission rather than by remembering.