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)"'
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}'
/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'
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
Terminal Actions¶
✅ Action: Fix Now — Add securityContext.runAsNonRoot: true¶
Do: 1. Verify the image has a non-root user defined:
2. Edit the deployment: Add to the container spec:securityContext:
runAsNonRoot: true
runAsUser: 1000
allowPrivilegeEscalation: false
readOnlyRootFilesystem: true
capabilities:
drop: ["ALL"]
kubectl rollout status deployment/<name>
kubectl get pods -l app=<name> -o jsonpath='{.items[*].spec.containers[*].securityContext}'
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
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>
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
✅ 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
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
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: trueis 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
runAsUseroverride 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¶
- Topic Packs: security, kubernetes, containers
- Related trees: found-vulnerability.md, dependency-cve.md
- Runbooks: incident_response.md