Container Security Scanning Cheat Sheet
Tools and workflows for scanning container images for vulnerabilities, misconfigurations, and secrets before deployment.
Trivy Image Scan
Scan a Docker image for OS and library vulnerabilities.
# Basic scan, fail on HIGH/CRITICALtrivy image --severity HIGH,CRITICAL --exit-code 1 myapp:1.2.0# Scan a Dockerfile for misconfigurationstrivy config ./Dockerfile# Scan for exposed secrets in an imagetrivy image --scanners secret myapp:1.2.0
Grype Vulnerability Scan
Anchore's Grype scanner for images and filesystems.
grype myapp:1.2.0 --fail-on high# Scan a local directory instead of an imagegrype dir:./build# Output SBOM-style JSON for CI artifactsgrype myapp:1.2.0 -o json > scan-results.json
Where to Scan in the Pipeline
Shift-left points for container security checks.
- Pre-commit / IDE- Lint Dockerfiles (hadolint) before code is even pushed
- CI build stage- Scan the built image and fail the pipeline on critical CVEs
- Registry admission- Block pushes/pulls of unscanned or non-compliant images
- Runtime (admission controller)- Kubernetes admission webhook (e.g. OPA/Gatekeeper) rejects unscanned images at deploy time
Hardened Dockerfile Snippet
Common hardening practices to reduce attack surface.
FROM node:18-alpine# Run as non-root userRUN addgroup -S appgroup && adduser -S appuser -G appgroupWORKDIR /appCOPY --chown=appuser:appgroup . .RUN npm ci --only=productionUSER appuserEXPOSE 3000CMD ["node", "server.js"]
Key Concepts
Terms commonly encountered in container security tooling.
- SBOM (Software Bill of Materials)- Machine-readable inventory of all components/packages in an image, e.g. produced by Syft
- Base image minimization- Prefer distroless or alpine images to shrink the vulnerability surface
- CVSS score- Standardized severity score (0-10) used to prioritize remediation
- Admission controller- Kubernetes component that can accept/reject resources at creation time based on policy
Cosign: Sign and Verify Image Provenance
Sign images in CI and enforce signature verification at deploy time (sigstore/cosign).
# Keyless signing using OIDC identity (GitHub Actions, GitLab CI, etc.)COSIGN_EXPERIMENTAL=1 cosign sign myregistry.example.com/myapp@sha256:abc123...# Verify signature came from a trusted CI identitycosign verify \ --certificate-identity-regexp "^https://github.com/myorg/.*" \ --certificate-oidc-issuer https://token.actions.githubusercontent.com \ myregistry.example.com/myapp@sha256:abc123...# Attach and verify an SBOM attestationcosign attach sbom --sbom sbom.spdx.json myregistry.example.com/myapp@sha256:abc123...cosign verify-attestation --type spdx myregistry.example.com/myapp@sha256:abc123...
Suppressing False Positives with VEX
Use a .trivyignore or VEX document to suppress a CVE that is confirmed non-exploitable in your build.
# .trivyignore — suppress a specific CVE with a documented reasoncat > .trivyignore <<'EOF'# CVE-2023-1234: affects a code path we never call (see JIRA-4821)CVE-2023-1234EOFtrivy image --ignorefile .trivyignore --exit-code 1 --severity HIGH,CRITICAL myapp:1.2.0# Generate a machine-readable VEX statement instead of a blanket ignoretrivy image --format openvex myapp:1.2.0 > myapp.openvex.json
OPA/Gatekeeper Constraint: Block Unsigned Images
Admission-time policy requiring images to come from an approved registry with a digest pin.
apiVersion: constraints.gatekeeper.sh/v1beta1kind: K8sAllowedReposmetadata: name: approved-registries-onlyspec: match: kinds: - apiGroups: [""] kinds: ["Pod"] parameters: repos: - "myregistry.example.com/"---# Companion Rego snippet enforcing digest pinning (no floating tags)package k8sdigestpinviolation[{"msg": msg}] { container := input.review.object.spec.containers[_] not contains(container.image, "@sha256:") msg := sprintf("image %v must be pinned by digest, not tag", [container.image])}
Scanner Technique Trade-offs
How different scanning approaches catch (or miss) different classes of risk.
- Static package DB scanning- Trivy/Grype match installed packages against CVE feeds — fast, but blind to runtime behavior and zero-days not yet in the feed
- SBOM diffing- Comparing SBOMs across builds surfaces newly introduced dependencies even before a CVE is assigned
- Runtime behavioral scanning- Tools like Falco detect anomalous syscalls/process trees at runtime, catching exploitation static scans miss
- Secret entropy scanning- Detects high-entropy strings/API-key patterns baked into layers, independent of CVE databases
- License compliance scanning- Flags copyleft/incompatible licenses pulled in transitively — a supply-chain risk, not a CVE
- Layer-aware diffing- Scanning per-layer instead of the flattened final image pinpoints which Dockerfile instruction introduced a vuln
Generate and Attach an SBOM with Syft
Produce a full software bill of materials as part of the build, not as an afterthought scan.
# Generate SPDX-format SBOM from the built imagesyft myapp:1.2.0 -o spdx-json=myapp-sbom.spdx.json# Generate CycloneDX for tooling that expects it (e.g. Dependency-Track)syft myapp:1.2.0 -o cyclonedx-json=myapp-sbom.cdx.json# Fail CI if the SBOM shows a banned packagejq -e '.artifacts[] | select(.name=="log4j-core")' myapp-sbom.spdx.json && exit 1 || true
Pin base images by digest, not just tag, in production Dockerfiles — a mutable tag like `node:18-alpine` can silently change under you and reintroduce a vulnerability you already remediated.