Open Source Security

How to Monitor Go Module Substitution Attacks

Defend against Go module substitution attacks with GOPROXY, GOSUMDB, vendor verification, and checksum database monitoring — complete with working examples.

Shadab Khan
Security Engineer
5 min read

Go's module system is unusually strong against substitution — go.sum plus the checksum database makes silent replacement hard. But it is not impossible. Misconfigured GOPROXY, abandoned modules whose path gets reclaimed, vanity import hijacks, and internal modules that collide with public names are all live threats. This tutorial shows you how to lock down GOPROXY and GOSUMDB for a multi-repo Go 1.22 codebase, run a private Athens proxy as a cache with an allowlist, detect go.sum tampering in CI, and audit transitive replace directives. Prerequisites: Go 1.22+, Docker, and 45 minutes. Works equally well for enterprise monorepos and open-source libraries.

What does GOPROXY do?

GOPROXY controls where go get fetches modules from. The default https://proxy.golang.org,direct means "try Google's proxy first, fall back to cloning from the source." Setting it to a private proxy or an explicit list is how you enforce provenance.

go env GOPROXY
# https://proxy.golang.org,direct
go env GOSUMDB
# sum.golang.org

direct is the escape hatch — it lets Go clone from any VCS host. Removing it entirely means "if it is not in my proxy, the build fails." That is the posture you want for internal modules.

How do I enforce GOSUMDB?

Set GOSUMDB=sum.golang.org and GOFLAGS=-mod=readonly organization-wide. The checksum database cross-signs every module version globally; a forked proxy that serves different bytes is caught instantly.

export GOSUMDB=sum.golang.org
export GONOSUMCHECK=""
export GOFLAGS="-mod=readonly -trimpath"
go mod download
# verifying github.com/spf13/cobra@v1.8.0: checksum ok
# verifying golang.org/x/text@v0.14.0: checksum ok

For private modules set GONOSUMCHECK=github.com/acme/* to skip the public sumdb for paths that never publish there. Use sparingly — every exception is a hole in your verification.

How do I set up a private Athens proxy?

Run Athens in Docker with an allowlist filter. It becomes the single ingress point for all Go module fetches, which gives you audit logs, caching, and the ability to block compromised modules instantly.

docker run -d -p 3000:3000 \
  -e ATHENS_DISK_STORAGE_ROOT=/var/lib/athens \
  -e ATHENS_STORAGE_TYPE=disk \
  -e ATHENS_FILTER_FILE=/etc/athens/filter \
  -v athens-data:/var/lib/athens \
  -v $PWD/filter:/etc/athens/filter:ro \
  gomods/athens:v0.15.0

cat filter
# + github.com/acme
# + golang.org/x
# - github.com/evil-org
# D

The trailing D means "deny by default." Anything not explicitly allowed is rejected with 403. Deploy one Athens per region and point all developer machines and CI runners at it via GOPROXY=https://athens.acme.internal,off.

How do I detect go.sum tampering?

Run go mod verify and go mod tidy -diff in CI. verify checks that the on-disk modules match go.sum; tidy -diff (Go 1.23+) shows any difference a tidy would produce without modifying files.

go mod verify
# all modules verified

go mod tidy -diff
# (no output = clean)

git diff --stat go.sum
#  go.sum | 4 ++--
# If the diff is non-empty but no dep was changed, someone hand-edited go.sum.

A PR that changes go.sum without touching go.mod is a red flag. Require a reviewer to look at it and explain the change — usually the answer is "I accidentally committed, let me revert."

How do I audit replace directives?

Use go list -m -json all and filter for Replace fields. A replace pointing to a local path or a non-standard fork is the most common substitution vector in internal codebases.

go list -m -json all | jq -r 'select(.Replace) |
  "\(.Path) -> \(.Replace.Path)@\(.Replace.Version // "LOCAL")"'
# github.com/acme/internal -> github.com/acme/internal@v0.5.2
# golang.org/x/crypto -> ../vendored/crypto@LOCAL

LOCAL replaces are a code smell at release time — someone forgot to remove a dev override. Block the release if any Replace.Version is empty.

How do I monitor the checksum database proactively?

Subscribe to the sum.golang.org transparency log via gosum-scraper or the official go-module-database-watcher. You get a feed of every new module version added and can alert on new versions of modules you depend on.

go install golang.org/x/mod/sumdb/gosum@latest
curl -s "https://sum.golang.org/lookup/github.com/acme/api@v1.4.0"
# 2
# github.com/acme/api v1.4.0 h1:abc...=
# github.com/acme/api v1.4.0/go.mod h1:def...=

Alerts on unexpected new versions of your internal modules catch dependency-confusion attacks where an attacker publishes a public module with the same path as your private one. Go's proxy takes the first published version it sees, forever — so a single-event alarm is sufficient.

How Safeguard Helps

Safeguard ingests go.sum and go.mod from every repo you connect and builds a live inventory of your Go module supply chain, including every replace directive and every module hosted outside the standard proxy. Griffin AI cross-references each module against the Go vulnerability database and compromised-package intelligence, highlighting modules where the publisher identity, domain ownership, or proxy origin recently changed — the exact signature of a substitution attack. The platform auto-generates CycloneDX SBOMs for Go binaries with full transitive provenance and stores each go.sum hash for longitudinal comparison across builds. Policy gates can block a build that introduces an unapproved replace or a module hosted outside your Athens proxy, with alerts flowing to Slack or Jira on the first anomaly.

Never miss an update

Weekly insights on software supply chain security, delivered to your inbox.