Open Source Security

Go Proxy and Private Module Security

Mixing public and private modules through a Go proxy is where most teams get their configuration wrong, and the mistakes range from leaked module names to accepted unverified code.

Shadab Khan
Security Engineer
6 min read

Every Go team eventually has to answer the same question: how do we use the public module ecosystem and our own private modules at the same time, without leaking the names of our internal packages to proxy.golang.org and without accepting unverified code into the build? The answer is a combination of GOPROXY, GOPRIVATE, GONOSUMCHECK, and GONOSUMDB, and most of the incidents I have seen in this area trace back to one of those being misconfigured.

Let me walk through how these pieces fit together, where the common mistakes are, and what a sensible default looks like.

What does GOPROXY do?

GOPROXY is a comma-separated list of URLs that the Go command consults in order when it needs to fetch a module. The default since Go 1.13, released September 2019, is https://proxy.golang.org,direct. The direct keyword at the end means: if the preceding proxies fail or return 404, fetch the module directly from the version control system.

The important behavior is that the Go command will ask each proxy in turn for any module name, including your private ones. If example-corp.internal/auth is in your go.mod and GOPROXY=https://proxy.golang.org,direct, the Go command will first ask proxy.golang.org for example-corp.internal/auth. The public proxy will return 410 Gone or 404 Not Found, and the Go command will fall through to direct. But the request was made. The module name was sent. If anyone is logging queries to the public proxy, they now know that example-corp.internal/auth exists.

GOPRIVATE keeps names off the wire

GOPRIVATE is a pattern list of module prefixes that should be treated as private. Modules matching these patterns bypass the proxy list entirely and are fetched directly from the VCS. They also bypass sum.golang.org, because a private module is not expected to be in the public checksum log.

A sensible default looks like this:

GOPROXY=https://proxy.golang.org,direct
GOPRIVATE=example-corp.internal/*,github.com/example-corp/*

With GOPRIVATE set, the Go command skips the public proxy for those patterns and goes straight to the VCS. Your internal module names never appear in requests to proxy.golang.org. This is the right default for most organizations.

What about internal proxies?

Many teams run an internal Go module proxy, either Athens, Artifactory, JFrog, Sonatype Nexus, or something home-grown. An internal proxy has several benefits. It caches public modules so builds do not depend on public infrastructure being up. It can be audited centrally. It can enforce policies like blocking known-bad modules. And it can serve private modules alongside public ones.

A common configuration is:

GOPROXY=https://goproxy.example-corp.com

The internal proxy forwards requests for public modules to proxy.golang.org and serves private modules from an internal source. If you go this route, three things matter for security. First, the internal proxy should verify public modules against sum.golang.org before caching them. Not all proxies do this by default. Second, the internal proxy should authenticate and authorize private module access; too many I have seen are open to anyone on the corporate network. Third, CI and developers should not be able to override GOPROXY without noise; it is too easy to type GOPROXY=direct go build to debug and forget to unset it.

Is GONOSUMCHECK ever a good idea?

The short answer is no. GONOSUMCHECK=1 disables checksum verification entirely. It exists because early Go versions needed an escape hatch, and sometimes during a proxy outage you need to get a build out. But making it permanent in CI or in developer environments turns off one of the strongest integrity guarantees Go provides.

GONOSUMDB is different. It tells the Go command not to contact sum.golang.org for specific modules, usually private ones. Checksums are still verified against local go.sum; they are just not double-checked against the public log. This is the right way to handle private modules that should not go to the public sumdb.

GOPRIVATE actually implies GONOSUMCHECK for matching modules, so in most cases you do not need to set GONOSUMDB explicitly. Check your environment for stray GONOSUMCHECK=* settings; I have found them in base images more than once.

The 410 Gone story

If you want a module permanently removed from the public proxy, such as an accidentally published internal module, you can contact the Go team to have it marked as 410 Gone. This happens rarely but has real implications. In early 2023 there were several reports of companies accidentally publishing internal modules to the public proxy because a developer ran go get against an internal path from a machine with the wrong GOPROXY. The public proxy cached the module, including whatever secrets might have been in source. Getting it removed took days.

The lesson is to get GOPRIVATE right from the start. Set it at the OS level, bake it into container images, and verify it in pre-commit hooks.

Authentication to private repos

Fetching private modules via direct mode requires the Go command to authenticate to your VCS. For GitHub, this means either a .netrc file, a GitHub token in the URL, or a credential helper. For GitLab and others, similar mechanisms apply.

This is where credentials sometimes leak. I have seen tokens end up in ~/.netrc files that got included in Docker build contexts, or tokens passed as build args that ended up in image history. The solution is a build-time secret that is available during go mod download and then discarded. BuildKit's --mount=type=secret feature does this cleanly.

How do you rotate a compromised private module?

Say someone pushed a credential or a key to a private Go module and you need to yank it. Rotating go.sum is only half the story. You also need to invalidate every copy of the module in every module cache and internal proxy. Athens has a DELETE /{module}/@v/{version} endpoint that drops the cached version. Artifactory has similar features. Without cache invalidation, the bad version can continue to be served to new builds even after the source is gone.

CVE watch for Go private module tooling

CVE-2021-33195 affected net package DNS handling and touched code that fetched modules. CVE-2022-27664 in September 2022 was a denial-of-service in net/http that could affect anyone running a Go module proxy. Keeping the proxy binary updated matters as much as keeping the application binaries updated.

Athens versions before 0.11.0 had authentication edge cases that were addressed in later releases. If you run Athens, keep it current. Same for Artifactory, where Go support has evolved significantly since 7.0.

How Safeguard Helps

Safeguard inspects the GOPROXY, GOPRIVATE, and GONOSUMCHECK configuration of every build environment it observes and flags setups that would send internal module names to the public proxy or disable checksum verification. When an internal module is accidentally fetched from proxy.golang.org, Safeguard raises a leak alert and records the exposure for incident review. Policy gates can require that all builds use an approved internal proxy and that GOPRIVATE covers the expected module prefixes before a deploy is allowed to proceed.

Never miss an update

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