HackBlue

Tools · Containers and Kubernetes security · 05 / 20

Hardening pods with securityContext: runAsNonRoot, read-only root filesystem, and capabilities

Write a pod spec that meets the restricted standard, understand what each securityContext field changes, and fix the common reasons hardened containers fail to start.

10 min read · practice only with authorization

The fields that matter

runAsNonRoot: true makes the kubelet refuse to start a container whose user is UID 0. runAsUser sets a specific UID. allowPrivilegeEscalation: false sets no_new_privs so setuid binaries cannot gain privileges. capabilities.drop: [ALL] removes Linux capabilities such as NET_RAW and CHOWN. readOnlyRootFilesystem: true makes the image filesystem immutable at runtime. seccompProfile type RuntimeDefault applies the runtime’s default system call filter.

A restricted-friendly spec

Pod-level settings apply to all containers; container-level settings override them. Writable paths the app needs, such as /tmp or a cache directory, become emptyDir volumes.

Snippet
apiVersion: v1
kind: Pod
metadata:
  name: api
spec:
  automountServiceAccountToken: false
  securityContext:
    runAsNonRoot: true
    runAsUser: 10001
    seccompProfile:
      type: RuntimeDefault
  containers:
    - name: api
      image: ghcr.io/example/api:1.4.2
      securityContext:
        allowPrivilegeEscalation: false
        readOnlyRootFilesystem: true
        capabilities:
          drop: ["ALL"]
      volumeMounts:
        - name: tmp
          mountPath: /tmp
  volumes:
    - name: tmp
      emptyDir: {}

Lab: harden nginx step by step

Deploy the stock nginx image with the hardened securityContext and watch it fail: it wants to run as root, bind port 80, and write to /var/cache/nginx. Switch to the nginxinc/nginx-unprivileged image, which listens on 8080 as a non-root user, and add emptyDir volumes for /tmp and /var/cache/nginx. Check the result with kubectl describe and confirm inside the container that id shows a non-zero UID and that touching a file in / fails.

Snippet
kubectl exec api -- id
kubectl exec api -- touch /test   # expect: Read-only file system
kubectl exec api -- grep Cap /proc/1/status

Common failures

CreateContainerConfigError with “container has runAsNonRoot and image will run as root” means the image has no USER instruction or uses a name instead of a UID; set runAsUser. Crash loops after enabling readOnlyRootFilesystem usually mean the app writes somewhere; read the logs for the path and mount an emptyDir there. Apps binding ports below 1024 need a higher port when capabilities are dropped.

Diagram

Default container versus hardened container

Each field removes a specific power.

Default

User. Often root (UID 0)

Capabilities. Runtime default set, including NET_RAW

Filesystem. Writable image layer

Hardened

User. UID 10001, runAsNonRoot

Capabilities. None (drop ALL)

Filesystem. Read-only, emptyDir for /tmp

Check yourself

1. A container fails with “image will run as root” under runAsNonRoot. What is the usual fix?

2. After enabling readOnlyRootFilesystem, the app crashes writing to /var/cache/app. What should you do?

3. Which field stops setuid binaries from gaining extra privileges?