Skip to content

ADR-017: Image Scan Gates on Fixable Vulnerabilities, Reports All

Field Value
Date 2026-07-29
Status Accepted
Deciders Holger Zahnleiter
Supersedes
Superseded by

Context and Problem Statement

Once scan-image could authenticate against the registry, it started doing its job — and immediately blocked every merge to the default branch for five of six artifacts in the reference mono-repo. The findings split into two categories that the gate treated identically:

Fixable. The Java services' app.jar carried outdated dependencies with published fixes: spring-boot 4.0.3 (fixed in 4.0.6), tomcat-embed-core 11.0.18 (fixed in 11.0.22), jackson-databind and jackson-core (fixed in 2.21.4 / 3.1.4). Acting on these is ordinary dependency maintenance.

Not fixable. The gcr.io/distroless/java25-debian13 base image ships libexpat1 2.7.1-2 and libuuid1 2.41-5. Six HIGH CVEs against them report status affected with an empty fixed version — Debian has not published a patched package. No change in the generated repository can resolve them. Rebuilding, bumping dependencies, or switching base image variants does not help; only a Debian release does.

The sixth artifact passed only because a static Go binary on distroless/static-debian12 carries almost no OS packages at all — not because it is more secure, but because it has less surface to report on.

A gate that cannot be made green regardless of what anyone does is not a gate. It is a permanently red job that teaches the team to merge past a failing pipeline, which then also hides the fixable findings sitting next to it.

Decision Drivers

  • security.md: "run Trivy in CI; fail on high-severity vulnerabilities" — the intent is to block shipping known-vulnerable code, not to block on facts outside the repository's control.
  • A red gate must be actionable. If the only way to clear it is to wait for a third party, it cannot gate a merge.
  • Suppressing findings from the report would be worse than the problem: operators still need to know what their image is exposed to, fixable or not.

Considered Options

  1. Report everything; gate only on vulnerabilities with a published fix — chosen.
  2. Keep gating on all HIGH/CRITICAL findings.
  3. Maintain a .trivyignore listing the specific unfixed CVEs.
  4. Raise the severity threshold so fewer findings block.

Decision Outcome

Chosen: option 1. Each image-scan job runs Trivy twice against the same image:

- trivy image --image-src remote --severity HIGH,CRITICAL --exit-code 0 "$IMAGE"
- trivy image --image-src remote --severity HIGH,CRITICAL --ignore-unfixed --exit-code 1 "$IMAGE"

The first pass is the complete picture and never fails the job. The second decides the job's outcome and considers only what someone can act on today. The vulnerability database is fetched once and cached within the job, so the second pass costs seconds. make scan-image mirrors both passes, so a local run and CI agree.

This applies to scan-image and scan-release-image in all six templates that build an image. The trivy fs source scan is deliberately left unchanged — it is currently green, and changing a passing gate on speculation is not warranted. It carries the same latent problem and should get the same treatment the first time an unfixed source-dependency CVE blocks it.

Positive Consequences

  • The gate is satisfiable, so a red scan-image again means "someone must do something".
  • Fixable findings stop being buried under unfixable ones.
  • Full exposure remains visible in the job log and is not silently dropped.

Negative Consequences / Risks

  • An unfixed CRITICAL in a base image no longer blocks a release. This is a deliberate trade: blocking on it never removed the vulnerability, it only removed the signal. Base-image currency is a separate, ongoing concern — the pinned tags must still be advanced regularly.
  • Unfixed findings are reported but not enforced, so nothing forces anyone to read them. Reviewing the report pass is a habit the team has to keep, not something the pipeline can guarantee.

Rejected alternatives

  • Option 2 leaves the pipeline permanently red for every Debian- or Alpine-based image, which is every artifact except a static Go binary.
  • Option 3 requires editing a suppression file in every generated repository each time a CVE is published, and stale entries silently suppress findings that later became fixable. The --ignore-unfixed rule expresses the same intent without a list to maintain.
  • Option 4 would hide fixable HIGH findings too, which is precisely the signal worth keeping.
  • specs/guidelines/security.md, specs/guidelines/ci_cd.md.
  • ADR-016 — the same scan-image job; that ADR fixed its registry authentication.
  • Trivy's --ignore-unfixed: https://trivy.dev/docs/configuration/filtering/