Skip to content
Home Cloud & Infrastructure Security Container Image Security
Guide

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.

Suphi Cankurt
Suphi Cankurt
AppSec Enthusiast
Updated February 11, 2026
8 min read
0 Comments

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:

  1. Build the image in your CI pipeline
  2. Scan the image before pushing to a registry
  3. Fail the build if critical or high-severity CVEs are found
  4. Push to the registry only if the scan passes
  5. 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 latest tag
  • 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?
Container image security is the practice of scanning, hardening, and verifying container images to remove vulnerabilities before they reach production. It covers base image selection, Dockerfile best practices, vulnerability scanning in CI/CD pipelines, image signing for provenance, and admission control to block unverified images at deploy time.
How do I scan container images for vulnerabilities?
Run a scanner like Trivy, Grype, or Snyk Container against your image. The scanner compares installed packages against vulnerability databases (NVD, GitHub Advisory Database, vendor feeds) and reports known CVEs with severity ratings. Most scanners support both local images and remote registries. Add the scan to your CI/CD pipeline so every build gets checked automatically.
What is the most secure base image for containers?
Distroless images from Google and Chainguard Images are the most secure options. They contain only the application runtime with no shell, package manager, or system utilities. A distroless Python image has roughly 20 packages versus 400+ in a standard Python image. Alpine Linux is a lighter alternative with a smaller package set than Debian or Ubuntu, but it still includes a shell and package manager.
How often should I rebuild container images?
Rebuild at minimum weekly. New CVEs are published daily, and your base image’s OS packages accumulate known vulnerabilities over time. Many teams rebuild nightly and redeploy if the scan results change. Automated pipelines that trigger rebuilds when base image updates are published provide the tightest feedback loop.
What is image signing and why does it matter?
Image signing uses cryptographic signatures to verify that a container image was built by a trusted source and has not been tampered with. Tools like Cosign (from the Sigstore project) and Docker Content Trust / Notary handle signing and verification. Without signing, you have no guarantee that the image you pull is the same one your CI pipeline built.
Can I block unsigned or vulnerable images from running in Kubernetes?
Yes. Kubernetes admission controllers like Kyverno, OPA Gatekeeper, and Sigstore Policy Controller can reject pods that reference unsigned images or images with critical vulnerabilities. Configure them to block deployments that fail your security checks.
Suphi Cankurt
Written by
Suphi Cankurt

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.