← writing

scan the image you actually deploy

I don't consider a container ready to deploy because the source scan passed. I want the digest of the image that's going to run, and the scan result attached to that digest.

Open server component on a dark workbench under an amber inspection lamp.
Generated illustration of hardware inspection.

A repository and a built image contain different things. The build can add operating-system packages, transitive dependencies, files from another stage and binaries fetched from the network. It can also leave out development dependencies that were present in the source. Reviewing the diff and scanning the repository are useful, but neither gives you a complete account of what the orchestrator is about to pull.

This is especially easy to lose track of when an AI tool writes the application, Dockerfile and deployment workflow together. The endpoint works, the tests pass, and the surrounding choices can slip through as part of the setup. A base image was chosen. Packages were downloaded. Someone, or something, decided which user the process runs as. Those choices deserve review even if you barely had to touch the files yourself.

I don't think “vibe coding” changes the reason to scan images. It does shorten the gap between getting a working application and wanting to ship it. The final artifact still needs an owner who can account for its contents.

a tag isn't a record of what you scanned

Suppose CI scans app:latest and deployment pulls app:latest later. The tag can point to a different image by then. Both steps can succeed, and the release can still deploy something the scanner never examined.

Build the candidate once, attach the SBOM and provenance, push to a controlled registry and resolve the resulting digest. Scan that reference and carry it through promotion and deployment. Kubernetes supports pulling by digest, so you can identify the particular image version rather than depend on what a tag happens to mean at pull time.

docker buildx build --sbom=true --provenance=true \
  -t registry.example/app:build-123 --push .

# resolve the pushed manifest digest in CI, then use that exact reference
trivy image --exit-code 1 --severity HIGH,CRITICAL \
  registry.example/app@sha256:<resolved-digest>

That command is only a sketch. The digest needs to come from the pushed artifact; resolving a local tag may give you a different reference. For a multi-architecture build, cover the platform variants that production can schedule. Check that the registry retains the attestations and that release tooling verifies them. An SBOM printed during CI but disconnected from the deployed artifact won't help much when you need to establish what a running image contains.

An inventory should account for operating-system and language packages wherever possible. Provenance connects the artifact to its source revision and build workflow. For signed images, verification also needs to check the expected signing identity and issuer. A signature from a builder you haven't approved is still the wrong signature for this release. Sigstore supports those identity checks; admission policy can enforce the resulting requirements when a workload enters the cluster.

reading the report

A high-severity CVE needs investigation, but a count of high-severity CVEs isn't a complete release decision. Is the affected component reachable in this workload? Is a fix available? Does the package come from the base image or application? Vendor backports can make a version-string comparison misleading, so it's worth checking the package's actual advisory context before deciding what to do with it.

Exceptions are sometimes necessary. Give each one an affected digest, a reason, an owner and an expiry. A permanent global ignore rule is easier to add than to remember, especially after it has made a failing pipeline green again.

Comparing the candidate with the last approved digest helps identify new findings. A new critical issue should interrupt the release, while an existing one may already have an agreed remediation deadline. Keep the deadline attached to that decision. Otherwise the baseline gradually becomes a list of problems everyone has stopped looking at. Approved images also need ongoing rescanning: new advisories can change the risk without changing a byte of the artifact.

Package matching has blind spots. A statically linked binary, vendored JavaScript bundle or downloaded model runtime may not look like an ordinary installed package to the scanner. Unexplained files in the final layers deserve a look, especially when a build step fetches them from the network. Compare the filesystem with what you expected the application to need. A report with no findings is less reassuring if the tool didn't recognize a substantial part of the image.

There are also mistakes a CVE report wasn't meant to catch. A secret deleted in a later Dockerfile layer can remain in the earlier layer. Use build-time secret mounts instead of ARG or ENV for credentials, check the build context and layers, and exclude files the build doesn't need. Review the runtime user, entrypoint, writable filesystem, Linux capabilities and network access. Broad host permissions on a process running as root don't become acceptable because its packages happen to have no reported vulnerabilities.

the deploy path has to respect the result

A pull-request comment saying “scan passed” doesn't enforce anything on its own. If the manual deployment path accepts an arbitrary tag, somebody can deploy a different artifact. Have the release controller accept approved digests and retain the approval for each one. Admission can reject mutable tags or unsigned images where the policy requires it. An emergency exception needs the same ownership, expiry and audit trail as any other bypass.

Decide what a scanner outage means before one interrupts a release. No result shouldn't silently count as a clean result. At the same time, you don't need to run expensive vulnerability matching synchronously for every pod start. Do that work in CI and continuous registry rescanning, then let admission make a quick check of artifact identity and approval status. That avoids making every startup depend on a live advisory service.

Record the policy version and scan time with the digest. When an advisory or policy changes, those fields let you decide which earlier approvals are still valid and which need reviewing. Without them, “approved” tells you very little about what was checked or when.

I would test this with a deliberately vulnerable fixture in a non-production environment. Follow it through CI, promotion and admission, and confirm that each required gate blocks it. Repeat with a clean image and a time-limited exception. That catches gaps between tools, including a release script that ignores the scanner's exit code or an admission rule that never matches the workload.

Keep the record after deployment. When a new advisory arrives, you need to connect the affected digest to the workloads still using it. If the only surviving identifier is a mutable tag, someone has to reconstruct which build it referred to at the time. That's avoidable work during an investigation, and a good reason to insist on the digest before the first release.

technical references: trivy filtering and severity ↗, docker build attestations ↗, kubernetes image digests ↗, kubernetes security checklist ↗, and sigstore verification ↗.