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:
- CISA KEV status -- If a CVE is on the CISA KEV catalog, it is being actively exploited. Fix it immediately.
- EPSS percentile -- CVEs with high EPSS scores are statistically likely to be exploited. Prioritize them over equally-scored CVEs with low EPSS.
- Confidence tier -- Findings classified as ConfirmedInstalled are more trustworthy than HeuristicUnverified findings. Start remediation with confirmed findings.
- 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.
Rollout Checklist: Adding Scanning to an Existing Pipeline
The eight practices above describe what a mature scanning program looks like. Getting there from zero is a separate problem: if you turn on a blocking gate against a codebase that has never been scanned, you will block every build on day one and the gate will be switched off by the end of the week. This checklist covers the sequencing -- the order to turn things on, and the organisational decisions that have to be made before each step, rather than the technical practices themselves.
Phase 1 — Observe (do not fail anything yet)
- Run the scanner in report-only mode on every build and publish the report as a CI artifact. Nothing blocks, nothing warns in chat.
- Record the finding count for each image on the first run. This is your baseline; every later decision is measured against it, not against zero.
- Scan the base image on its own, separately from your built image. The difference between the two counts tells you how much of the problem is yours versus inherited.
- Confirm the scanner is reading the package database, not guessing from file paths — a report full of heuristic matches will poison the baseline.
Phase 2 — Decide the rules before enforcing them
- Name an owner per image. An unowned image will never be remediated, no matter what the gate says.
- Write down which signals block and which only warn (for example KEV status and confidence tier block; raw CVSS warns). Put it in the repo, not in someone's head.
- Agree the fix-by window for each severity band with the teams who will have to meet it, and write it down. A window nobody agreed to is a window nobody meets.
- Define the exception path: who approves an accepted risk, where the acceptance is recorded, and when it is revisited.
- Require every suppression to carry an expiry date and a reason. Suppressions without expiry are how a scanning program silently dies.
Phase 3 — Enforce narrowly, then widen
- Turn the gate on for newly introduced findings only; let the pre-existing baseline through. Teams can stop the bleeding before they pay down debt.
- Start with one service that has an engaged owner rather than the whole estate. Fix the workflow friction there before it becomes everyone's friction.
- Fail the build on the policy, not on the tool: a scanner crash, a rate-limited API, or an empty database should be a distinguishable failure from a real policy violation.
- Ratchet the baseline downward on a schedule instead of demanding a single big cleanup.
Phase 4 — Keep it alive
- Re-scan already-published images on a schedule. New CVEs are disclosed against images you are not rebuilding.
- Alert on the rate of change, not the absolute count — a jump in findings between two consecutive builds is a signal; a large steady-state number is just your base image.
- Review expired suppressions on a recurring cadence and treat an expired one as a finding again.
- Track median time-to-remediate for the findings that actually blocked something. If it is climbing, the policy is too broad and people are routing around it.
- Re-run the base image comparison periodically — a base image that was the lean choice at adoption may not be a year later.
Adoption sequencing checklist for container scanning programs. Structural guidance only -- no thresholds or timings are prescribed here, because the right ones depend on your release cadence and risk appetite.
Get Started
curl -fsSL https://scanrook.sh/install | bash
scanrook scan --file ./image.tar --format json --out report.json