What this check reads
The check takes the first 10 tagged local images from docker images --format '{{.Repository}}:{{.Tag}}', <none> entries excluded, and reads docker inspect -f '{{.Config.User}}' <img> for each of them. Three values count as root: an empty string, the literal root, and 0. If none of the sampled images fall into that group the check PASSes; if any do it WARNs and names at most 5 of them, and an empty image list is the SKIP.
When it applies
Needs the docker binary and daemon access; otherwise SKIP. It inspects the first 10 tagged local images and reads .Config.User from each - the USER declared in the image, with empty, root and 0 all counting as root. This is about images, not running containers: an image with no USER that is always run with -u 1000 still produces a finding, and conversely an image with a USER can be overridden at run time. The running-container equivalent is the Container User check. Despite the check's name, only the default user is reviewed - exposed ports, healthchecks and other image configuration are not examined here.
What each result means
| Result | When | What it means |
|---|---|---|
| SKIP | Docker not installed, daemon unreachable, or no local images | Nothing was inspected. No image list was produced, so the check says nothing about the default user of anything on this host. |
| PASS | Every image sets a non-root USER | Every image in the sample declares a USER that is neither root nor 0. A run-time --user override can still change what the container actually runs as. |
| WARN | Some images have no USER | At least one sampled image would run as root unless the run command overrides it, and the message names up to five. |
Why it matters
Same principle as Container User but at the image level: CIS Docker 4.1 "Ensure that a user for the container has been created"; Dockerfile USER; Dockle CIS-DI-0001. Images without USER run as root unless overridden at run time.
Why it fails, and when it is wrong
- Official images (nginx, postgres, redis, mysql, mongo, node, python) ship without
USERon purpose (entrypoint drops privileges, or they leave it to the operator). This WARN is informational for those; the Container User check on the running container is the one that matters. - Images pulled only as build stages or caches are included in the sample.
- Sample is 10 images, in
docker imagesorder (newest first). - Only the exact values
rootand0count, alongside an empty one.USER 0:0andUSER root:rootname a group as well, match neither string, and pass as non-root. The Container User check does treat those forms as root on the running container.
How to fix it
Create the account and hand it its files
Do this after the package installs, which still need root.
RUN groupadd -r -g 10001 app && useradd -r -u 10001 -g app app \
&& chown -R 10001:10001 /appThat is the Debian, Ubuntu and RHEL form. On Alpine use addgroup -S -g 10001 app && adduser -S -u 10001 -G app app.
Switch with a numeric USER
USER 10001:10001Put it after every instruction that needs root and before CMD. A numeric ID does not depend on an /etc/passwd lookup at run time.
Rebuild and confirm
docker build -t app:1.4.3 .
docker run --rm app:1.4.3 id
# expected: uid=10001, not uid=0(root)For images you do not build
You cannot change someone else's image. Use its non-root variant where one exists, derive your own image that adds a USER line, or set the user at run time with --user or Compose user:, and record the base-image finding as accepted.
Verify the fix
docker images --format '{{.Repository}}:{{.Tag}}' | grep -v '<none>' | \
xargs -I{} sh -c 'printf "%s user=%s\n" {} "$(docker image inspect -f "{{.Config.User}}" {})"'
# expected: a non-root user or UID for every image
# Confirm a rebuilt image really runs as that user:
docker run --rm <image> id
# expected: uid=10001 (or whatever you set), not uid=0(root)Debugging
Three different cases share this status. 'Docker not installed' is correct on a host with no Docker. 'Cannot query Docker daemon' means the audit account could not reach the daemon: check systemctl is-active docker, then docker ps as that user and sudo docker ps, and run the audit as root or with sudo if only the second works. 'No local images found' means the daemon held no tagged images, which is normal on a host that pulls at deploy time; check with docker images -a. It is also what you get when the audit has root but the daemon is down, because an empty answer and an empty image list look the same to the script. The Docker Inventory check covers the unreachable case in more detail.
Most official images (nginx, postgres, node) ship without a USER by design and expect you to set it at run time, or drop privileges in their entrypoint. You cannot fix someone else's image; set the user when you run it (--user, Compose user:) or in your own derived image, and record the base-image finding as accepted.
The image's USER can be overridden at run time. Cross-check with the Container User check and docker inspect -f '{{.Config.User}}' $(docker ps -q).
The instruction applies to everything after it, so package installs and chowns must come first. Create the account explicitly and give it ownership: RUN useradd -u 10001 -r app && chown -R 10001 /app, then USER 10001. Prefer a numeric UID so the runtime does not depend on /etc/passwd lookups.
The sample is capped at 10 images and the message lists at most 5. Use the command above for the complete picture.
Sources
- Docker: Dockerfile USER
- Docker: Building best practices (USER)
- Dockle checks (CIS-DI-0001 Create a user for the container)
How the script reads this
Next
Re-run the Image Vulnerabilities audit after applying the fix and confirm this check moves to PASS. CtrlOps runs all 8 checks over your existing SSH connection and scores the result, so the change is visible without reading another config file.
All 8 Image Vulnerabilities fixes