Back to Kubernetes: Core Concepts
Q7Question 7 of 16

Enforcing Least Privilege at the Pod Level and Why Running as Root Breaches It

Key Concepts
  • runAsNonRoot: true makes the kubelet refuse to start a container that would run as UID 0.
  • readOnlyRootFilesystem: true makes the container's root filesystem immutable, preventing an attacker from dropping malicious binaries.
  • Drop ALL Linux capabilities and add back only the specific ones the workload genuinely needs (usually none).
  • allowPrivilegeEscalation: false blocks setuid-based escalation so a child process cannot gain more privileges.
  • Pod Security Admission enforces these rules cluster-wide using the restricted profile at admission time.
  • Running as root breaches least privilege because root inside a container can escalate to node-level control through a kernel or runtime vulnerability.

Definition

Give a container only the permissions it needs via securityContext, enforce it cluster-wide with the restricted Pod Security profile, and never run as root because root plus one exploit means node takeover.

Least privilege at the Pod level means a container runs with only the permissions it truly needs.

  • runAsNonRoot / runAsUser – refuse to start as UID 0; pin to a specific non-zero user.
  • readOnlyRootFilesystem – make the container's filesystem immutable; write only to mounted volumes.
  • capabilities – drop ALL and add back only what is genuinely required.
  • allowPrivilegeEscalation: false – block setuid-based privilege gain.
  • Pod Security Admission – cluster-wide enforcement using the restricted profile so non-compliant Pods are rejected at admission.
  • Why root is dangerous – a container shares the host kernel; root inside plus a vulnerability means node-level control.

At the National Bank of India, least privilege means a junior clerk gets a key that opens only their own drawer, not the vault, the server room, and the CEO's office. The securityContext is the access-control form that says which drawer the clerk may open and bans them from upgrading their own badge. Pod Security Admission is the security desk that refuses to let anyone in who demands master keys. Running as root is handing every new clerk the building master key on day one.

Understanding the Concepts

What is a securityContext?

The securityContext is set at the Pod level and can be overridden per container. Several fields together implement least privilege.

YAML
# Fully hardened Pod for the transfer service
apiVersion: apps/v1
kind: Deployment
metadata:
  name: transfer-service
spec:
  template:
    spec:
      securityContext:
        runAsNonRoot: true
        runAsUser: 10001
        fsGroup: 10001
      containers:
        - name: transfer
          image: nbi-registry.example.com/transfer-service:2.0.0
          securityContext:
            readOnlyRootFilesystem: true
            allowPrivilegeEscalation: false
            capabilities:
              drop: ["ALL"]
          volumeMounts:
            - name: tmp
              mountPath: /tmp
      volumes:
        - name: tmp
          emptyDir: {}   # writable scratch space for temp files

What does runAsNonRoot do?

runAsNonRoot: true makes the kubelet refuse to start a container whose image would run as UID 0. Combined with runAsUser: 10001, the process is pinned to an unprivileged account.

YAML
# Pod-level: applies to all containers
securityContext:
  runAsNonRoot: true
  runAsUser: 10001      # specific non-zero UID
  runAsGroup: 10001
  fsGroup: 10001
BASH
# If the Dockerfile uses USER root, the kubelet rejects the Pod
kubectl describe pod transfer-service-x9k
# Error: container has runAsNonRoot and image will run as root

What does readOnlyRootFilesystem do?

readOnlyRootFilesystem: true makes the container's root filesystem immutable. An attacker cannot drop a malicious binary, tamper with application code, or modify configuration. The application writes only to explicitly mounted writable volumes.

YAML
# Container cannot write to its root filesystem
securityContext:
  readOnlyRootFilesystem: true

# But the app needs /tmp for temporary files
volumeMounts:
  - name: tmp
    mountPath: /tmp
volumes:
  - name: tmp
    emptyDir: {}
BASH
# Attempt to write to the root filesystem
kubectl exec transfer-service-x9k -- touch /malicious
# touch: /malicious: Read-only file system

# Writing to /tmp works because it is a mounted writable volume
kubectl exec transfer-service-x9k -- touch /tmp/tempfile
# (success)

How do you manage Linux capabilities?

Linux capabilities are a finer-grained split of root's powers. Drop all of them and add back only the specific ones the workload genuinely needs. For a typical banking microservice, the answer is none.

YAML
# Drop all capabilities, add none back
securityContext:
  capabilities:
    drop: ["ALL"]
    # add: ["NET_BIND_SERVICE"]   # only if the app binds to port < 1024
BASH
# Verify capabilities inside the container
kubectl exec transfer-service-x9k -- cat /proc/1/status | grep Cap
# CapPrm: 0000000000000000   <- no capabilities
# CapEff: 0000000000000000   <- no capabilities

What does allowPrivilegeEscalation do?

allowPrivilegeEscalation: false sets the Linux no_new_privs flag so a child process cannot gain more privileges than its parent. This blocks setuid-based escalation attacks.

YAML
securityContext:
  allowPrivilegeEscalation: false

How do you enforce this cluster-wide?

Relying on every team to remember to set securityContext is fragile. Pod Security Admission is built into the Kubernetes API server and enforces rules at admission time.

There are three profiles:

| Profile | Description |
|---|---|
| privileged | No restrictions at all |
| baseline | Blocks known privilege escalations |
| restricted | Requires runAsNonRoot, dropped capabilities, no escalation, restricted seccomp |

And three enforcement modes:

| Mode | Behaviour |
|---|---|
| enforce | Rejects violating Pods |
| audit | Records violations in audit log |
| warn | Returns a warning to the user |

BASH
# Label the banking namespace with the restricted profile
kubectl label namespace banking \
  pod-security.kubernetes.io/enforce=restricted \
  pod-security.kubernetes.io/audit=restricted \
  pod-security.kubernetes.io/warn=restricted

# Now any Pod that tries to run as root is rejected
kubectl apply -f root-pod.yaml -n banking
# Error: pods "root-pod" is forbidden:
#   violates PodSecurity "restricted":
#   allowPrivilegeEscalation != false,
#   runAsNonRoot != true

Why does running as root breach least privilege?

A container is not a strong security boundary like a virtual machine. It shares the host kernel. Root inside the container is the same UID 0 the kernel recognizes. If an attacker escapes the container through a kernel bug, a misconfigured mount, or an added capability like CAP_SYS_ADMIN, they are root on the node and can read every other Pod's secrets, volumes, and kubelet credentials.

BASH
# WRONG - container runs as root
# A code injection vulnerability + kernel exploit = node takeover
kubectl exec root-pod -- id
# uid=0(root) gid=0(root)  <- full root privileges

# CORRECT - container runs as non-root with no capabilities
kubectl exec transfer-service-x9k -- id
# uid=10001 gid=10001 groups=10001  <- limited unprivileged user

What are common mistakes?

The most common mistake is leaving securityContext at defaults, which means the container may run as root with capabilities and a writable filesystem.

YAML
# WRONG - no securityContext at all
apiVersion: v1
kind: Pod
spec:
  containers:
    - name: transfer
      image: nbi-registry.example.com/transfer-service:2.0.0
      # runs as whatever user the Dockerfile specifies (often root)
      # writable filesystem, default capabilities, escalation allowed

# CORRECT - fully hardened
apiVersion: v1
kind: Pod
spec:
  securityContext:
    runAsNonRoot: true
    runAsUser: 10001
  containers:
    - name: transfer
      image: nbi-registry.example.com/transfer-service:2.0.0
      securityContext:
        readOnlyRootFilesystem: true
        allowPrivilegeEscalation: false
        capabilities:
          drop: ["ALL"]

What about policy engines like Kyverno or OPA Gatekeeper?

Pod Security Admission covers the standard profiles. For custom organizational rules, layer a policy engine on top.

YAML
# Kyverno policy: require specific labels on all Pods
apiVersion: kyverno.io/v1
kind: ClusterPolicy
metadata:
  name: require-team-label
spec:
  validationFailureAction: Enforce
  rules:
    - name: check-team-label
      match:
        any:
          - resources:
              kinds: ["Pod"]
      validate:
        message: "Every Pod must have a 'team' label."
        pattern:
          metadata:
            labels:
              team: "?*"

What should a senior engineer know?

The blast radius of a compromised container is the key concept. Running as non-root with dropped capabilities and no escalation means that even a full application compromise is contained to a weak identity that cannot pivot to the node. In a multi-tenant banking cluster, that containment is the difference between a single compromised service and a cluster-wide breach.

BASH
# Audit all Pods for security compliance
kubectl get pods -A -o json | jq -r '
  .items[] | select(
    .spec.containers[].securityContext.runAsNonRoot != true
  ) | .metadata.namespace + "/" + .metadata.name
'

Visual Overview

DIAGRAM
  Pod Security Layers
  =====================

  Cluster Level
  ┌────────────────────────────────────────────┐
  │  Pod Security Admission (restricted)        │
  │  Rejects Pods that violate the profile      │
  │  + Kyverno / OPA Gatekeeper (custom rules)  │
  └────────────────────┬───────────────────────┘
                       │ Pod passes admission
                       ▼
  Pod Level
  ┌────────────────────────────────────────────┐
  │  securityContext:                           │
  │    runAsNonRoot: true                       │
  │    runAsUser: 10001                         │
  └────────────────────┬───────────────────────┘
                       │
                       ▼
  Container Level
  ┌────────────────────────────────────────────┐
  │  securityContext:                           │
  │    readOnlyRootFilesystem: true             │
  │    allowPrivilegeEscalation: false          │
  │    capabilities:                            │
  │      drop: ["ALL"]                          │
  └────────────────────────────────────────────┘


  Blast Radius: Root vs Non-Root
  ================================

  Running as root + kernel exploit:
  ┌──────────┐     ┌──────────┐     ┌──────────┐
  │Container │ →→→ │  Node    │ →→→ │ Cluster  │
  │(root)    │     │(root)    │     │(all Pods)│
  └──────────┘     └──────────┘     └──────────┘
  Compromise expands to entire cluster

  Running as non-root + no capabilities:
  ┌──────────┐
  │Container │  ← contained here
  │(uid 10001│
  │ no caps) │
  └──────────┘
  Compromise stays in one weak container

Key Takeaway

Least privilege at the Pod level means a container runs with only the permissions it genuinely needs. Set runAsNonRoot to true and pin a specific non-zero user ID. Mount the root filesystem read-only. Drop all Linux capabilities and add back none. Set allowPrivilegeEscalation to false. Together these shrink the blast radius so that even a full application compromise is contained to a weak, unprivileged identity.

Relying on every team to remember these settings is fragile, so enforce them cluster-wide with Pod Security Admission. Label the namespace with the restricted profile in enforce mode, and any Pod that tries to run as root or keep extra capabilities is rejected before it ever runs.

The reason running as root breaches least privilege is that a container shares the host kernel. Root inside the container is the same UID 0 the kernel recognizes, so a kernel bug or a dangerous capability lets an attacker escape to the node and compromise the entire cluster.

Remember one sentence: run as non-root, drop all capabilities, read-only filesystem, and enforce the restricted profile so no team can ever ship a privileged Pod.

---

Share
Use ← → arrow keys to navigate