DEV Community

Cover image for 'Chainguard' image for secure service
kination
kination

Posted on

'Chainguard' image for secure service

If you work as DevOps or system backend development, one of critical point causing your stress will be 'security'. Even though it is always at the top of the priority list, its substance remains elusive.

Assume you are maintaining k8s cluster, or docker image(in recent system, most of devops engineer will be related at least one of these). Even if your code is secure, the base image (Debian, Ubuntu, or even Alpine) often carries inherent technical debt, and within that debt, there are often security risks capable of compromising the service. Now, Chainguard image starts at this point.

What is Chainguard?

In short, Chainguard Images are "secure-by-default" container images. They are 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 the bare minimum required to run the app. No shell, no package managers (in the runtime), and no bloat.
  • Daily Rebuilds: Every image is rebuilt daily from upstream sources to patch vulnerabilities immediately.
  • SBOMs & Signing: They come with Software Bill of Materials and Sigstore signatures out of the box.

The Showdown: Official vs Chainguard

The most immediate benefit is reduction in noise. Let's look at a comparison I ran using trivy on a standard Python image and Chainguard.

  • Official Image (python:3.11): Over 300 vulnerabilities, with some of them categorized as "Critical" or "High"
  • Chainguard Image (cgr.dev/chainguard/python:latest): 0 CVEs

This isn't magic; it's just aggressive minimalism. By removing the OS components that your application doesn't actually use, you remove the attack surface.

Hands-on: Migration Guide

Migrating isn't always a simple swap, and it's more painful with Chainguard. This images are distroless, you cannot use apt-get or apk in the final runtime image. 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"]
Enter fullscreen mode Exit fullscreen mode

It is pretty simple dockerfile, to run python application.

Here's the migrated version with chainguard image:

# 1: Make 'builder'
# Use '-dev' tag here, 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: Actual 'runner'
# This image has no 'shell' and '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 . .

# Set path to 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"]
Enter fullscreen mode Exit fullscreen mode

Gotchas and Troubleshooting
"I can't exec into the pod!": Since the runtime image has no shell (/bin/sh or bash), you cannot run kubectl exec -it -- sh. If you need to debug, you must use ephemeral debug containers or temporarily switch to the -dev image tag.

Permissions: Chainguard enforces non-root execution. If your app tries to write to / or install packages at runtime, it will fail. Ensure your app only writes to designated directories (like /tmp or a mounted volume) where the nonroot user has permissions.

Package restriction

Work without sudo

"Why can't I run or install sudo?"

...is perhaps the most common question when first switching to Chainguard(so do I).

By security-first design, chainguard images do not include sudo command (and mostly they do not allow to install through package manager). They are built to follow the principle of least privilege, running as a pre-configured non-root user (UID 65532) by default. This effectively eliminates entire classes of privilege escalation attacks.

The Problem: Legacy setup scripts or runtime commands that rely on sudo will fail with a command not found error.

The Solution: Frankly, there are no simple solution.

You must shift your mindset to the build stage. First thing you need to do is refactoring application design, to run without root access as possible. For example, Ii your app needs to bind to a privileged port (like 80), don't make it go on by using sudo. Instead, bind to a higher port (like 8080) and handle the mapping at the infrastructure level (e.g., Kubernetes Service or Load Balancer).

But still there will be several process still needs it. Any operation requiring root privileges, such as installing system dependencies or modifying file permissions, must be performed in a multi-stage build using the USER root directive.

"Latest-Only" Package Philosophy

Chainguard's underlying package ecosystem, Wolfi, operates on a Rolling Release model. This introduces a specific constraint regarding package versioning.

Fixed Repo Constraints: Chainguard images are configured to pull exclusively from trusted Wolfi or Chainguard repositories. Adding third-party or unverified repos is strongly discouraged to maintain the "Zero CVE" guarantee.

The Latest-Only Rule: In the public Wolfi repository, generally only the latest stable version of a package is maintained.

If you try to pin an outdated version (e.g., apk add openssl=1.1.1), you will likely encounter an "Unsatisfiable constraints" error because that version has been removed from the index to prevent the use of vulnerable software.

The Philosophy: Chainguard forces you to stay current. This "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 (like node:14 or python:3.6) for production-level service, is stability.

"It works, so please don't touch it."

However, in the standard Docker world, if you pin a version like python:3.6-slim, that base OS (likely an old Debian version) stops receiving security updates. You are sitting on a ticking time bomb of OS-level vulnerabilities.

Frankly, everybody knows they needs to do something, but they cannot. Migrating production software is not just changing some code and package. It needs long-term testing, thinking of millions of use cases. Modern software are built on top of giant "system & source codes", and even top-level software engineer cannot expect all of exceptional cases. That's why even well-skilled engineers are concerning of migration every time.

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. If your deployment pipeline relies on the exact same SHA digest existing for months, this will break your flow. You need to get used to the idea of Rolling Tags.
  • The Cost: It is worth noting that for End-of-Life (EOL) language versions (like Python 3.7 or Java 8), Chainguard usually moves those images to their paid tier. The free tier focuses on currently supported versions.

Reference

Top comments (0)