Build and supply chain

Wolfi's split packages and the mistakes they cause, the secure way

You typed apk add gnupg, the install said OK, and gpg is not there. You're not crazy, you heard right. On Wolfi, the GnuPG meta package installs an SBOM file and nothing else.

The short answer

Wolfi splits each upstream project into small packages: gpg, gpgv and gpg-agent are separate, and the gnupg meta package pulls none of them. Install by command with apk add cmd:gpgv, list exactly the packages an image needs, and test every command the image must run in CI.

Updated Houssam Hammoudi, CTOTested with cgr.dev/chainguard/wolfi-base:latest (apk-tools 2.14.10), gnupg 2.4.9-r11

On this page
  1. What goes wrong
  2. What the docs say
  3. The secure configuration
  4. Prove it
  5. Mistakes people make
  6. Checklist

What goes wrong

Wolfi, the distribution under Chainguard images, builds one upstream project into many small packages. That is good for security: an image gets only the files it uses, and a scanner reports only on what is there.

It also breaks habits from Debian and Alpine. Three mistakes come up again and again:

  • Installing the meta package and assuming it pulls in the whole suite. gnupg on Wolfi installs no binaries.
  • Installing the obvious package and missing a helper. gpg alone cannot create a key, because gpg-agent is a separate package.
  • "Fixing" a missing command by moving to a -dev image or adding a whole toolchain, which brings back the shell, the package manager and the vulnerabilities you left Debian to avoid.

The security cost is quiet. A signature check in a Dockerfile that calls gpgv, wrapped in || true or a loose if, passes when gpgv is not installed. The image builds, and nothing was verified.

What the docs say

Packages are designed to be granular and independent, to support minimal images

Source: Chainguard Academy, Wolfi overview

Although compiling said extensions as built-in packages makes for a simpler build, it also increases the size of the original package and creates a wider surface for possible vulnerabilities.

Source: Chainguard Academy, Building a Wolfi package

GNU Privacy Guard 2 - meta package for full GnuPG suite

Source: wolfi-dev/os, gnupg.yaml

The recipe calls gnupg a meta package for the full suite, but its runtime dependencies are only merged-lib, merged-usrsbin and wolfi-baselayout. The binaries live in subpackages (gpg, gpgv, gpg-agent, gpgsm, gnupg-dirmngr and others) that nothing pulls in for you. Neither the Wolfi overview nor the recipe warns about this.

The secure configuration

Install by command, not by guess, and pin the result.

dockerfile
# Build stage: wolfi-base has apk and a shell.
FROM cgr.dev/chainguard/wolfi-base@sha256:<digest> AS verify

# Ask apk for the package that provides each command you will call.
# cmd:<name> resolves to the package that ships /usr/bin/<name>.
RUN apk add --no-cache cmd:gpgv cmd:curl

# Fail loudly if a command is missing, before it is used.
RUN for c in gpgv curl; do command -v "$c" >/dev/null || { echo "missing: $c"; exit 1; }; done

# Verify with gpgv against a keyring you ship, never "|| true".
COPY release-signing-key.gpg /keys/release.gpg
RUN curl -fsSLO https://downloads.example.com/tool-1.2.3.tar.gz \
 && curl -fsSLO https://downloads.example.com/tool-1.2.3.tar.gz.sig \
 && gpgv --keyring /keys/release.gpg tool-1.2.3.tar.gz.sig tool-1.2.3.tar.gz \
 && tar -xzf tool-1.2.3.tar.gz -C /opt

# Runtime stage: no apk, no shell, no gpg.
FROM cgr.dev/chainguard/static@sha256:<digest>
COPY --from=verify /opt/tool /opt/tool
USER 65532:65532
ENTRYPOINT ["/opt/tool/bin/tool"]

Once the build works, replace each cmd: entry with the package name that apk search -q cmd:<name> printed, so the image says exactly what it contains. Common surprises:

You wantWolfi package
gpg (sign, encrypt)gpg plus gpg-agent for any key operation
gpgv (verify only)gpgv
ssh-keygenopenssh-keygen (not in openssh-client)
dig, nslookupbind-tools
psprocps
useraddshadow (busybox adduser is already in wolfi-base)
envsubstgettext-envsubst (smaller than all of gettext)
pippy3.14-pip (one package per Python version)

For signature checks, prefer gpgv over gpg: it only verifies, needs no agent, and is the smaller package.

Prove it

In a disposable wolfi-base container, install the meta package first:

bash
apk add gnupg
apk info -R gnupg ; apk info -L gnupg
command -v gpg gpgv gpg-agent
text
gnupg-2.4.9-r11 depends on:
merged-lib
merged-usrsbin
wolfi-baselayout
gnupg-2.4.9-r11 contains:
var/lib/db/sbom/gnupg-2.4.9-r11.spdx.json
gpg        not found
gpgv       not found
gpg-agent  not found

The install succeeds and ships one file, its own SBOM. Now the obvious package:

bash
apk add gpg
gpg --batch --passphrase "" --quick-gen-key [email protected]
text
gpg        /usr/bin/gpg
gpgv       not found
gpg-agent  not found

gpg: can't connect to the gpg-agent: Configuration error
gpg: agent_genkey failed: No agent running
gpg: key generation failed: No agent running

Ask apk which packages ship the missing commands, and install by command:

bash
apk search -q cmd:gpg-agent cmd:gpgv
apk add cmd:gpg-agent cmd:gpgv
text
gpg-agent
gpgv
gpg        /usr/bin/gpg
gpgv       /usr/bin/gpgv
gpg-agent  /usr/bin/gpg-agent
key generated

The same lookup for other commands that trip people up:

text
ssh-keygen  -> openssh-keygen
dig         -> bind-tools
ps          -> procps
useradd     -> shadow
envsubst    -> gettext gettext-envsubst

Mistakes people make

Trusting a meta package

On Debian, gnupg pulls in the suite. On Wolfi, it pulls in nothing you can run. Check with apk info -R <package> before you rely on a meta package.

Signature checks that pass when the tool is missing

gpgv ... || true, or a script that checks the exit code of the wrong command, turns "gpgv: not found" into "verified". Check that each command exists before the step that uses it, and let the verify step fail the build.

Jumping to the -dev image in production

The -dev variants add a shell and apk so you can build. Use them in a build stage. Copy the result into a runtime image without a shell or package manager.

Installing the whole toolchain for one command

apk add gettext to get envsubst, or openssh to get ssh-keygen, adds packages that every scan will report on. Install the subpackage.

Floating package versions in a pinned image

A pinned base digest with apk add curl still installs whatever version is current on build day. Pin versions where builds must be repeatable (apk add curl=<version>), or build the image with apko from a locked package list.

Checklist

  • Every apk add names the package that ships the command, found with apk search -q cmd:<name>.
  • No image relies on the gnupg meta package for binaries.
  • Signature checks use gpgv with a shipped keyring and fail the build on error.
  • A build step checks with command -v that each needed command exists.
  • -dev images are used only in build stages.
  • The runtime stage has no apk and no shell.
  • Package versions are pinned where builds must be repeatable.
  • The base image is pinned by digest.

Wolfi gives you exactly the packages you ask for, not the ones you meant. Ask by command and you get both.

H2-CSDE

Learn it on a live range

Dependencies and vendoring, in DevSecOps and Supply Chain: a real host in your browser, and every objective checked on the machine.

Start free

H2 Scanner

Want this caught before it merges?

The H2 Scanner runs in your CI and flags the weaknesses pages like this one warn about, on every pull request.

Talk to us