Docker audit fix

How to Audit and Secure Unpinned Running Docker Images

A container started from a bare image name or the latest tag cannot be tied back to a tested build, because the tag moves under you. Redeploy those containers pinned to a version or a digest, and keep an image scanner available.

Hiren KalariyaLast reviewed: Oct 19, 2026Check image-hygiene
Medium
severity
Yes
needs root or sudo
5
results it can return
5 of 5
checks in this audit

Every threshold on this page is transcribed from the docker-security-network-supply-chain audit script that CtrlOps runs, and a build check fails if the two ever disagree. See all 5 Network & Supply Chain checks, or how the audit runs.

Medium severityNeeds sudo

What this check reads

It reads the image reference of every running container with docker ps --format '{{.Image}}' and counts the ones matching :latest$ or having no tag and no digest at all (^[^:@]+$, a bare nginx). Separately it hunts for a scanner, checking PATH for trivy, grype, docker-scout, clair and snyk and then trying docker scout version. The two signals are combined into one result: a PASS needs an unpinned count of zero and at least one scanner, and every other combination, pinned images with no scanner included, is a WARN.

When it applies

Needs a reachable daemon; otherwise SKIP. Note it does not SKIP when nothing is running - with no containers the image count is zero and the result then turns purely on whether a scanner is installed. It combines two unrelated signals: the images of running containers matching :latest or carrying no tag or digest at all (^[^:@]+$, i.e. bare nginx), and whether any of trivy, grype, docker-scout, clair, snyk is on PATH or docker scout version works. PASS requires both - pinned images and a scanner present. Other floating tags (stable, main, edge) are not matched, and the scanner test only proves a binary exists, never that anything is scanned.

What each result means

Result thresholds this check applies
ResultWhenWhat it means
SKIPDaemon unreachableNeither half ran, so nothing is known about your image references or your scanning tools. The scanner probe sits inside the same branch and is skipped with everything else.
WARNUnpinned containers and no scannerBoth halves failed: containers are running from references that move, and no scanner binary is installed here to tell you what is inside them.
WARNUnpinned containers (scanner present)A scanner was found, but at least one running container came from a bare name or :latest, so you cannot say which build is actually in production. The message names the scanner it found.
WARNPinned but no scannerImage references are pinned, but no scanner binary exists on this host. Scanning done in CI is invisible here and is a perfectly good answer to this half.
PASSPinned and a scanner is availableNo running image matched the unpinned pattern and a scanner binary is present. Other floating tags such as stable or main are not matched, and an installed scanner is not evidence that anything gets scanned.

Why it matters

Combines two supply-chain hygiene signals: reproducible, pinned deployments (CIS Docker 4.2, OWASP Docker Cheat Sheet Rule #9) and the presence of scanning tooling (CIS Docker 4.4). A container running app or app:latest cannot be correlated with a tested build.

Why it fails, and when it is wrong

  • docker ps shows the image name as given at docker run. Compose with image: app and a local build: shows app (no tag) → unpinned. Tag local builds with a version (image: app:${GIT_SHA}).
  • Digest-pinned images (nginx@sha256:...) contain @ and correctly count as pinned.
  • The bare-name pattern treats any : as a tag, so an untagged image from a registry with a port, localhost:5000/app or registry.example.com:5000/team/app, counts as pinned even though it resolves to latest.
  • Scanner presence on the host is a proxy; CI-side scanning is invisible and perfectly acceptable. A PASS requires a local scanner binary, which many teams do not install on production hosts. Treat the scanner half as advisory.

How to fix it

Find the unpinned containers

docker ps --format '{{.Image}} {{.Names}}' | grep -E '^([^:@ ]+|[^ ]*:latest) '

Also check by eye for untagged references to a registry with a port, such as localhost:5000/app, which the check counts as pinned.

Pin a version or digest

docker image inspect <image> --format '{{index .RepoDigests 0}}'
image: nginx:1.27.1                 # or nginx@sha256:<digest>

For a service built locally, give the build a version: image: app:${GIT_SHA}.

Recreate the containers

A container keeps the reference it was created from, so the finding only clears once it is recreated.

docker compose up -d --force-recreate

Scan images, here or in CI

Install trivy on the host to clear the other half of the check, or rely on the scanning stage in CI, where a failed scan can block a release. The CI route is the stronger one even though this check cannot see it.

Verify the fix

# Running images, and which of them are unpinned:
docker ps --format '{{.Names}}\t{{.Image}}'
docker ps --format '{{.Image}}' | grep -E '(:latest$|^[^:@]+$)'
# expected: no output

# The strongest pin is a digest:
docker inspect -f '{{.Name}} {{.Config.Image}}' $(docker ps -q)     # @sha256:... in the reference means digest-pinned
docker image inspect <image> --format '{{index .RepoDigests 0}}'

# A scanner being installed is not the same as scanning - prove it runs:
trivy image --quiet --severity CRITICAL,HIGH <image>

Debugging

The audit account could not talk to the daemon. To find out why, check systemctl is-active docker, then docker ps as the audit user and sudo docker ps, and run the audit as root or with sudo if only the second works. A host where the daemon answers but nothing is running does not SKIP here; it gets a result decided by the scanner half alone. The Docker Inventory check walks through the unreachable case in more detail.

Half the check. Install trivy (or grype/scout); more importantly, scan in CI where you can block a release, since a scanner on the host proves nothing about your pipeline.

Repin in the Compose file or run command and recreate the containers. A container keeps the image it was created from, so the finding will not clear until you docker compose up -d --force-recreate.

stable, main, edge and truncated version tags are not matched by the pattern. Review your image: keys directly.

That is the trade-off. Pair digest pins with a scheduled rebuild or a bot (Renovate/Dependabot) that raises the pin deliberately, so upgrades are a reviewed change rather than an accident.

Image tagging is also covered by Floating Image Tags (local images, not running ones) and scanning by Image Vulnerability Scan (which actually runs the scanner). Treat this one as the summary and those as the detail.

Sources

How the script reads this

Next

Re-run the Network & Supply Chain audit after applying the fix and confirm this check moves to PASS. CtrlOps runs all 5 checks over your existing SSH connection and scores the result, so the change is visible without reading another config file.

All 5 Network & Supply Chain fixes
Audit your fleet

Find every one of these on every server, in one click

CtrlOps runs this audit over your existing SSH connection - no agents, no scripts to manage. $7/user/month after a 1 month free trial - no credit card required.

Windows

✓ Start instantly·✓ No credit card·✓ No sneaky autorenewals