Skip to content
← Blog

Technical explainer

Go module supply chain security: GOPROXY, go.sum, and install-time gating

3 min read

Go has one of the better-designed module systems for supply-chain integrity — and one of the smaller install-time attack surfaces of any mainstream ecosystem. There are no postinstall scripts in Go modules. Fetching a dependency downloads source, not arbitrary code that runs on download. That removes the single most-abused vector in npm and PyPI.

It does not remove supply-chain risk. The code still compiles into your binary, go generate and build constraints can run tooling, and a malicious module is a malicious module whether or not it runs at fetch time. This post covers what the Go toolchain already verifies, where the gaps are, and how install-time gating fits.

What Go already verifies

Two mechanisms do most of the work.

go.sum + the checksum database. Every module version your build depends on has a h1: hash in go.sum — a dirhash.Hash1 digest over the sorted file tree of the module zip. On every build, go recomputes that hash for the downloaded zip and refuses to proceed if it doesn't match. The first time a hash is recorded, go cross-checks it against the public checksum database (sum.golang.org), an append-only transparency log. So a registry that serves you different bytes than everyone else gets caught.

GOPROXY. By default Go fetches modules through proxy.golang.org, which serves the module metadata (@v/list, @v/<version>.info/.mod) and the source zip (@v/<version>.zip) from one host. That single-host design is what makes a transparent proxy practical: point GOPROXY at a local gate and every module fetch — metadata and bytes — flows through one place.

Where the gaps are

  • go.sum pins integrity, not safety. It guarantees you get the same bytes as the checksum DB. It says nothing about whether those bytes are malicious. A typosquatted module, a dependency-confusion package on a public proxy shadowing an internal path, or a compromised maintainer's new tag all produce a valid go.sum entry.
  • GOPRIVATE / GONOSUMDB holes. Modules matched by GOPRIVATE skip the checksum database entirely. Misconfigured globs here are a real-world dependency-confusion vector.
  • Build-time execution. go generate, cgo, and build tooling run code during the build. The fetch is safe; the build is not inherently contained.

How install-time gating fits

A registry-proxy gate sits between go and proxy.golang.org. You set GOPROXY to the local gate; the gate forwards to the real proxy and inspects every module zip before it reaches the build cache:

  • OSV lookup against the Go ecosystem for known vulnerabilities at the resolved version.
  • Threat-feed match for known-malicious modules.
  • h1: verification — the gate computes dirhash.Hash1 over the served zip and compares it to the go.sum pin, catching a tampered or substituted artifact independently of go's own check.

Because the gate is the only network destination the build needs (the Go proxy protocol serves the checksum DB too, under /sumdb/), it composes cleanly with an OS sandbox: restrict the build's network to the gate, and a compromised build step can't reach an exfiltration endpoint either.

With Veln, veln safe go build sets GOPROXY to a local gate whose upstream is proxy.golang.org, runs the module enforcer on every @v/<version>.zip, and contains the build in an OS-level sandbox (Landlock, sandbox-exec, or a Job Object) — same command, same go.sum, no workflow change. For CI and pre-commit, veln verify walks go.sum directly and scores every pinned module.

The practical takeaway

Go's defaults are good: keep the checksum database on (don't over-broaden GOPRIVATE), commit go.sum, and treat a go.sum mismatch as a hard stop. Integrity verification answers "are these the right bytes." It does not answer "are these bytes safe to run" — that's the question OSV, threat-feed, and a contained build are for.