.NETMedium
Docker image security scanning — Trivy, Snyk, and the SBOM workflow
Every image you ship inherits hundreds of OS packages and library deps. Scanning surfaces known CVEs before they become a breach.
The two scan layers
- OS packages in the base image (Debian, Alpine, distroless).
- Application dependencies (npm, pip, NuGet, Maven).
Trivy and Snyk both cover both.
Trivy — quick start
# Pull and scan
trivy image --severity HIGH,CRITICAL --exit-code 1 myorg/api:1.0.3
CI gate (GitHub Actions):
- name: Scan image
uses: aquasecurity/trivy-action@master
with:
image-ref: ${{ steps.build.outputs.image }}
format: sarif
output: trivy.sarif
severity: HIGH,CRITICAL
exit-code: 1
- uses: github/codeql-action/upload-sarif@v3
with: { sarif_file: trivy.sarif }
SBOM (Software Bill of Materials)
A machine-readable inventory of every component shipped. Generate at build time, store with the image.
syft myorg/api:1.0.3 -o spdx-json=sbom.json
# Sign and attach to the registry
cosign attest --predicate sbom.json myorg/api:1.0.3
When a new CVE drops next year, you query SBOMs instead of rescanning every image.
Reducing attack surface — the four levers
| Lever | Effect |
|---|---|
| Distroless / scratch base | Drops shell, apt, and 90% of packages |
| Multi-stage build | Dev tools live in builder stage, never in final |
| Pin base by digest | FROM debian:bookworm@sha256:abc... reproducible + auditable |
| Drop root | USER 1000 in Dockerfile; deny --privileged |
Multi-stage example
FROM mcr.microsoft.com/dotnet/sdk:8.0 AS build
WORKDIR /src
COPY . .
RUN dotnet publish -c Release -o /out
FROM mcr.microsoft.com/dotnet/aspnet:8.0-distroless
WORKDIR /app
COPY --from=build /out .
USER 1000
ENTRYPOINT ["dotnet", "Api.dll"]
Production playbook
- Block builds on HIGH/CRITICAL CVEs with no upstream fix > 30 days.
- Rebuild + redeploy weekly even if app code did not change — base-image CVEs accumulate.
- Use registry policy (Harbor, ACR with Defender, ECR scan-on-push) as a backstop.