Container Image Security
How to build and maintain secure container images. Covers base image selection, vulnerability scanning in CI/CD, image hardening, registry security, and supply chain integrity.
Why container images are a security problem
Every container image is a frozen snapshot of an operating system, libraries, and your application code. That snapshot includes whatever vulnerabilities existed at build time, and it stays frozen until you rebuild. An image built three months ago might have dozens of newly discovered CVEs that nobody has patched.
The scale makes manual tracking impossible. A typical production environment runs hundreds of unique images, each containing anywhere from 50 to 500 OS packages and an equal number of application dependencies. Sysdig’s 2024 Container Security Report found that 87% of container images in production contain at least one high or critical vulnerability. The average image carries 230+ known vulnerabilities.
The problem compounds because images are shared. A vulnerable base image gets inherited by every application image built on top of it. If your organization standardizes on python:3.12-slim and that image picks up a critical OpenSSL CVE, every Python service inherits the risk.
Choosing secure base images
Your base image determines the security floor for everything built on top of it. The fewer packages included, the fewer vulnerabilities you inherit.
Distroless images
Google’s distroless images contain only the language runtime and its dependencies. No shell, no package manager, no coreutils. A distroless Python image has roughly 20 packages compared to 400+ in python:3.12. Fewer packages means fewer CVEs and less surface area for attackers.
The tradeoff is debuggability. Without a shell, you cannot exec into a running container to poke around. You can work around this with debug variants for non-production environments or ephemeral debug containers in Kubernetes.
Chainguard Images
Chainguard builds minimal container images using the Wolfi Linux distribution, designed specifically for containers. Every image is rebuilt daily, signed with Cosign, and ships with SBOMs. Chainguard claims zero known CVEs at time of build. The free developer images cover common runtimes (Node, Python, Go, Java). Enterprise images cover a wider catalog.
Alpine Linux
Alpine uses musl libc and busybox, resulting in base images around 5 MB. It has far fewer packages than Debian or Ubuntu, which reduces vulnerability counts. Alpine images are a good middle ground: smaller attack surface than full distributions, but you still have a shell and apk package manager for debugging.
Watch out for compatibility issues. Some applications behave differently on musl versus glibc. Test thoroughly before switching from Debian to Alpine.
What to avoid
Avoid latest tags on any base image. They give you no reproducibility. An image built today and one built tomorrow can be completely different even though they use the same tag. Pin to a specific digest or version tag and update deliberately.
Avoid full distribution images like ubuntu:22.04 or debian:bookworm unless you need specific packages from them. They carry hundreds of packages your application does not use, each one a potential vulnerability.
Scanning images in CI/CD
Image scanning belongs in your build pipeline. Every image should be scanned before it reaches a registry, and again periodically after that because new CVEs surface daily.
Tool options
Trivy is the most widely adopted open-source image scanner. It checks OS packages, language-specific dependencies, IaC misconfigurations, and secrets in a single binary. Trivy supports Docker, OCI, and Podman images. It pulls vulnerability data from NVD, GitHub Advisories, and vendor-specific feeds. 31,700+ GitHub stars. See our Trivy vs Snyk Container comparison for a detailed evaluation.
Grype (from Anchore) is another open-source option focused specifically on vulnerability scanning. It uses the same vulnerability database as Anchore Enterprise and produces clean, parseable output. Good if you want a dedicated scanner without Trivy’s broader scope.
Snyk Container integrates image scanning into the Snyk platform. It provides fix advice (suggesting which base image version resolves the most CVEs), monitor mode for deployed images, and integration with Snyk’s SCA and code analysis. Commercial product with a free tier.
CI/CD integration pattern
The typical setup:
- Build the image in your CI pipeline
- Scan the image before pushing to a registry
- Fail the build if critical or high-severity CVEs are found
- Push to the registry only if the scan passes
- Optionally sign the image after a clean scan
A GitHub Actions example with Trivy:
- name: Build image
run: docker build -t myapp:${{ github.sha }} .
- name: Scan image
uses: aquasecurity/trivy-action@master
with:
image-ref: myapp:${{ github.sha }}
severity: CRITICAL,HIGH
exit-code: 1
Scanning frequency
Pipeline scans catch issues at build time. But images sitting in your registry accumulate new vulnerabilities as CVEs are published. Schedule registry scans at least daily. Trivy, Grype, and Snyk Container all support scanning images already pushed to registries like Docker Hub, Amazon ECR, Google Artifact Registry, and Azure Container Registry.
Hardening Dockerfiles
Scanning catches known vulnerabilities in packages. Hardening your Dockerfiles addresses a different set of problems: misconfigurations that no CVE database tracks.
Multi-stage builds
Use multi-stage builds to separate build dependencies from the runtime image. Your build stage can include compilers, build tools, and test frameworks. Your final stage copies only the compiled binary or application artifacts.
FROM golang:1.22 AS builder
WORKDIR /app
COPY . .
RUN go build -o myapp
FROM gcr.io/distroless/static-debian12
COPY --from=builder /app/myapp /
CMD ["/myapp"]
The final image contains only the binary and the distroless base. No Go compiler, no source code, no build tools.
Run as non-root
By default, containers run as root. If an attacker exploits your application, they get root inside the container. Combined with a container escape vulnerability, that becomes root on the host.
Always set a non-root user:
RUN addgroup -S appgroup && adduser -S appuser -G appgroup
USER appuser
Minimize installed packages
Every package you install is a potential vulnerability. Install only what your application needs. Use --no-install-recommends with apt or --no-cache with apk. Remove package manager caches after installation.
RUN apt-get update && \
apt-get install -y --no-install-recommends libpq5 && \
rm -rf /var/lib/apt/lists/*
Do not embed secrets
Never COPY or ENV credentials, API keys, or certificates into an image. Image layers are permanent and accessible to anyone who pulls the image. Use runtime secret injection through Kubernetes secrets, Vault, or your cloud provider’s secret manager.
Scanners like Trivy detect hardcoded secrets in image layers, but prevention is better than detection.
Use .dockerignore
A .dockerignore file prevents sensitive files from being copied into the build context. Include .env, .git, *.pem, credentials.json, and anything else that should not be in an image.
Registry security and image signing
Your container registry is a high-value target. An attacker who compromises it can push malicious images that your infrastructure pulls and runs.
Registry access controls
Limit who can push images. Use service accounts for CI pipelines with narrow push permissions scoped to specific repositories. Require authentication for all pulls in production environments. Disable anonymous access.
Enable vulnerability scanning in the registry itself. Amazon ECR, Google Artifact Registry, Azure Container Registry, Docker Hub, and Harbor all support automated scanning on push.
Image signing with Cosign
Cosign from the Sigstore project makes image signing practical. It signs images with ephemeral keys backed by certificate transparency logs, eliminating the need to manage long-lived signing keys.
cosign sign --yes myregistry.com/myapp:v1.2.3
Verification happens at pull time or through admission controllers:
cosign verify myregistry.com/myapp:v1.2.3
SBOM generation
Software Bill of Materials (SBOMs) document everything inside an image. Generate SBOMs at build time with Trivy (trivy image --format spdx-json) or Syft and attach them to the image in the registry. SBOMs let you answer “which of our images contain Log4j?” in minutes instead of days.
For more on SBOMs, see our guide on what is SBOM.
Runtime image policies
Scanning and signing happen at build time. Admission controllers enforce policies at deploy time – the last check before a workload runs in your cluster.
Kubernetes admission controllers
Kyverno uses declarative YAML policies. Easy to write and maintain. A policy to block images from untrusted registries:
apiVersion: kyverno.io/v1
kind: ClusterPolicy
metadata:
name: restrict-image-registries
spec:
validationFailureAction: Enforce
rules:
- name: validate-registries
match:
any:
- resources:
kinds:
- Pod
validate:
message: "Images must come from approved registries"
pattern:
spec:
containers:
- image: "myregistry.com/*"
OPA Gatekeeper uses Rego policies. More expressive than Kyverno but steeper learning curve.
Sigstore Policy Controller specifically verifies image signatures and attestations. Use it alongside Kyverno or Gatekeeper if you want signature-based admission control.
What to enforce
At minimum, enforce these at the admission controller level:
- Images must come from approved registries
- Images must not use the
latesttag - Images must be signed by your CI pipeline
- Images must not run as root (where feasible)
Common vulnerabilities in container images
Understanding what scanners find helps you prioritize and triage results.
OS package vulnerabilities
These are CVEs in packages like OpenSSL, glibc, curl, zlib, and other system libraries. They make up the bulk of scanner findings. Many are in packages your application does not directly use but that exist in the base image. The fix is usually upgrading the base image or the specific package.
Language dependency vulnerabilities
NPM packages, Python packages, Go modules, Java JARs. These are the same CVEs that SCA tools find in your source code, but in the context of what is actually installed in the runtime image. Trivy and Snyk Container both scan language dependencies inside images.
Misconfigurations
Running as root, exposed ports that should not be open, writable filesystem when read-only would work, missing health checks. These are not CVEs but they weaken the security posture of the container.
Embedded secrets
API keys, database passwords, certificates, SSH keys accidentally baked into image layers. Even if you delete the file in a later layer, the secret persists in the layer history.
Malicious base images
Typosquatting on Docker Hub. An image named pythonn or node-js that looks like an official image but contains a cryptominer or backdoor. Always use official images or verified publishers, and always pull by digest in production.
FAQ
For more on cloud infrastructure security, see our Cloud Infrastructure Security hub and the IaC Security guide.
This guide is part of our Cloud & Infrastructure Security resource hub.
Frequently Asked Questions
What is container image security?
How do I scan container images for vulnerabilities?
What is the most secure base image for containers?
How often should I rebuild container images?
What is image signing and why does it matter?
Can I block unsigned or vulnerable images from running in Kubernetes?

Suphi Cankurt is an application security enthusiast based in Helsinki, Finland. He reviews and compares 129 AppSec tools across 10 categories on AppSec Santa. Learn more.
Comments
Powered by Giscus — comments are stored in GitHub Discussions.