skip to content

Your platform team proposes a broad GOPRIVATE pattern so internal modules resolve — how wide should it be, and who owns that call?

level: principalimportance: nice to knowfreq 28%

answer

  1. a too-wide pattern never fails a build
  2. ask what else the glob matches
  3. two properties, two separate overrides
  4. written by infrastructure, overruled by security
  5. exempt paths need a compensating control

basics

~20 s

Every GOPRIVATE pattern silently removes checksum-database verification for everything it matches, so treat the list as a reviewed security control, not a build convenience. Keep patterns as narrow as the real private paths, and require review to widen one.

solid answer

~50 s

The framing I insist on is that `GOPRIVATE` is not a convenience variable — each pattern silently removes the checksum-database check for everything it matches, with no warning and no failing build when it is too wide. So the list is a reviewed artefact: version-controlled in the build image, one line of rationale per pattern, changed by pull request, and not overridable by a job's own environment. I keep patterns as narrow as the actual private paths, and I split the decision instead of reaching for `GOPRIVATE` reflexively: if the internal proxy can serve private modules you need no proxy exemption at all, only the database one, and `GONOPROXY` and `GONOSUMDB` override `GOPRIVATE` for those two decisions separately. Ownership sits with build infrastructure, with security able to overrule, and every exemption buys a compensating control — go.sum diffs reviewed like code, first ingestion in one place, `go mod verify` on the runners.

code

text · 7 lines
text
# reflexive: skips the proxy AND the checksum database, for anything under the glob
GOPRIVATE=*.internal.example,github.com/*

# narrow: private modules still flow through the internal proxy,
# only the checksum-database lookup is waived, and only for real private paths
GOPROXY=https://proxy.internal.example/mod
GONOSUMDB=*.internal.example,github.com/acme-internal/*

go deeper

for a junior

Know that these patterns exist so internal modules can be fetched without going through the public proxy, and that changing them is not an individual decision.

for a middle

Explain both effects of matching a path — direct fetching and no checksum-database lookup — and that go.sum hashes are still recorded for those modules either way.

for a senior

Be ready to narrow a proposed pattern in review: ask what else the glob matches, and choose the specific override that gives up only the property you must.

for a principal

Own the policy end to end — who writes the list, who can overrule, how jobs are prevented from overriding it, what compensating controls every exemption buys, and what running a proper mirror instead would cost.

## Why this is a decision someone must own A too-wide exemption pattern has no symptom. Builds go green, no warning is printed, and the only observable difference is that a class of dependencies is no longer checked against an outside record. That combination — high consequence, zero feedback — is the signature of a control that must be owned explicitly rather than left to whoever is unblocking a build at the time. In most organisations the pattern is first written by an engineer standing up a self-hosted or air-gapped build environment, under time pressure, to make internal modules resolve. That is a reasonable origin and a terrible steady state. ## What the exemption actually does Matching a module path with `GOPRIVATE` does two things at once: those modules are fetched directly rather than through the proxy, and their hashes are not checked against the checksum database. It does *not* stop `go.sum` lines from being recorded for them, and it does not stop `go mod verify` from working — the pinning survives, the outside opinion does not. Understanding that split is what lets you narrow the decision, because the two halves have separate overrides: `GONOPROXY` and `GONOSUMDB` accept the same kind of glob list and override `GOPRIVATE` for the proxy decision and the checksum-database decision respectively. That matters more than it sounds. If your internal proxy can serve your private modules — most can — then you need no proxy exemption at all. The only property you actually have to give up is the database lookup, because a private path genuinely has no public record. Reaching for `GOPRIVATE` because it is the variable everyone knows gives away routing as well, and routing is where the interesting failures live: a module fetched direct skips the mirror's controls, its availability guarantees, and its logging. ## How wide is too wide The test is what *else* a pattern matches. A pattern scoped to a domain the company actually controls is defensible. The dangerous shapes are the ones that reach past it: - A bare host-level glob such as `github.com/*`, which exempts every public module on that host, not just the company's repositories. - A trailing wildcard that swallows a namespace shared with third parties. - A pattern added to make one incident go away, which then matches a whole ecosystem. The asymmetry to name out loud is that a too-narrow pattern fails loudly on the next build and gets fixed in minutes, while a too-wide one never fails at all. So the default posture is deny, and the burden of proof sits on widening. ## Who decides, and how the decision is recorded Build infrastructure writes the list, because they carry the operational consequences of a pattern that is too narrow. Security reviews it and can overrule, because they carry the consequences of one that is too wide. The mechanics that make that real: the patterns live in version control as part of the build image or the shared toolchain configuration, each with a comment naming the modules it exists for; changes go through review like any other code; and jobs cannot override the settings through their own environment — which you enforce by dumping the resolved settings at the start of a build and failing if they do not match the expected values. Without that last piece the policy is advisory, because any job can set an environment variable. ## What you owe in exchange for every exemption An exempted module has no global tamper-evidence, so something else has to carry that weight: - `go.sum` becomes a reviewed artefact rather than a generated one — a diff that adds or changes a hash for an exempted path is read, not rubber-stamped. - First ingestion of a new module happens in one controlled place rather than on whichever machine happened to build first, so the pinned hash has a known provenance. - `go mod verify` runs on long-lived runners, since the local cache is now the only integrity anchor those modules have. - The exemption list is reviewed on a schedule; entries survive the reasons they were added, and an entry nobody can justify is removed. ## The cost side, stated honestly The alternative to broad exemptions is an internal proxy that can serve private modules *and* mirror the checksum database's endpoints, which is real infrastructure with a real owner and a real bill. A principal-level answer names that tradeoff instead of pretending the strict posture is free: you are choosing between the operating cost of running that mirror properly and the number of dependencies whose integrity you can no longer prove anything about. What you should refuse is the third option that usually wins by default — paying neither cost, and discovering the exemption list's true width during an incident.

  • Your internal proxy can serve private modules. Which exemption do you actually need, and which do you not?
    You need only the checksum-database exemption, because a private path has no public record to check against. You do not need the proxy exemption, since the mirror can serve those modules. Using `GONOSUMDB` rather than `GOPRIVATE` keeps private fetches flowing through the proxy, preserving its logging, caching and availability guarantees, and gives up exactly one property instead of two.
  • How would you stop an individual CI job from widening the exemption list on its own?
    Keep the patterns in the build image or the shared toolchain configuration under version control, then make the build assert them: dump the resolved GOPRIVATE, GONOPROXY, GOSUMDB and GOFLAGS at the start of every job and fail if they differ from the expected values. Without that assertion the policy is advisory, because any job can export an environment variable before invoking the toolchain.
  • What review cadence would you put on the exemption list, and what triggers removing an entry?
    Review on a fixed schedule and on every change to the module layout, because entries outlive their reasons. An entry goes when the modules it covers no longer exist, when the proxy gained the ability to serve them, or when nobody can name what it is for. Each surviving entry should carry a rationale line, and an entry added during an incident should be revisited once the incident is closed.
  • How do you argue against a proposal to simply set GOSUMDB=off in the standard build image?
    It converts every dependency in the organisation, not just internal ones, back to trusting whatever the mirror served on ingestion day, and it does so with no observable symptom. The legitimate need behind such proposals is usually network isolation, which a proxy that mirrors the database's endpoints solves. If that is genuinely impossible, scope the exemption to the paths that need it and buy the compensating controls rather than turning the check off globally.

An exemption pattern is like a guest list written as a wildcard: nobody notices it was too broad until you look at who came in, because letting extra people through never sets off an alarm.

saying these in an interview costs you the question

  • Treats GOPRIVATE as a routing convenience with no security effect
  • Uses a host-wide glob that also matches public modules
  • Lets individual CI jobs set the exemption variables
  • Assumes exempted modules lose go.sum pinning as well
  • Adds an exemption during an incident and never revisits it
  • Claims the strict posture costs nothing to operate