'Chainguard' image for secure service
Source: Dev.to
If you work in DevOps or system‑backend development, one of the biggest sources of stress is security. Even though it’s always at the top of the priority list, its substance often feels elusive.
Assume you’re maintaining a Kubernetes cluster or building Docker images (most DevOps engineers deal with at least one of these). Even if your own code is secure, the base image — Debian, Ubuntu, Alpine, etc. — often carries technical debt, and that debt can contain security risks that compromise your service. This is where Chainguard images come in.
What is Chainguard?
In short, Chainguard Images are “secure‑by‑default” container images built on top of Wolfi, a Linux “undistro” designed specifically for containers.
Unlike standard Docker Hub images, Chainguard images have a few distinct characteristics:
- Distroless – They contain only the bare minimum required to run the app. No shell, no package manager (in the runtime), and no unnecessary bloat.
- Daily Rebuilds – Every image is rebuilt daily from upstream sources to patch vulnerabilities immediately.
- SBOMs & Signing – They ship with a Software Bill of Materials and Sigstore signatures out of the box.
The Showdown: Official vs. Chainguard
The most immediate benefit is a reduction in noise. Below is a comparison I ran with Trivy on a standard Python image and the Chainguard equivalent.
| Image | Vulnerabilities |
|---|---|
Official (python:3.11) | > 300 vulnerabilities, including “Critical” and “High” severity |
Chainguard (cgr.dev/chainguard/python:latest) | 0 CVEs |
This isn’t magic; it’s aggressive minimalism. By removing OS components your application doesn’t use, you shrink the attack surface dramatically.
Hands‑On: Migration Guide
Migrating isn’t always a simple swap, especially with Chainguard’s distroless images. Because they lack a shell and package manager at runtime, you must use multi‑stage builds.
Tool Restriction
A typical (and slightly vulnerable) Dockerfile might look like this:
# Standard Python Image
FROM python:3.9-slim
WORKDIR /app
# Installing dependencies
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY . .
# Running as root (default)
CMD ["python", "app.py"]
Migrated Version with Chainguard
# 1️⃣ Builder stage
# Use the `-dev` tag because we need a shell and build tools (gcc, headers, etc.)
FROM cgr.dev/chainguard/python:latest-dev AS builder
WORKDIR /app
# Create a virtual environment
RUN python -m venv /app/venv
ENV PATH="/app/venv/bin:$PATH"
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
# 2️⃣ Runtime stage
# This image has no shell or package manager; it is pure runtime.
FROM cgr.dev/chainguard/python:latest
WORKDIR /app
# Copy the virtual environment from the builder
COPY --from=builder /app/venv /app/venv
COPY . .
# Use the virtualenv
ENV PATH="/app/venv/bin:$PATH"
# Chainguard runs as a non‑root user (`nonroot`) by default.
# No need to create a user manually.
CMD ["python", "app.py"]
Gotchas & Troubleshooting
| Issue | Explanation | Fix |
|---|---|---|
| “I can’t exec into the pod!” | The runtime image has no shell (/bin/sh or bash). | Use an ephemeral debug container or temporarily switch to the -dev tag for debugging. |
| Permissions | Chainguard enforces non‑root execution. Writing to / or installing packages at runtime will fail. | Ensure the app writes only to writable locations (e.g., /tmp or a mounted volume). |
Missing sudo | sudo isn’t included, and package managers are unavailable at runtime. | Perform any privileged operations in the builder stage (using USER root if needed) and design the app to run without root. |
By design, Chainguard images omit sudo (and most package managers) to follow the principle of least privilege. They run as a pre‑configured non‑root user (UID 65532) by default, eliminating entire classes of privilege‑escalation attacks.
Solution: Shift privileged tasks to the build stage. Refactor your application to run without root whenever possible. If you must perform root‑level tasks (e.g., installing system dependencies), do them in a multi‑stage build using the USER root directive, then switch back to the non‑root user for the final image.
“Latest‑Only” Package Philosophy
Chainguard’s underlying package ecosystem, Wolfi, follows a Rolling Release model, which introduces a specific constraint regarding package versioning.
- Fixed Repo Constraints – Chainguard images pull exclusively from trusted Wolfi or Chainguard repositories. Adding third‑party or unverified repos is strongly discouraged to preserve the “Zero CVE” guarantee.
- Latest‑Only Rule – The public Wolfi repository generally maintains only the latest stable version of a package. Attempting to pin an older version (e.g.,
apk add openssl=1.1.1) will result in an “Unsatisfiable constraints” error because that version has been removed to prevent vulnerable software from being used.
Philosophy: Chainguard forces you to stay on the most recent, vetted packages, reducing the risk of lingering vulnerabilities.
Bottom Line
Chainguard images give you a secure, minimal, and reproducible foundation for containerized workloads. The trade‑off is a shift in workflow: you must adopt multi‑stage builds, embrace non‑root execution, and rely on the latest vetted packages. Once you adjust, the payoff is a dramatically reduced vulnerability surface and far less noise in your security scanning tools.
The “latest‑only” approach ensures that you aren’t unknowingly baking known vulnerabilities into your images. If your system absolutely requires a legacy, EOL (End‑of‑Life) version, you may need to consider their Enterprise tier, which provides a private repository for older, patched versions.
The Trap of “Legacy Version” Maintenance
One of the biggest reasons teams stick to old Docker images (e.g., node:14 or python:3.6) for production‑level services is stability.
“It works, so please don’t touch it.”
In the standard Docker world, pinning a version like python:3.6‑slim means the underlying OS (often an old Debian release) stops receiving security updates. You’re sitting on a ticking time bomb of OS‑level vulnerabilities.
Everyone knows they need to act, but migration is rarely a simple code change. It requires extensive testing and consideration of countless use cases. Chainguard changes this paradigm: even if you use a specific language version, Chainguard rebuilds the underlying Wolfi OS layers every single day.
- The Good: You get the stability of your language version with the security of a bleeding‑edge OS.
- The Bad: The image hash changes daily. Pipelines that rely on an exact SHA digest persisting for months will break; you need to embrace rolling tags.
- The Cost: For End‑of‑Life language versions (e.g., Python 3.7 or Java 8), Chainguard usually moves those images to their paid tier. The free tier focuses on currently supported versions.