Remediation Guides

Running Containers as Root User Should Be Avoided: How to Fix It in Docker, Kubernetes and ECS

26 September 2026 8 min read

“Running containers as root user should be avoided” is a hardening finding raised when a Kubernetes pod, Dockerfile or ECS task definition lets the container process run as UID 0. To fix it, create a numeric non-root user in the image, set runAsNonRoot: true and runAsUser in the pod securityContext (or user in ECS), and enforce it at admission.

What the scanner is actually detecting

Four common checks raise this finding, and they do not look at the same thing:

Scanner Finding What it reads Fails when Severity
Microsoft Defender for Cloud Running containers as root user should be avoided Live pods, through the Azure Policy add-on A container (including init and ephemeral containers) resolves to runAsUser: 0, or has neither runAsNonRoot: true nor any runAsUser at container or pod level High
Trivy KSV-0012 (KSV012) Runs as root user Kubernetes manifests and workloads The pod securityContext does not set runAsNonRoot: true and at least one container or init container does not set it either Medium
Trivy DS-0002 (DS002) Image user should not be ‘root’ Dockerfile or image config There is no USER instruction at all, or the last one is root or 0 High
AWS Security Hub ECS.20 ECS Task Definitions should configure non-root users in Linux container definitions Latest active task definition revision Any Linux container definition has no user, or uses root or 0 Medium

The Defender recommendation maps to the Azure Policy definition Kubernetes cluster pods and containers should only run with approved user and group IDs (default rule MustRunAsNonRoot, default effect Audit, with kube-system, gatekeeper-system, azure-arc and azure-extensions-usage-system excluded). Three differences explain most “fixed but still red” tickets:

  • Trivy KSV-0012 only accepts runAsNonRoot: true. A pod with runAsUser: 10001 alone passes Defender but still fails KSV-0012.
  • Defender, KSV-0012 and ECS.20 never look inside the image. A USER 10001 line in the Dockerfile does not clear them.
  • DS-0002 never looks at the pod or task definition. A securityContext does not clear it.

Real-world risk, stated honestly

Running as root inside a container is not a host compromise by itself: namespaces, cgroups, the reduced default capability set and seccomp still confine the process. But unless user namespaces are in use, UID 0 in the container is UID 0 to the host kernel, so root removes a layer that limits damage when something else fails:

  • Container escape bugs get easier to use. CVE-2019-5736 in runc, for example, let an attacker overwrite the host runc binary, and so gain host root, by running a command as root inside a container.
  • Host mounts become dangerous. A hostPath volume, a mounted Docker socket or a shared data directory is fully writable to a root process.
  • Other misconfigurations compound. Added capabilities or privileged mode give far more to a root process than to UID 10001.

Without host mounts, this is defense in depth. Prioritize internet-facing services and anything that mounts host paths or runs privileged.

How to confirm it on your hosts and clusters

Docker hosts and images:

# Empty output, "root" or "0" means the image defaults to root
docker inspect --format '{{.Config.User}}' registry.example.com/web:1.4.2

# Configured user of every running container
docker ps --quiet | xargs docker inspect --format '{{.Name}} user={{.Config.User}}'

Kubernetes workloads:

kubectl get pods -A -o custom-columns='NS:.metadata.namespace,POD:.metadata.name,POD_NONROOT:.spec.securityContext.runAsNonRoot,POD_UID:.spec.securityContext.runAsUser,CTR_NONROOT:.spec.containers[*].securityContext.runAsNonRoot,CTR_UID:.spec.containers[*].securityContext.runAsUser'

# What the process actually runs as (needs an id binary in the image)
kubectl exec -n shop deploy/web -- id

Where all four columns show <none>, the image decides, and the official Debian, Ubuntu and Alpine images default to root. To reproduce the Trivy results locally:

trivy config ./deploy/
trivy image --image-config-scanners misconfig registry.example.com/web:1.4.2

Amazon ECS:

aws ecs describe-task-definition --task-definition web 
  --query 'taskDefinition.containerDefinitions[].{name:name,user:user}'

A null user for any container, sidecars included, fails ECS.20.

How to fix it

1. Dockerfile: create a user with a fixed numeric UID

Alpine (BusyBox tools):

FROM alpine:3.20
RUN addgroup -S -g 10001 app && adduser -S -D -H -u 10001 -G app app
WORKDIR /app
COPY --chown=10001:10001 . /app
USER 10001:10001
CMD ["./server"]

Debian and Ubuntu:

RUN groupadd --system --gid 10001 app 
 && useradd --system --uid 10001 --gid app --no-log-init --no-create-home app
COPY --chown=10001:10001 . /app
USER 10001:10001

Details that matter:

  • Use the numeric UID in USER. If a pod sets runAsNonRoot without runAsUser, the kubelet cannot verify a user name and refuses to start the container.
  • Give the user a group. Docker’s USER documentation notes that a user without a primary group runs with the root group.
  • Use COPY –chown. Without it, copied files are owned by UID 0 and GID 0, so any write to them fails.
  • Put USER in the final stage, since it applies only to the current build stage, and do not install sudo.

On plain Docker hosts, docker run –user 10001:10001 is a stopgap until the image is rebuilt, but it does not clear DS-0002. If you are also choosing a base image, several minimal images ship a non-root variant; see our comparison of distroless, Alpine and Debian base images.

2. Kubernetes: set the securityContext

apiVersion: apps/v1
kind: Deployment
metadata:
  name: web
  namespace: shop
spec:
  template:
    spec:
      securityContext:
        runAsNonRoot: true
        runAsUser: 10001
        runAsGroup: 10001
        fsGroup: 10001
        fsGroupChangePolicy: "OnRootMismatch"
        seccompProfile:
          type: RuntimeDefault
      containers:
        - name: web
          image: registry.example.com/web:1.4.3
          securityContext:
            allowPrivilegeEscalation: false
            capabilities:
              drop: ["ALL"]

Pod-level fields apply to every container, including init containers; a container-level value overrides them. runAsUser satisfies Defender. runAsNonRoot is what KSV-0012 and the restricted profile check, and it makes the kubelet refuse any container whose effective user would be UID 0. fsGroup gives the group write access to volumes that support ownership management, and OnRootMismatch skips the recursive ownership change when the volume root already matches. If Helm, Argo CD or Flux owns the object, change the source, or the next sync reverts your edit.

3. Enforce it with Pod Security Admission

# Preview which existing pods would violate "restricted"
kubectl label --dry-run=server --overwrite ns shop 
  pod-security.kubernetes.io/enforce=restricted

# Warn and audit first, enforce once warnings stop
kubectl label --overwrite ns shop 
  pod-security.kubernetes.io/warn=restricted 
  pod-security.kubernetes.io/audit=restricted
kubectl label --overwrite ns shop pod-security.kubernetes.io/enforce=restricted

The restricted profile requires runAsNonRoot: true and forbids runAsUser: 0. It also requires allowPrivilegeEscalation: false, dropping ALL capabilities and a RuntimeDefault or Localhost seccomp profile, which is why the manifest above sets them. On AKS you can instead change the Azure Policy assignment effect from Audit to Deny.

4. Amazon ECS

"containerDefinitions": [
  {
    "name": "web",
    "image": "123456789012.dkr.ecr.eu-west-1.amazonaws.com/web:1.4.3",
    "user": "10001:10001"
  }
]

ECS accepts user, user:group, uid, uid:gid, user:gid and uid:group, with IDs as positive integers. The parameter is not supported for Windows containers, which ECS.21 covers instead. Set it on every container definition, register a new revision, then point the service at it:

aws ecs update-service --cluster prod --service web --task-definition web:7

How to verify the fix and rescan

docker run --rm --entrypoint id registry.example.com/web:1.4.3
# uid=10001(app) gid=10001(app)

kubectl exec -n shop deploy/web -- id -u
# 10001

aws ecs describe-services --cluster prod --services web 
  --query 'services[].taskDefinition'

Rerun trivy config and trivy image; KSV-0012 and DS-0002 should disappear. ECS.20 evaluates only the latest active revision, so a pass does not prove the service runs it; describe-services does. Defender updates after the next Azure Policy compliance evaluation, which is not instant. To catch regressions, gate image builds on these checks as described in scanning Docker images in CI.

What can break and how to roll back

  • Permission denied on writes to logs, caches, PID files or upload directories inside root-owned image paths. Fix ownership in the Dockerfile or mount an emptyDir.
  • Ports below 1024. A non-root process usually cannot bind 80 or 443. Listen on 8080 or 8443 and map the port in the Service or with -p.
  • CreateContainerConfigError with “image has non-numeric user” (use a numeric USER or set runAsUser) or “image will run as root” (the image still defaults to UID 0).
  • Entrypoints that start as root and drop privileges with chown or gosu. Look for the vendor’s unprivileged variant.
  • Persistent volumes with root-owned data on volume types where fsGroup is not supported. Fix ownership once with a controlled job.
  • PSA enforcement. Existing pods keep running, but new pods from rollouts, scaling or node drains are rejected. Watch kubectl get events -n shop –field-selector reason=FailedCreate.

Rollback is quick, since nothing here changes the host:

kubectl rollout undo deployment/web -n shop
kubectl label ns shop pod-security.kubernetes.io/enforce-
aws ecs update-service --cluster prod --service web --task-definition web:6

For plain Docker, redeploy the previous image digest.

Common false positive reasons

  • The base image is already non-root but your Dockerfile has no USER line, so DS-0002 still fires on the Dockerfile. Add an explicit USER to record the intent.
  • The image runs as UID 10001 but the pod spec says nothing. Defender and KSV-0012 read only the spec, so they flag a process that is not actually root.
  • The ECS task definition omits user while the image sets USER 10001. ECS.20 does not inspect the image.
  • Infrastructure agents such as CNI, CSI or node log collectors that genuinely need root. These are accepted exceptions: use a namespace with a less strict profile, the policy’s excludedNamespaces or excludedImages parameters, and AVD-KSV-0012 in .trivyignore.

More patterns are covered in our guide to container scanner false positives.

FAQ

Is runAsUser enough without runAsNonRoot?

For Defender, yes: any runAsUser other than 0 passes. For Trivy KSV-0012 and the restricted profile, no. Set both; runAsNonRoot also makes the kubelet block a container that would run as UID 0.

Why does DS-0002 pass when my final image still runs as root?

DS-0002 takes the last USER instruction anywhere in the Dockerfile, so a USER in a builder stage can satisfy it. Put USER in the final stage and check the built image with docker inspect.

Do user namespaces or rootless Docker make this finding irrelevant?

They reduce host impact, but these checks read configuration, so the finding stays. A non-root USER is still the portable fix.

Does this apply to Windows containers?

No. ECS.20 and the Kubernetes runAsUser rules cover Linux only. For Windows tasks, ECS.21 flags the default ContainerAdministrator account.

Tracking this finding across many hosts

If your container scanners are among the 16 that SITEY imports findings from, these results can sit in one self-hosted list next to your host vulnerabilities. Duplicates are merged per scanner, not across scanners, so a workload flagged by both Defender and Trivy appears twice. Its AI triage can suggest likely false positives with evidence, and a human makes the final call.

Sources

SITEY closes the loop, not just the report.Discover, validate, fix and verify in your own infrastructure.

See pricing