Skip to content

Decision Tree: Container Running as Root

Category: Security Response Starting Question: "I found a container running as root — what's the risk and what do I do?" Estimated traversal: 2-5 minutes Domains: security, kubernetes, containers, devops


The Tree

I found a container running as root — what's the risk and what do I do?
│
├── Is this in production, staging, or development?
│   │
│   ├── Development / local / CI runner only
│   │   └── Lower urgency — but still fix it so bad habits don't reach prod
│   │       → Document exception + schedule fix; skip to "Fix with image change"
│   │
│   └── Production or staging — proceed with full triage below
│
├── Is the container privileged? (`--privileged` flag or `privileged: true` in securityContext)
│   ```bash
│   # Check all pods for privileged containers
│   kubectl get pods --all-namespaces -o json | \
│     jq -r '.items[] | select(.spec.containers[].securityContext.privileged==true) |
│             "\(.metadata.namespace)/\(.metadata.name)"'
│
│   # Check a specific pod
│   kubectl get pod <pod> -o jsonpath='{.spec.containers[*].securityContext.privileged}'
│   ```
│   │
│   ├── YES — container is privileged
│   │   │
│   │   ├── Does it also mount the host filesystem or Docker socket?
│   │   │   ```bash
│   │   │   kubectl get pod <pod> -o json | jq '.spec.volumes[] | select(.hostPath)'
│   │   │   kubectl get pod <pod> -o json | \
│   │   │     jq '.spec.volumes[] | select(.hostPath.path=="/var/run/docker.sock")'
│   │   │   ```
│   │   │   │
│   │   │   ├── YES — privileged + host mount
│   │   │   │   └── ✅ ACTION: CRITICAL — Escalate + Contain
│   │   │   │       This is full host escape capability.
│   │   │   │       An attacker with shell in this container owns the node.
│   │   │   │
│   │   │   └── NO — privileged but no host mount
│   │   │       └── Still critical — privileged containers can use many kernel exploits
│   │   │           └── ✅ ACTION: Escalate + Fix (High Priority)
│   │   │               Remove privileged flag; find why it was needed
│   │   │
│   │   └── Why does the container need privileged mode?
│   │       (Network tools? sysctl changes? Direct hardware access? Legacy reason?)
│   │       │
│   │       ├── No clear reason — someone added it for convenience
│   │       │   └── Remove immediately; it was never needed
│   │       │
│   │       └── Legitimate reason (e.g., node agent, CNI plugin, eBPF tool)
│   │           └── Document the exception formally with security team sign-off
│   │               Add admission controller rule to limit to known namespaces
│   │
│   └── NO — container is not privileged (just running as UID 0)
│       │
│       ├── Does it mount the host filesystem?
│       │   ```bash
│       │   kubectl get pod <pod> -o json | \
│       │     jq '.spec.volumes[] | select(.hostPath) | .hostPath.path'
│       │   ```
│       │   │
│       │   ├── YES — hostPath mount
│       │   │   │
│       │   │   ├── Is the mount path a critical system path?
│       │   │   │   (/, /etc, /var/run, /proc, /sys, /root, /home, /usr)
│       │   │   │   │
│       │   │   │   ├── YES — mounts critical host path
│       │   │   │   │   └── ✅ ACTION: Escalate + Fix (High Priority)
│       │   │   │   │       Root in container + write access to host path = host escape
│       │   │   │   │
│       │   │   │   └── NO — mounts non-critical path (e.g., /tmp/hostdir, /data/logs)
│       │   │   │       └── Moderate risk — fix root user issue anyway
│       │   │   │           → ✅ ACTION: Fix Now — add runAsNonRoot
│       │   │   │
│       │   │   └── Is the mount read-only?
│       │   │       `kubectl get pod <pod> -o json | jq '.spec.containers[].volumeMounts[] | select(.readOnly)'`
│       │   │       │
│       │   │       ├── readOnly: true — risk significantly reduced
│       │   │       │   └── ✅ ACTION: Fix the root user issue; mount risk is mitigated
│       │   │       │
│       │   │       └── Not read-only — writable host mount
│       │   │           └── ✅ ACTION: Fix Now — both root user and mount policy
│       │   │
│       │   └── NO — no host mounts
│       │       └── Non-privileged root in a container with no host access
│       │           → Lower blast radius, but still needs fixing
│       │
│       ├── Does the container need root for a legitimate reason?
│       │   (Binding port < 1024? Volume ownership on root-owned paths?
│       │    Running a legacy init system? Network namespace manipulation?)
│       │   │
│       │   ├── YES — legitimate need for root
│       │   │   │
│       │   │   ├── Can the need be addressed without root?
│       │   │   │   - Port < 1024: use `CAP_NET_BIND_SERVICE` capability only
│       │   │   │   - File ownership: use `initContainer` to chown, then drop to non-root
│       │   │   │   - Init system: use tini or dumb-init as non-root
│       │   │   │   │
│       │   │   │   ├── YES — workaround exists
│       │   │   │   │   └── ✅ ACTION: Fix with Capability Drop + Non-Root User
│       │   │   │   │
│       │   │   │   └── NO — genuinely requires root (rare)
│       │   │   │       └── ✅ ACTION: Document Exception Formally
│       │   │   │           Apply compensating controls (read-only root FS,
│       │   │   │           seccomp profile, AppArmor, NetworkPolicy)
│       │   │   │
│       │   │   └── Is there a working non-root alternative?
│       │   │       (Official non-root image variant? Distroless? UBI minimal?)
│       │   │       │
│       │   │       ├── YES → ✅ ACTION: Switch to non-root image variant
│       │   │       │
│       │   │       └── NO → Modify Dockerfile to add non-root user
│       │   │
│       │   └── NO — no legitimate reason for root
│       │       │
│       │       ├── Is it a simple fix (image already supports non-root)?
│       │       │   `docker inspect <image> | jq '.[].Config.User'`
│       │       │   │
│       │       │   ├── User already set in image (e.g., "1000" or "appuser")
│       │       │   │   └── ✅ ACTION: Fix Now — add securityContext to manifest
│       │       │   │       Just add runAsNonRoot + runAsUser to pod spec
│       │       │   │
│       │       │   └── User not set in image — image runs as root by default
│       │       │       └── ✅ ACTION: Fix with Image Change
│       │       │           Modify Dockerfile to add non-root user
│       │       │
│       │       └── Is a PSA/OPA/Kyverno policy supposed to block this?
│       │           ```bash
│       │           # Pod Security Admission (replaced PSP, removed in K8s 1.25)
│       │           kubectl label --dry-run=server --overwrite ns <namespace> \
│       │             pod-security.kubernetes.io/enforce=restricted
│       │           kubectl get ns <namespace> -o jsonpath='{.metadata.labels}' | grep pod-security
│       │           kubectl get clusterpolicy -n kyverno 2>/dev/null  # Kyverno
│       │           kubectl get constrainttemplate 2>/dev/null  # OPA Gatekeeper
│       │           ```
│       │           │
│       │           ├── Policy exists but this container got through
│       │           │   └── ✅ ACTION: Fix Policy Gap — enforcement mode?
│       │           │       Warn vs Enforce mode; namespace exemptions?
│       │           │
│       │           └── No policy in place → add one
│       │               └── ✅ ACTION: Add Admission Controller Policy
│       │
└── What is the blast radius if this container is compromised?
    │
    ├── High blast radius (data store, payment processing, secret access, internet-facing)
    │   └── Treat as P1 — fix within hours, not days
    │
    └── Low blast radius (internal utility, read-only job, isolated namespace)
        └── Fix within the sprint — document, assign, track

Node Details

Check 1: Is the container privileged?

Command/method:

# Find all privileged containers across the cluster
kubectl get pods --all-namespaces -o json | \
  jq -r '.items[] | . as $pod |
    .spec.containers[] | select(.securityContext.privileged==true) |
    "\($pod.metadata.namespace)/\($pod.metadata.name)/\(.name)"'

# Check initContainers too
kubectl get pods --all-namespaces -o json | \
  jq -r '.items[] | . as $pod |
    .spec.initContainers[]? | select(.securityContext.privileged==true) |
    "INIT: \($pod.metadata.namespace)/\($pod.metadata.name)/\(.name)"'
What you're looking for: The privileged: true flag gives the container nearly full access to the host kernel — all devices, all capabilities, no seccomp filtering. It is equivalent to running as root on the host. Common pitfall: Checking only the pod spec, not the container spec. Privilege is set per-container (spec.containers[].securityContext), not just at the pod level (spec.securityContext). Both must be checked.

Check 2: Does it mount the Docker socket or host filesystem?

Command/method:

# Check for Docker socket mount (full Docker daemon access = host escape)
kubectl get pods --all-namespaces -o json | \
  jq -r '.items[] | select(.spec.volumes[]?.hostPath.path=="/var/run/docker.sock") |
          "\(.metadata.namespace)/\(.metadata.name)"'

# Check for any hostPath mounts
kubectl get pod <pod> -n <namespace> -o json | \
  jq '.spec.volumes[] | select(.hostPath) | {name: .name, path: .hostPath.path}'

# Check which containers mount which volumes
kubectl get pod <pod> -n <namespace> -o json | \
  jq '.spec.containers[] | {container: .name, mounts: .volumeMounts}'
What you're looking for: /var/run/docker.sock is the worst case — any process that can talk to the Docker daemon can escape to the host. Any hostPath mount to a sensitive directory is also dangerous. Even read-only mounts to /etc expose sensitive configuration. Common pitfall: Volumes are defined at the pod level but mounted at the container level. A pod may define a hostPath volume but only mount it in one of its containers. Check both the volume definitions and the container's volumeMounts.

Check 3: What does the container need root for?

Command/method:

# Check what capabilities the process actually uses
# Run the container and inspect
docker run --rm <image> id
docker run --rm <image> cat /proc/1/status | grep Cap

# Decode capability hex to human-readable
capsh --decode=<hex>

# Check if port binding is the reason (ports < 1024 require root OR CAP_NET_BIND_SERVICE)
kubectl get pod <pod> -o json | jq '.spec.containers[].ports[]'

# Check if init system is the reason
kubectl get pod <pod> -o json | jq '.spec.containers[].command'
What you're looking for: The specific kernel capability or system operation that requires root. In most cases, the real need is one narrow capability (like NET_BIND_SERVICE), not full root. Root is rarely actually required. Common pitfall: "It's always been this way" is not a legitimate reason for root. The original author may have added root out of convenience. Actually test whether the container works as non-root before assuming it cannot.

Check 4: Is there a policy that should have blocked this?

Command/method:

# Kyverno — check for require-run-as-non-root policy
kubectl get clusterpolicy require-run-as-non-root 2>/dev/null -o yaml | grep action

# OPA Gatekeeper — list constraint templates
kubectl get constrainttemplate k8srequiredusers 2>/dev/null

# Pod Security Standards (Kubernetes 1.23+)
kubectl get ns <namespace> -o json | jq '.metadata.labels | to_entries[] | select(.key | contains("pod-security"))'

# PSP was removed in K8s 1.25 — use Pod Security Admission (PSA) instead
# kubectl get psp restricted -o yaml  # no longer available on 1.25+
kubectl get ns <namespace> -o json | jq '.metadata.labels | to_entries[] | select(.key | contains("pod-security"))'

# Check if namespace is exempt from any policy
kubectl get ns <namespace> --show-labels
What you're looking for: Whether there is a policy in place and whether it is in warn or enforce mode. A policy in warn mode will log violations but not block them — this is how containers running as root slip through despite "having a policy." Common pitfall: Namespaces created before the policy was applied may be exempt. Check that the admission controller webhook is applied to the namespace in question, not just cluster-wide in theory.


Terminal Actions

✅ Action: Fix Now — Add securityContext.runAsNonRoot: true

Do: 1. Verify the image has a non-root user defined:

docker run --rm --entrypoint="" <image> id
# Should show a non-root UID (not 0)
2. Edit the deployment:
kubectl edit deployment <name> -n <namespace>
Add to the container spec:
securityContext:
  runAsNonRoot: true
  runAsUser: 1000
  allowPrivilegeEscalation: false
  readOnlyRootFilesystem: true
  capabilities:
    drop: ["ALL"]
3. Apply and verify rollout:
kubectl rollout status deployment/<name>
kubectl get pods -l app=<name> -o jsonpath='{.items[*].spec.containers[*].securityContext}'
4. Run health check: curl -s http://<service>/health Verify: kubectl get pod <pod> -o json | jq '.spec.containers[].securityContext' shows runAsNonRoot: true. Pod is Running and healthy. Application logs show no permission errors.

✅ Action: Fix with Image Change — Modify Dockerfile

Do: 1. Add a non-root user to the Dockerfile:

# At the end of the build stage, before the final CMD:
RUN groupadd --gid 1000 appgroup && \
    useradd --uid 1000 --gid appgroup --shell /bin/bash --create-home appuser

# Fix ownership of any files the app needs to write
RUN chown -R appuser:appgroup /app /tmp

USER appuser
2. Build and test locally:
docker build -t <image>:nonroot-test .
docker run --rm --user 1000 <image>:nonroot-test id
docker run --rm --user 1000 <image>:nonroot-test <entrypoint-command>
3. Push and deploy; monitor for file permission errors in logs Verify: docker run --rm <image>:new id shows uid=1000. Container starts and passes health check. No permission denied in application logs.

✅ Action: Escalate + Contain (Privileged + Host Mount)

Do: 1. Immediately notify security team — this is a critical vulnerability 2. Assess whether the container has been compromised:

# Check for unexpected processes
kubectl exec <pod> -- ps aux
# Check for outbound connections
kubectl exec <pod> -- ss -tnp
# Check for written files in sensitive locations
kubectl exec <pod> -- find /host -newer /tmp -type f 2>/dev/null | head -20
3. If no evidence of compromise: apply NetworkPolicy to restrict egress immediately 4. Deploy a replacement pod with correct security context; drain the vulnerable pod 5. Remove the privileged flag and host mount from the manifest; redeploy 6. Add an admission controller rule to block privileged + hostPath combinations Verify: New pod runs without privileged flag. No host mounts. Admission controller blocks future equivalent deployments.

✅ Action: Fix with Capability Drop + Non-Root User

Do: 1. Identify the required capability (example: binding port 443 needs NET_BIND_SERVICE) 2. Drop all capabilities and add back only the needed one:

securityContext:
  runAsNonRoot: true
  runAsUser: 1000
  allowPrivilegeEscalation: false
  capabilities:
    drop: ["ALL"]
    add: ["NET_BIND_SERVICE"]  # Only if required for port binding
3. Alternatively for port binding: use a service to expose port 443 externally while the container listens on 8443 4. Test that the application starts and functions correctly Verify: Pod running as UID 1000. Application serving correctly. kubectl exec <pod> -- id shows non-root UID.

✅ Action: Add Admission Controller Policy

Do: 1. Install Kyverno if not present: helm install kyverno kyverno/kyverno -n kyverno --create-namespace 2. Apply a require-non-root policy:

apiVersion: kyverno.io/v1
kind: ClusterPolicy
metadata:
  name: require-run-as-non-root
spec:
  validationFailureAction: enforce
  rules:
    - name: check-runAsNonRoot
      match:
        resources:
          kinds: [Pod]
      validate:
        message: "Containers must not run as root. Set securityContext.runAsNonRoot=true."
        pattern:
          spec:
            containers:
              - securityContext:
                  runAsNonRoot: true
3. Start in audit mode (change enforce to audit) and review violations before switching to enforce Verify: kubectl apply of a root-running pod is blocked with the policy message. Existing pods are reported as violations in audit mode.

⚠️ Escalation: Privileged Container with Host Mount

When: Container is running privileged AND has a host filesystem or Docker socket mount — especially in production. Who: Security team lead immediately; CISO if there is any evidence of active exploitation. Include in page: Pod name, namespace, node name, what volumes are mounted (paths), whether the container is internet-facing, whether there is any evidence of exploitation (unusual processes, outbound connections), and proposed containment plan.


Edge Cases

  • The container is a DaemonSet for a legitimate node agent (Datadog, Falco, Fluent Bit, CNI plugin): These legitimately need host access. Document the exception in your security posture, apply the most restrictive securityContext that still allows function, and ensure the workload is from a trusted image with a pinned digest.
  • The container is running as root but readOnlyRootFilesystem: true is set: This reduces but does not eliminate risk. A read-only root filesystem still allows exploiting kernel vulnerabilities. Fix the root user issue separately.
  • The Dockerfile is not in your control (third-party image): Use a runAsUser override in the pod spec: securityContext.runAsUser: 1000. This may fail if the image has files owned only by root — test carefully and check for permission errors.
  • The container has been running as root for years with no incidents: This is not evidence of safety; it is evidence of being lucky. Apply the fix — the blast radius does not decrease over time.
  • PSP is deprecated in your Kubernetes version: PodSecurityPolicy was removed in Kubernetes 1.25. Migrate to Pod Security Standards (namespace labels) or a third-party admission controller (Kyverno, OPA Gatekeeper).

Cross-References