Docker audit fix

How to Secure Dockerfile Build Configurations on Linux

Docker image build best practices are the CIS section 4 rules dockle enforces: a non-root USER, no credentials in ENV, no ADD from a remote URL, no setuid binaries and a HEALTHCHECK. Install dockle and clear its FATAL findings first.

Hiren KalariyaLast reviewed: Oct 12, 2026Check image-best-practices
Low
severity
Yes
needs root or sudo
5
results it can return
7 of 8
checks in this audit

Every threshold on this page is transcribed from the docker-security-image-vulnerabilities audit script that CtrlOps runs, and a build check fails if the two ever disagree. See all 8 Image Vulnerabilities checks, or how the audit runs.

Low severityNeeds sudo

What this check reads

Which branch runs depends on whether dockle is on PATH. If it is, the check runs dockle --exit-code 0 --format json <img> against the first 3 tagged local images and counts occurrences of "level":"FATAL" in the JSON: one or more is the WARN, a zero count is the PASS, and findings below fatal are not counted at all. If it is not, the check falls back to reading docker inspect -f '{{.Config.Healthcheck}}' on up to 10 images, where a literal <nil> means no HEALTHCHECK is defined. That fallback threshold is asymmetric: it WARNs only when every sampled image comes back <nil>, so a single image with a HEALTHCHECK passes the whole host.

The dockle branch does not work as intended today. Dockle pretty-prints its JSON with a space after the colon ("level": "FATAL"), so the compact pattern never matches and the count stays at zero. With dockle installed the check currently returns PASS whatever dockle found.

When it applies

Needs the docker binary and daemon access; otherwise SKIP. Which branch runs - and how much it actually tells you - depends on whether dockle is installed:

  • With dockle - up to 3 images are audited against dockle's full checkpoint list, counting FATAL-level findings only. Lower-severity findings are ignored.
  • Without dockle - up to 10 images are inspected for a single thing: whether .Config.Healthcheck is defined. The bar is very low, and asymmetric: WARN only if none of the images define a HEALTHCHECK, PASS if even one does. A host where 9 of 10 images lack a healthcheck therefore passes.

Note a HEALTHCHECK is an availability feature rather than a security control; this branch is a stand-in for a real best-practices audit, which is why the severity is LOW.

What each result means

Result thresholds this check applies
ResultWhenWhat it means
SKIPDocker not installed, daemon unreachable, or no images to checkNothing was audited. Neither branch ran, so no claim was made about how your images are built.
WARNDockle found FATAL findingsDockle flagged at least one fatal-level rule across the three images it checked. This branch cannot be reached today, because the count never matches the JSON dockle actually prints.
PASSDockle cleanWith dockle installed this is the only verdict the check can reach today, so it does not show the images are clean. Run dockle yourself and read its findings.
WARNNo Dockle and **none** of the images define a HEALTHCHECKDockle is not installed, so HEALTHCHECK presence was the only thing tested, and not one of the sampled images defines one.
PASSNo Dockle and at least one image has a HEALTHCHECKOne image with a HEALTHCHECK was enough to pass the host. Nothing else about build hygiene was examined.

Why it matters

Dockle implements the CIS Docker Benchmark section 4 (image) checks: no root user, no ADD for remote URLs, no credentials in ENV, no setuid files, HEALTHCHECK present, apt-get caches cleaned, etc. HEALTHCHECK (CIS Docker 4.6 / 5.26) lets the orchestrator detect hung services and avoid routing traffic to them.

Why it fails, and when it is wrong

  • The dockle count never matches (known issue). The script greps for "level":"FATAL" with no space, and dockle writes "level": "FATAL". Fatal findings are counted as zero and the result is PASS.
  • Even with the pattern corrected, only FATAL findings count. Dockle rates a missing non-root user (CIS-DI-0001) as WARN and a missing HEALTHCHECK (CIS-DI-0006) as INFO, so neither would move this result.
  • The fallback passes if any image has a healthcheck; it is a weak proxy.
  • Compose healthcheck: blocks set the check on the container, not the image; the fallback inspects images and misses them.
  • Dockle FATAL levels include items that are hard to satisfy on some base images; review its output rather than treating the count as absolute.

How to fix it

Install dockle and run it on every image

Dockle publishes a .deb and an .rpm with each release; the repository in Sources has the install commands. Then run it across everything, not the three images the check samples.

for i in $(docker images --format '{{.Repository}}:{{.Tag}}' | grep -v '<none>'); do
  echo "== $i"; dockle "$i"
done

Clear the FATAL findings first

The ones that come up most are CIS-DI-0010 (a credential in ENV or a file), CIS-DI-0009 (ADD where COPY would do), DKL-DI-0005 (apt-get caches left in the layer) and DKL-DI-0001 (sudo in the image). The apt-get one is fixed in the same RUN that installs:

RUN apt-get update && apt-get install -y --no-install-recommends curl \
 && rm -rf /var/lib/apt/lists/*

Add a HEALTHCHECK where it helps

HEALTHCHECK --interval=30s --timeout=3s CMD curl -fsS http://localhost:8080/healthz || exit 1

The command runs inside the container, so the binary it uses has to exist there.

Rebuild and run dockle again

docker build -t app:1.4.3 . && dockle app:1.4.3
# expected: no FATAL findings

Verify the fix

# Run a real best-practices audit across every image, not three:
for i in $(docker images --format '{{.Repository}}:{{.Tag}}' | grep -v '<none>'); do
  echo "== $i"; dockle --exit-code 0 "$i"
done
# expected: no FATAL findings

# The fallback's single test, per image:
docker images --format '{{.Repository}}:{{.Tag}}' | grep -v '<none>' | \
  xargs -I{} sh -c 'printf "%s hc=%s\n" {} "$(docker image inspect -f "{{.Config.Healthcheck}}" {})"'
# <nil> means no HEALTHCHECK

# Confirm a healthcheck actually reports:
docker inspect -f '{{.State.Health.Status}}' <container>    # expected: healthy

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 to check' 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.

The known issue above: the check counts fatal findings with a pattern that never matches dockle's JSON, so the count is always zero. Run dockle <image> yourself and go by what it prints.

Nearly meaningless: one image with a HEALTHCHECK is enough to pass the whole host. Install dockle, or use the per-image loop above, before treating this as a real result.

Accurate but narrow. Add HEALTHCHECK CMD instructions where they make sense, and remember this branch is not auditing anything else.

Run dockle <image> directly. Each finding carries a checkpoint ID, for example CIS-DI-0010 (secrets stored in the Dockerfile), CIS-DI-0009 (ADD used where COPY would do) or DKL-DI-0005 (apt-get caches not cleared), and the checkpoint list in Sources explains each one.

A deliberate cost cap, since dockle is slow. The message says how many were checked; use the loop for the rest.

The command runs inside the container, so it must exist there (curl often does not) and must exit 0. Test with docker exec <c> <your healthcheck command>; echo $?.

Sources

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
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