← Blog

Docker Image Security: Scanning, Hardening and Provenance

A practical guide to Docker image security: scan for vulnerabilities, harden with minimal base images, sign for provenance, and secure the image supply chain.

Bruno Baldo·Oct 5, 2026·Updated Sep 14, 2026·10 min read·Reviewed by Rainforest Technologies

A Docker image is the single artifact that travels from a developer's laptop, through CI, into a registry, and onto whatever runs it in production. That makes Docker image security one of the highest-leverage investments a team can make: harden the image once and every environment that pulls it inherits the result; ship a vulnerable or tampered image once and every one of them inherits that instead. The image is a frozen filesystem that a runtime trusts completely — it does not inspect what is inside, it simply runs it. Deciding what goes in, checking it with container image scanning, and proving where it came from is where container supply chain security actually begins.

This is a deep dive into the image itself, part of the broader Docker and container security picture. It is deliberately Docker- and registry-focused: for how the same concerns play out at the orchestration layer — admission control, verified pulls, runtime drift — see the companion guide on Kubernetes image security. Here we stay with the build, the registry, and the local workflow where images are made.

Anatomy of image risk

To secure an image on purpose rather than by habit, it helps to name the distinct kinds of risk it carries.

OS packages. The base image ships a distribution's worth of system libraries and utilities. These accumulate CVEs over time, and a base image that was clean two months ago almost certainly is not today. Unpatched OS packages are the single most common source of image vulnerabilities.

Application dependencies. Your app pulls in open-source libraries from npm, PyPI, Maven, Go modules, and their transitive dependencies. A known-vulnerable version buried deep in the tree is just as exploitable inside a container as anywhere else — and easier to miss.

Layers and history. A Docker image is not a flat filesystem; it is a stack of layers, each recording a build step. Layers are cached, content-addressed, and immutable. That is great for reproducibility and terrible for anything you wish you had not committed, because a file added in one layer persists in history even if a later layer removes it.

Embedded secrets. Secrets get baked into images more often than anyone admits: an API key in an ENV line, a .npmrc token, an SSH key COPY-ed in for a private dependency and never scrubbed. Because of the layer behavior above, RUN rm secret.key does not remove the secret — it only hides it from the final filesystem while leaving it intact in an earlier layer.

Provenance. Finally, the question most images cannot answer on their own: where did this actually come from? Without provenance you cannot tell an image your CI built and approved from one an attacker pushed to your registry under the same tag.

Harden at the source: minimal base images and non-root

The cheapest security wins happen before any scanner runs, by controlling what goes into the image in the first place.

Prefer minimal or distroless base images. A distroless image contains your application and its runtime dependencies and almost nothing else — no shell, no package manager, no general-purpose utilities. That shrinks the attack surface directly (fewer packages means fewer CVEs) and makes post-exploitation far harder, because an attacker who gets code execution finds no shell to pivot with. A multi-stage build lets you compile in a full toolchain image and ship only the result, so compilers and build tooling never reach production.

Run as a non-root user. If the process does not need root, do not grant it root.

# Multi-stage build: compile in a full image, ship a minimal one

FROM golang:1.23 AS build

WORKDIR /src

COPY . .

RUN CGO_ENABLED=0 go build -o /app ./cmd/server

FROM gcr.io/distroless/static:nonroot

COPY --from=build /app /app

USER nonroot:nonroot

ENTRYPOINT ["/app"]

The base-image choice and the Dockerfile instructions around it are worth treating as a discipline of their own — see Dockerfile security best practices for the build-time details, from pinning versions to ordering layers so secrets never enter them.

Vulnerability scanning: SCA across the whole image

You cannot fix what you cannot see, and container image scanning is how you see it. Effective scanning uses software composition analysis (SCA) to inventory everything in the image — both OS packages and application dependencies — and match that inventory against vulnerability data. Scanning only the OS layer or only the app dependencies leaves half the image unexamined; image vulnerability scanning has to span the whole artifact.

Two principles make scanning useful rather than noise.

Scan on build and continuously. Scan in CI so a developer gets feedback on the pull request that introduced a bad dependency, while the fix is a five-minute change. Then scan again at the registry, on a schedule, because vulnerability data changes even when your image does not. An image that passed clean last week can fail this week when a new critical CVE is disclosed against a package it already contains. Only continuous re-scanning of stored images catches that.

Fail the build on fixable criticals. A scan that only emits a report gets ignored. Wire the scanner into the pipeline so new critical (and, where appropriate, high) severity findings break the build, and scope the gate to issues that actually have a fix available so teams are not blocked on CVEs they cannot resolve.

# CI step: scan and fail on new fixable critical/high findings

rainforest image scan registry.example.com/app:$GIT_SHA \

  --severity CRITICAL,HIGH \

  --fail-on CRITICAL,HIGH \

  --ignore-unfixed

Inspecting layers and history for leaked secrets

Vulnerability scanning finds vulnerable packages; it will not necessarily find the private key someone copied in during a build. Because layers are immutable and cached, secret detection has to look at layer history, not just the final filesystem.

The image's own metadata is the starting point. docker history shows the command behind each layer, which surfaces obvious mistakes like a secret passed as a build argument or set in ENV:

docker history --no-trunc registry.example.com/app:$GIT_SHA

To go further, unpack the image and inspect each layer's contents. Saving an image expands it into its individual layer tarballs, so a file removed in a later layer is still recoverable — and detectable — in the layer that added it:

docker save registry.example.com/app:$GIT_SHA -o app.tar

mkdir app && tar -xf app.tar -C app

# each layer/*.tar is a filesystem diff you can search for keys, tokens, .env files

Automated secret detection folds this into the pipeline so every layer is scanned for credential patterns as part of the same gate that runs SCA — which is far more reliable than hoping a reviewer spots a leaked token in a diff. The remediation is not rm: it is rebuilding without the secret in any layer, rotating the exposed credential, and using build secret mounts so the value never lands in an image at all.

Tags versus immutable digests

A tag like app:1.4 is a mutable pointer. Whoever can push to the registry can repoint it at different content, which means an image you scanned and approved can quietly become a different image under the same name. A digest — app@sha256:... — is the image's content-addressed identity and cannot change without changing the reference.

The practical rules follow from that:

  • Never deploy `:latest` in production. With :latest you cannot say what is running, cannot reproduce a deploy, and cannot tie a live workload back to a specific scan result.
  • Pin production references by digest. A digest is reproducible and tamper-evident; it is the strongest guarantee that what you verified is what runs.
  • Treat release tags as immutable. Once a tag points at a digest, do not repoint it. Many registries can enforce tag immutability directly.

How those images are stored and served is its own topic; container registry security covers private registries, access control, and immutability enforcement in depth.

Image signing and provenance

Scanning tells you what is inside an image. Image signing and provenance tell you where it came from and whether to trust it at all.

The mechanics are straightforward. When your pipeline builds an image, it cryptographically signs the resulting digest and records attestations — machine-readable statements about how the image was produced: which source commit, which builder, which steps ran. Docker's own tooling has offered this in several forms over the years (Docker Content Trust, and more recently detached signatures and attestations via a Notation-style workflow and keyless, transparency-log signing in the mold of the cosign approach). The underlying principle is the same regardless of tool: sign the digest at build time, verify the signature before you trust the image.

This is the core of the SLSA framework (Supply-chain Levels for Software Artifacts), which defines increasing levels of provenance integrity for a build. Signing closes a gap that neither scanning nor registry access control can: even if an attacker pushes a malicious image under a legitimate name, it will not carry a valid signature from a key your organization controls, and verification rejects it. Because signing and verification live in the pipeline rather than in a manual step, provenance is enforced automatically for every image rather than depending on someone remembering to check.

An SBOM for every image

A software bill of materials (SBOM) is a complete, machine-readable inventory of everything an image contains — every OS package and every application dependency, with versions. Generating one per image, and storing it as an attestation alongside the image, changes how you respond to the next big vulnerability.

When a critical CVE lands in a widely used library, the question "which of our images ship the affected version?" becomes a query against stored SBOMs instead of a frantic round of rebuilds and manual inspection. The SBOM is also the natural input to continuous scanning: re-evaluating a stored SBOM against fresh vulnerability data is exactly how a registry re-scan flags an image that was clean when it was built. Modern builders can emit an SBOM as part of the build, so it carries the same provenance guarantees as the image itself. This is where image security meets software composition analysis most directly — the SBOM is the shared inventory both rely on.

From image to what runs it

Everything above produces a trustworthy artifact; something still has to refuse the untrustworthy ones. In a Docker-centric workflow that enforcement lives in CI (fail the build) and at the registry (block or quarantine images that fail policy). When those images run under an orchestrator, the same signals — a valid signature, a passing scan, a digest reference rather than a mutable tag — become inputs to admission control, which can deny any pod whose image does not meet policy before it is ever scheduled. The Kubernetes image security guide covers that gate in detail. The important point is continuity: the signature you attach and the SBOM you generate at build time are the very evidence the admission layer checks at deploy time, so image security and deploy-time enforcement are two ends of one chain.

That is the pattern across every section here. Image security is not a single tool but a sequence of checkpoints — base image, build, scan, secret-check, sign, SBOM, store, pin — and a weakness at any link undermines the rest. The earlier in that sequence a problem is caught, the cheaper it is to fix, which is why so much of the work belongs in CI and at the registry rather than after deployment.

How Rainforest helps

Securing that whole chain is easier when the checkpoints share one view of your software. Rainforest brings container security and software composition analysis together so you can scan Docker images across both OS packages and application dependencies, surface secrets baked into layer history, generate an SBOM per image, and track findings from the pull request through to the registry. Because the same platform also assesses the dependencies your applications pull in and the infrastructure-as-code that deploys them, an image and everything around it are evaluated as one connected supply chain rather than in isolation — which is how software supply chain failures, tracked as category A03 in the OWASP Top 10 (2025), actually reach production.

If you are building out Docker image security, book a demo and we will walk through scanning, hardening, and provenance for your image supply chain end to end.

Frequently asked questions

How do I scan a Docker image for vulnerabilities?

Use a container image scanner built on software composition analysis (SCA) that inventories both OS packages and application dependencies and matches them against known CVEs — scanning only one layer leaves the other unexamined. Run the scan in CI so developers get feedback on the change that introduced a problem, and run it again at the registry on a schedule so already-published images are re-evaluated as new vulnerabilities are disclosed. Configure the pipeline to fail on new critical (and typically high) severity findings, and scope the gate to issues that have a fix available so it stays actionable.

What is image signing and why does it matter?

Image signing attaches a cryptographic signature to an image's digest, and provenance attestations record how the image was built (the SLSA model). Before anything trusts the image, it verifies that signature. This matters because scanning and registry access control cannot tell you an image actually came from your pipeline — an attacker who can push to your registry could substitute a malicious image under a legitimate name. A valid signature from a key your organization controls, checked at verification time, is what catches exactly that substitution.

Should I use image tags or digests?

Use immutable digests (app@sha256:...) for anything you deploy to production. A tag is a mutable pointer that can be repointed at different content, so an image you scanned and approved can silently become a different image under the same name; a digest is content-addressed and cannot change without the reference changing. Never deploy :latest, because it makes deploys non-reproducible and impossible to tie back to a specific scan. Human-readable release tags are fine for convenience as long as you treat them as immutable and pin the actual deploy by digest.

What is a distroless image?

A distroless image is a minimal base image that contains only your application and its runtime dependencies — no shell, no package manager, and none of the general-purpose utilities a normal Linux distribution ships. That shrinks the attack surface (fewer packages means fewer CVEs for a scanner to ever find) and makes post-exploitation much harder, because an attacker who gains code execution has no shell to pivot with. Combine it with a non-root user and a multi-stage build so compilers and build tooling stay out of the final image entirely.

How do I find secrets baked into an image?

Look at layer history, not just the final filesystem, because a secret added in one layer survives there even if a later layer deletes it. Start with docker history --no-trunc to catch secrets passed through build arguments or ENV, then use docker save to unpack the image into its per-layer tarballs and search each one for keys, tokens, and .env files. Better, run automated secret detection in your pipeline so every layer is scanned as part of the same gate as SCA. If you find one, rebuild without the secret in any layer, rotate the exposed credential, and use build secret mounts so the value never enters an image again.

Bruno Baldo

Written by

Bruno Baldo

CMO

Um pouco de marketing e um pouco de curiosidade e temos a receita pra criar um apaixonado por cyber!

Keep reading