How to Scan Container Image Layers and the Docker Malware Feed

Advanced 20 minutes Platform Engineers / Security Engineers Security & Policy

Catch vulnerabilities hidden beneath a clean manifest with per-layer Trivy scanning, and close the OpenSSF index gap for containers with the Docker-specific feed.

Overview

Container images are two classes of supply-chain blind spot wrapped in one artifact:

  1. Manifest-only scans miss layers. An attacker can publish an image whose top-level manifest references clean base layers and sneak a compromised binary into a lower layer that the manifest-level scan never reaches.
  2. OpenSSF malware index is empty for Docker. The community OSV malicious-packages feed publishes almost no Docker entries, so image-based malware has been invisible to the standard lookup.

Two features close both gaps:

  • Per-layer Trivy walk — every layer of every pulled image is unpacked and scanned for known CVEs, configured via CHAINSAW_DOCKER_LAYER_SCAN=on (default).
  • Docker-specific malware feed — Chainsaw ships an embedded seed of known-malicious image digests plus an optional remote feed URL.

Prerequisites

  • Chainsaw admin access to environment configuration
  • Docker / OCI repositories proxied through Chainsaw
  • Sufficient disk for layer unpacking (sized by the layer cap, see Step 2)

Step 1: Confirm Layer Scanning Is On

Layer scanning is on by default. Verify via the /api/settings endpoint or the environment:

echo $CHAINSAW_DOCKER_LAYER_SCAN
# expected: on (or empty, which defaults to on)

Layer scan timings are emitted at DEBUG log level, and the counter chainsaw_docker_layers_scanned_total tracks volume:

sum(rate(chainsaw_docker_layers_scanned_total[5m]))

Step 2: Tune the Layer Size Cap

Very large layers (1 GiB+ base images, ML model layers) can blow the scan budget. Tune via:

export CHAINSAW_DOCKER_LAYER_SIZE_CAP_BYTES=1073741824   # default 1 GiB

Layers larger than the cap are skipped with reason layer_over_size_cap; the manifest-level scan still runs. Raise the cap if your builds pull legitimately-large layers, lower it if you need a faster ingestion budget.

Step 3: Create a Vulnerability Policy That Covers Layers

The existing isVulnerable condition covers both the manifest-level and the per-layer scan — no separate condition is required. A Docker-scoped rule that blocks critical CVEs now catches vulnerabilities hidden in lower layers:

  1. Name: Block — critical CVE in Docker layer or manifest
  2. Condition: isVulnerable = true && cvssMin >= 9.0
  3. Scope: Docker / OCI repositories
  4. Action: Block

Step 4: Enable the Docker Malware Feed

The feed is on by default (cfg.EnableDockerMalware = true). The embedded seed covers publicly-disclosed known-malicious digests at build time. To pull a live remote feed on top of the seed, set:

export CHAINSAW_DOCKER_MALWARE_FEED_URL=https://feeds.example.com/docker-malware.json

The feed format matches the embedded seed: a JSON array of entries with digest (in sha256:... form) and optional name + tag fields. At load time, Chainsaw populates two lookup indices — dockerDigests (exact sha256 match) and dockerNames (name+tag fallback for tagged pulls that haven’t yet resolved to a digest).

Step 5: Verify Digest-First Lookup

The malware lookup takes a fast-path on digest pulls:

docker pull registry.example.com/foo/bar@sha256:deadbeef...

If that digest is in either feed, the request is blocked with a malware violation. Name+tag pulls (foo/bar:1.2.3) fall back to the name+tag table — it’s a secondary signal because tags are mutable upstream, so the digest index should remain the primary defence.

Step 6: Alert on Malicious Image Detections

# Prometheus alerting rule
- alert: ChainsawMaliciousDockerImage
  expr: sum(rate(chainsaw_malware_detections_total{ecosystem="docker"}[5m])) > 0
  for: 0m
  labels:
    severity: critical
  annotations:
    summary: "Chainsaw detected a known-malicious Docker image"
    description: "Check the traffic view for the image digest and identify the requesting client."

Ecosystem Notes

SignalSupport
Per-layer Trivy vulnerability scan✅ Docker / OCI
Docker malware feed (digest + name+tag)✅ Docker / OCI
Hidden-Unicode scan on layer text files❌ (deferred — separate surface)
hasInstallScript on a container image❌ (no lifecycle-script concept)
versionAnomaly on image tags❌ (tags are not SemVer)

For container images, the layered defence-in-depth stack is: isSuspectedTyposquat + isKnownMalicious (via the Docker feed) + isVulnerable (manifest + per-layer) + hasProvenance (Sigstore where the image is signed).

Next Steps