Skip to content

security(ci): verify kubeconform against a pinned digest before installing - #538

Merged
LukasWodka merged 1 commit into
developfrom
sec/1426-supply-chain-pins
Aug 3, 2026
Merged

security(ci): verify kubeconform against a pinned digest before installing#538
LukasWodka merged 1 commit into
developfrom
sec/1426-supply-chain-pins

Conversation

@LukasWodka

@LukasWodkaLukasWodka commented Aug 3, 2026

Copy link
Copy Markdown
Contributor

What and why

The template job installed kubeconform like this:

curl -sSL https://github.com/yannh/kubeconform/releases/latest/download/kubeconform-linux-amd64.tar.gz \| tar xz -C /usr/local/bin

Two independent problems:

  1. releases/latest is a mutable pointer. The binary this job executed changed whenever upstream cut a release. We were not pinned to anything.
  2. curl | tar makes verification impossible, not just absent. Piping extracts bytes as they arrive, so there is no point at which a digest could be checked. The archive was unpacked onto PATH first and inspected never. curl also lacked -f, so an HTTP error body was fed to tar as if it were an archive.

The next step then executes that binary — four times per CI run, once per matrix platform.

Pin + digest

itemvalue
version0.8.0 (was releases/latest)
assetkubeconform-linux-amd64.tar.gz
SHA-2569bc2bffbf71f261128533edaf912153948b7ff238f9a531ae6d34466ec287883
checksum sourcethe release's own CHECKSUMS asset — https://github.com/yannh/kubeconform/releases/download/v0.8.0/CHECKSUMS

The digest is taken from upstream's published CHECKSUMS file, not computed from a download (computing it from the bytes you just fetched authenticates nothing).

Version pin is behaviour-preserving:releases/latest currently 302-redirects to v0.8.0, confirmed.

Pattern copied from tracebloc/.github's actionlint.yml (version + SHA-256 + sha256sum -c) rather than invented.

The failure path that reports success — and why this one doesn't

sha256sum -c treats a malformed line as "no properly formatted checksum lines found", and whether that exits non-zero depends on the coreutils build: GNU exits 1, the macOS sha256sum exits 0. An empty or truncated digest variable is therefore a plausible way to ship a step that verifies nothing and still goes green. So the digest is asserted to be 64 hex characters before it is relied on.

Tested on ubuntu:22.04 (GNU coreutils 8.32) by executing the verbatim run: block extracted from this YAML, checking both the exit code and whether a binary landed on PATH:

caseexitbinary installed
published digest (happy path)0yes — prints v0.8.0
digest mismatch1no
empty digest var1no
truncated digest1no
uppercase digest1no
HTTP 40422no
real substitution: v0.8.0 digest vs the v0.7.0 asset1no

Every bad case fails closed and installs nothing.

Required checks

This touches the template job, which runs the 4-platform matrix (aks, bm, eks, oc), so helm-ci exercises the new step 4×. actionlint 1.7.7 clean on this file. No chart or values changes, so Prereqs / PATH persist / E2E are untouched.

Refs tracebloc/backend#1426

🤖 Generated with Claude Code


Note

Low Risk
CI-only workflow change with no chart or runtime impact; slightly reduces risk of unverified third-party binaries on runners.

Overview
The template job in helm-ci no longer installs kubeconform from releases/latest via curl | tar. It pins v0.8.0 with a SHA-256 from upstream’s CHECKSUMS, downloads the tarball to disk (curl -fsSL with retries), validates the digest (including a regex guard so empty/truncated digests cannot pass silently), and only then extracts and installs the binary.

This hardens CI supply-chain for the step that runs kubeconform on rendered Helm manifests across the four-platform matrix; manifest validation behavior is unchanged aside from using a fixed, verified binary.

Reviewed by Cursor Bugbot for commit 4dc2029. Bugbot is set up for automated code reviews on this repo. Configure here.

…lling
The template job installed kubeconform with:
curl -sSL .../releases/latest/download/kubeconform-linux-amd64.tar.gz \
| tar xz -C /usr/local/bin
Two independent problems. `releases/latest` is a mutable pointer, so the
binary this job executed changed whenever upstream cut a release — we
were not pinned to anything. And piping curl into tar extracts the bytes
as they arrive, so there was no moment at which a digest could have been
checked even if we had one; the archive was unpacked onto PATH first and
inspected never.
What could previously execute unreviewed: whatever those bytes happened
to be. A replaced release asset, a compromised upstream account, or a
MITM on the download would land an executable in /usr/local/bin and the
next step ran it. This job runs on the matrix for all four platforms, so
it happened four times per CI run.
What now cannot: the download is pinned to an explicit version, written
to a temp file, verified against a digest pinned in the workflow, and
only installed once it matches. Bytes that do not match the digest never
become an executable on PATH.
Follows the pattern already used in tracebloc/.github's actionlint.yml
(version + SHA-256 + `sha256sum -c`).
kubeconform 0.8.0, kubeconform-linux-amd64.tar.gz
sha256 9bc2bffbf71f261128533edaf912153948b7ff238f9a531ae6d34466ec287883
source: the release's own CHECKSUMS asset (not computed from a download)
Version pin is behaviour-preserving: `releases/latest` currently
redirects to v0.8.0.
The digest is asserted to be 64 hex characters before use. `sha256sum -c`
treats a malformed line as "no properly formatted checksum lines found",
and whether that exits non-zero depends on the coreutils build — so an
empty or truncated variable could otherwise verify nothing while the step
still went green. Every failure mode was tested on ubuntu:22.04 (GNU
coreutils 8.32): digest mismatch, empty digest, truncated digest,
uppercase digest, HTTP 404, and a real substitution (pinned v0.8.0 digest
against the v0.7.0 asset). All exit non-zero and leave no binary on PATH.
Refs tracebloc/backend#1426
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@LukasWodkaLukasWodka self-assigned this Aug 3, 2026
@LukasWodka
LukasWodka merged commit 42ae03f into developAug 3, 2026
22 checks passed
@LukasWodka

Copy link
Copy Markdown
ContributorAuthor

/fr-pass

Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

@LukasWodka@saadqbal