Best practices

Container Scanning Best Practices for Security Teams

Container images are the deployment unit for most modern applications. Scanning them for vulnerabilities is essential, but doing it effectively requires more than just running a tool. This article covers practical guidance for building a container scanning program that produces actionable results.

1. Choose Base Images Carefully

Your choice of base image determines the majority of your vulnerability surface. A full-featured base image likeubuntu:22.04 includes hundreds of system packages, many of which your application does not use. Each package is a potential source of vulnerability findings.

Minimal base images like Alpine, distroless, or scratch significantly reduce the installed package count. An Alpine-based image typically has 10-20 base packages compared to 100+ in a Debian or Ubuntu image. Fewer packages means fewer findings and less triage work.

When choosing a base image, consider:

  • How frequently the base image maintainer publishes updates with security patches.
  • Whether the base image includes only the packages your application actually needs.
  • The security track record of the distribution (patch cadence, advisory quality).

2. Use Multi-Stage Builds

Multi-stage Docker builds separate the build environment from the runtime environment. Build dependencies like compilers, development headers, and build tools stay in the build stage and are not included in the final image. This reduces the attack surface and produces cleaner scan results.

# Build stage
FROM golang:1.22 AS builder
WORKDIR /app
COPY . .
RUN go build -o server .

# Runtime stage
FROM alpine:3.19
RUN apk add --no-cache ca-certificates
COPY --from=builder /app/server /usr/local/bin/server
CMD ["server"]

In this example, the Go compiler and all build tools exist only in the builder stage. The final image contains only Alpine's base packages, CA certificates, and the compiled binary. A scanner that reads the final image's package state will report only the packages that are actually present at runtime.

3. Scan at Build Time and in the Registry

Scanning should happen at multiple points in the image lifecycle:

  • CI/CD pipeline -- Scan images after they are built but before they are pushed to the registry. This catches vulnerabilities before deployment and can gate releases.
  • Registry -- Scan images periodically in the registry to detect newly disclosed vulnerabilities in already-deployed images.
  • Developer workstation -- Enable developers to scan locally during development to catch issues before they reach CI.

ScanRook supports all three scenarios. The CLI can be integrated into GitHub Actions and GitLab CI pipelines, run against images in a registry, or used directly on a developer's machine.

4. Prioritize Findings Effectively

A container scan will almost always produce findings. The question is which ones to fix first. Sorting by CVSS score alone is a common starting point, but it misses important context. A more effective prioritization model combines multiple signals:

  1. CISA KEV status -- If a CVE is on the CISA KEV catalog, it is being actively exploited. Fix it immediately.
  2. EPSS percentile -- CVEs with high EPSS scores are statistically likely to be exploited. Prioritize them over equally-scored CVEs with low EPSS.
  3. Confidence tier -- Findings classified as ConfirmedInstalled are more trustworthy than HeuristicUnverified findings. Start remediation with confirmed findings.
  4. CVSS base score -- Within a priority tier, use CVSS to further rank findings by severity.

5. Update Base Images Regularly

Base image maintainers publish updated images as security patches become available. Rebuilding your images on a regular cadence (at least weekly for production workloads) ensures that your deployed containers include the latest patches. Many organizations automate this with scheduled CI builds that pull the latest base image tag and rebuild.

Pin your base image to a specific version or digest for reproducibility, but have an automated process to update that pin. Using a floating tag like alpine:3.19 gives you automatic patch updates, while pinning to a digest like alpine@sha256:abc... gives you reproducibility. Choose based on your operational needs.

6. Remove Unnecessary Packages

After installing your application and its dependencies, remove packages that are no longer needed. Package managers often install "recommended" or "suggested" packages that your application does not use. In Debian/Ubuntu images, use --no-install-recommends with apt-get. In Alpine, use --no-cache with apk add to avoid caching package index data.

7. Track Changes Between Builds with SBOM Diff

Generating an SBOM for each image build and diffing it against the previous version helps you understand exactly what changed. New packages, updated versions, and removed components are all visible in the diff. This is valuable for change tracking, compliance reporting, and understanding why a scan result changed between releases.

ScanRook supports SBOM import and diffing. Generate an SBOM during your CI build, store it as a build artifact, and compare it against the previous release to track supply chain changes over time.

8. Automate and Gate Deployments

The most effective container scanning programs integrate scanning into the deployment pipeline as a gate. Define a policy (e.g., no Critical CVEs with EPSS above 90th percentile, no CISA KEV findings) and block deployments that violate it. This ensures that vulnerable images do not reach production while still allowing teams to deploy quickly when their images are clean.

ScanRook's JSON output format makes it straightforward to parse scan results in CI scripts and enforce policy gates based on finding counts, severity levels, EPSS thresholds, or KEV status.

Get Started

curl -fsSL https://scanrook.sh/install | bash
scanrook scan --file ./image.tar --format json --out report.json