DEV Community

EvvyTools
EvvyTools

Posted on

How to Fail a CI Build When a Downloaded Artifact's SHA-256 Does Not Match the Expected Hash

CI pipelines routinely download things. A Terraform binary pinned to a specific version. A custom container image base. A vendored model weight file from a release page. A third-party CLI tool installed before the build steps run. Each of those downloads is a potential supply chain failure point if the bytes you pulled do not match what you intended to pull.

The fix is short: store the expected SHA-256 alongside the URL, hash the downloaded file, and fail the build if the values do not match. The full recipe is short enough that there is no excuse for skipping it.

The Minimal Pattern

The verification step belongs in the same script that downloads the artifact. Splitting them across separate jobs creates a window where the unverified file might be used. Co-located looks roughly like this in shell:

EXPECTED_HASH="a3c5e8f2b9d1...64-hex-chars-total"
DOWNLOAD_URL="https://releases.example.com/v1.4/tool-linux-x86_64"

curl -fsSLo /tmp/tool "$DOWNLOAD_URL"
ACTUAL_HASH=$(sha256sum /tmp/tool | awk '{print $1}')

if [ "$ACTUAL_HASH" != "$EXPECTED_HASH" ]; then
  echo "Hash mismatch: expected $EXPECTED_HASH, got $ACTUAL_HASH"
  exit 1
fi

chmod +x /tmp/tool
mv /tmp/tool /usr/local/bin/tool
Enter fullscreen mode Exit fullscreen mode

The pattern is download, compute, compare, fail loudly, only then install. Skipping any of those steps weakens the guarantee. Reusing the file at /tmp/tool before the comparison passes leaves a window where a corrupted or swapped binary could be executed.

Whiteboard with sticky notes representing a planning workflow
Photo by Christina Morillo on Pexels

Using GitHub Actions or GitLab CI Built-In Steps

Most CI providers have community-maintained actions that wrap the download-and-verify pattern. GitHub Actions has actions for downloading specific tool versions (Terraform, kubectl, the major cloud CLIs) that perform hash verification automatically as long as you pin to a known version. Using those when they exist saves you from writing the shell yourself and gives you a maintained dependency that gets updates when the upstream tool releases.

When no official action exists for the tool you need, fall back to the shell pattern. Pin the expected hash in your workflow file, not in a separate config file that could drift. The hash should be visible in your version control history so any unauthorized change to the verification step shows up in a code review.

Where to Get the Expected Hash

The expected hash should come from the project's official release page over HTTPS. For projects with a SHA256SUMS manifest, copy the relevant line for the exact filename you are downloading. For projects with the hash only in release notes, copy from those notes (and consider opening an issue requesting a SHA256SUMS file for future releases).

Once you have the hash, commit it into your CI configuration. The expected hash is part of your build's pinned dependency state. Treating it as configuration ensures any change to it is reviewable and auditable.

When you upgrade the tool to a new version, you bump both the version pin and the expected hash in the same commit. This is the same workflow as bumping a pinned package version in a lockfile, just spelled out one level more explicitly.

For a quick spot-check of an expected hash you copied from a release page (to make sure you did not get a stray character on the paste), running the file through the free hash generator from EvvyTools and visually comparing the values is a fast sanity check before committing.

Handling Mismatches

When the comparison fails in CI, the build stops. That is the entire point. The failure message should be clear enough that whoever is on call can immediately distinguish between three causes: a network hiccup that corrupted the download, an upstream rebuild that changed the file under the same URL, or an actual mirror compromise.

Logging both the expected and actual hash in the failure message helps. The shell example above prints both, which lets the on-call engineer paste the actual hash back into the search bar of issue trackers to see whether anyone else hit the same hash recently.

For uncommonly hit failures, retrying once after a short delay rules out the network-hiccup case quickly. If the second download also produces a different hash than expected, the cause is upstream or malicious, not local. Two independent downloads producing the same wrong hash is a strong signal worth alerting on.

What to Verify, What to Skip

Not every downloaded file warrants a hash check. Files coming through your platform's already-verified package manager (apt, yum, Homebrew, npm with a lockfile, pip with a hashes-required lockfile) are verified for you. The check is most valuable for direct downloads from release pages that bypass any package-manager verification, and for downloads from URLs you control where the threat is your own infrastructure being compromised.

Anywhere you download a binary and immediately execute it without verification is a place to add the check. The cost is a few lines of shell; the benefit is the supply chain attack you do not have to respond to.

Tools and References

GitHub Actions documentation covers the secrets and workflow syntax for the CI examples above. GnuPG is the standard tool for verifying detached PGP signatures if you want to extend beyond hash-only verification. The SHA-2 specification on Wikipedia covers what SHA-256 is computing under the hood. OWASP maintains cryptography guidance worth bookmarking for any team building out a security-conscious CI pipeline.

For the broader context of why verifying downloaded files matters and how to set up the workflow outside of CI as well, the full walkthrough on verifying downloaded file checksums covers the trust model and the practical workflow end to end.

The Habit

The pattern is invisible most days. The build runs, the download succeeds, the hash matches, the install proceeds. Once or twice a year, the hash will not match for one of the boring reasons, and a few times in a career, the hash will not match because something genuinely wrong happened. In all three cases the verification step has paid for the seconds it adds to every other run.

Failing loudly on a hash mismatch is also the right behavior when an upstream project legitimately rebuilds and reuploads a release under the same version. That is a policy violation by the upstream maintainer, and your build failing on it surfaces the issue at the moment it actually matters rather than days later when something downstream subtly breaks.

The check is one of those small infrastructure investments that scales: write it once, copy the pattern into the next service, and never have to think about supply chain integrity for direct downloads in that codebase again.

A Note on Renovate and Dependabot for Hash Pinning

Several dependency-update bots (Renovate is the most flexible, Dependabot is the most common on GitHub) can be configured to keep pinned hashes up to date as upstream projects release new versions. The bot opens a PR whenever a new version is available, with the new version and new hash already updated in your workflow file.

This is the right way to manage pinned hashes long term: automated updates with human review on the PR, rather than manual edits whenever you happen to notice a new upstream version. The bot's PRs include a changelog link so you can quickly assess whether the version bump is worth merging, and the hash update is part of the same commit so the verification step continues to work after the merge.

If you are not using a dependency-update bot today and your CI workflows pin tool versions and hashes manually, setting one up is a one-afternoon project that pays for itself within a few months.

What This Pattern Does Not Replace

Hash verification on downloaded artifacts is one layer of supply chain defense. It catches in-transit and mirror compromises against the publisher's intended output. It does not catch a maintainer-compromised build, a backdoored upstream commit that the maintainer unwittingly released, or a typosquatting attack where you depend on a malicious lookalike package.

Those threats need different defenses: dependency audit tools, package signing, reproducible builds, and source review for security-critical components. Hash verification is the cheapest and most effective layer for what it actually defends against. The other layers are worth investing in on top of it for systems where the threat model warrants the higher cost.

Top comments (0)