What this check reads
The check lists every tagged local image with docker images --format '{{.Repository}}:{{.Tag}}', drops the <none> entries, and counts what remains as the total. It then filters that same list with grep -E ':(latest)$' and keeps the first 5 matches for the message. The threshold is that literal suffix and nothing else: one image whose tag is exactly latest produces the WARN, a list of images with no such tag is the PASS, and no tagged images at all is the SKIP.
When it applies
Needs the docker binary and daemon access; otherwise SKIP. It lists all tagged local images to count the total, then reports the first 5 whose tag is exactly latest. Two scope limits follow from that: other floating tags - stable, main, edge, 3 as a shorthand for 3.x - are not matched, and an image pinned by digest is indistinguishable from one pinned by version tag, since only the literal latest triggers the finding. It looks at what is on the host, not at what your Dockerfiles say, so a FROM ubuntu:latest that has been built into a locally-tagged image will not be reported.
What each result means
| Result | When | What it means |
|---|---|---|
| SKIP | Docker not installed, daemon unreachable, or no local images | Nothing was examined. No image list was produced, so the check makes no claim about how your tags are pinned. |
| WARN | Any image tagged `:latest` | At least one local image sits on the floating :latest tag, and the message names up to five of them. |
| PASS | None | No local image carries the literal :latest tag. Other floating tags such as stable, main or edge are not covered by this result. |
Why it matters
latest is a moving pointer: what you pulled in testing is not necessarily what production runs, so builds are not reproducible and a compromised or broken upstream push propagates silently. Pin a version tag (nginx:1.27.1) or, for full integrity, a digest (nginx@sha256:...). OWASP Docker Cheat Sheet Rule #9 (pin images), CIS Docker 4.2 "Ensure that containers use only trusted base images", SLSA / supply-chain guidance, NIST SP 800-190 4.1.
Why it fails, and when it is wrong
- Locally built images tagged
myapp:latestby default (docker build -t myapp .) trigger this even though they are not pulled from a registry. Tag builds with a version or git SHA as well. - Only images on disk are checked; a running container from
app:latestwhose image was since retagged shows up in the Unpinned Running Images check instead. - Pinning by digest requires a process for updates (Renovate/Dependabot) or you freeze security fixes.
How to fix it
List the images on :latest
docker images --format '{{.Repository}}:{{.Tag}}' | grep -v '<none>' | grep ':latest$'Pin a version or a digest
An explicit version tag makes builds reproducible; a digest also proves the bytes have not changed.
docker image inspect <image> --format '{{index .RepoDigests 0}}'FROM python:3.12.5-slim-bookworm@sha256:<digest>image: ghcr.io/org/app:1.4.2Tag your own builds with a version
docker build -t myapp . tags the result latest by default, which is how most locally built images end up in this finding.
docker build -t myapp:1.4.2 -t myapp:$(git rev-parse --short HEAD) .Remove the stale tag and automate the bumps
The check reads the local image store, so the WARN stays until the old :latest image is gone.
docker rmi <repo>:latestA pinned image does not pick up patches on its own. Let Renovate or Dependabot raise the pins, so pinning does not freeze your security fixes.
Verify the fix
docker images --format '{{.Repository}}:{{.Tag}}' | grep -v '<none>' | grep ':latest$'
# expected: no output
# Catch the floating tags this check does not match:
grep -rn '^FROM' Dockerfile */Dockerfile 2>/dev/null
grep -rn 'image:' docker-compose.yml compose.yaml 2>/dev/null
# The strongest form of pinning - by digest:
docker image inspect <image> --format '{{index .RepoDigests 0}}'
# then use FROM node:22-alpine@sha256:... in the DockerfileDebugging
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.
A base image pulled as :latest stays in the local store under that tag, and keeps the WARN alive after you have pinned everything. docker rmi <repo>:latest removes one, and docker image prune -a removes every image no container uses (plain docker image prune only removes untagged ones). The durable fix is to pin the tag in the Dockerfile or Compose file so nothing pulls :latest again.
The blind spot. stable, main, edge and truncated version tags all float, and none of them are matched here. Check your FROM lines and image: keys with the greps above.
The message truncates at 5; the full list is the first command above.
Pinned images do not get patches automatically. Pair pinning with a scheduled rebuild and a scanner (see the Image Vulnerability Scan check) or a tool like Renovate/Dependabot that raises the pin deliberately.
Sources
- Docker: Dockerfile FROM (tags and digests)
- Docker: Building best practices (pin base image versions)
- Docker: docker pull by digest
- OWASP Docker Security Cheat Sheet
- SLSA supply-chain levels (pinned dependencies)
- Renovate: Docker datasource and digest pinning
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