The keychain opt-out¶
The OS-keychain backend lives in a separate subpackage,
credentials/keychain, that you activate with a
blank import. This page explains why it is structured that way, how the opt-out
actually works at link time, and how to verify it.
The requirement: a provably keychain-free build¶
Some deployments — regulated environments, air-gapped hosts, compliance-audited tools — must ship a binary that performs no inter-process communication with the platform keychain: no D-Bus session-bus traffic, no Keychain Services calls, nothing to review. "We do not use the keychain" is not enough; they need "the keychain code is not in the binary", provable from the artefact.
That requirement drives the whole design.
How the opt-out works¶
The core package defines the Backend interface and installs a stub backend at
init: every operation returns ErrCredentialUnsupported, and it imports nothing but
the standard library and cockroachdb/errors. No keychain code is reachable.
The real backend lives in the keychain subpackage, which imports
go-keyring (and, transitively, godbus on
Linux and wincred on Windows). Its init() calls RegisterBackend, swapping the
stub for the go-keyring implementation. A tool activates it with:
The key property: Go's linker performs dead-code elimination on unimported
packages. If your main never imports the keychain subpackage (directly or
transitively), the linker never pulls go-keyring, godbus, or wincred into the binary.
They exist in the module but not in the artefact. Keep the import in a dedicated
file (cmd/mytool/keychain.go) and a build variant that deletes that file ships
keychain-free.
This is dependency inversion: the core exposes a registration seam
(RegisterBackend),
and the optional heavy dependency plugs into it from the outside, so the core never
depends on it.
Verify it from the artefact, not the source¶
A source-level SBOM generated from go.sum lists go-keyring regardless — the module
graph contains it either way. That is expected and not a finding. What matters is the
linked binary:
# Build a keychain-free variant (no keychain blank import), then inspect it.
go build -o mytool ./cmd/mytool
go version -m mytool # module build info
# or, definitively, a binary-level SBOM:
cyclonedx-gomod bin -json mytool | grep -i keyring # → no matches
A binary-level SBOM tool (syft, cyclonedx-gomod bin) reads what the linker actually
included. When the blank import is absent, go-keyring does not appear. That is the
audit evidence.
The module's own depfootprint_test.go encodes the same boundary as a test: the core
and credtest packages must not depend on go-keyring, and the keychain subpackage
must — a regression on either side fails CI.
Why a subpackage, not a separate module¶
You might expect each backend to be its own module (as the toolkit's chat providers
are). It is a deliberate, recorded decision that credentials does not do this.
The go-tool-base decision log rejected a bundled "secrets-provider registry" with
first-party vendor implementations (Vault, SSM, 1Password): secrets management is
deployment-specific, and shipping one vendor's adapter invites a rabbit hole of them.
The conclusion was that Backend is the extension point and tool authors supply
their own vendor adapters — so there is no family of first-party backend modules to
keep version-aligned.
That leaves exactly one first-party backend: the OS keychain. It is the light,
canonical, cross-platform local store — the natural default, and go-keyring is a far
lighter dependency than any cloud SDK. Splitting one small backend into its own module
would add a version-compatibility matrix for no benefit. So it stays a subpackage,
isolated by the linker rather than by a module boundary. Heavy, vendor-specific stores
do not become subpackages either — they are your custom Backend.
Consequences¶
- The default build is safe by default. A tool that does nothing links the stub; keychain support is an explicit act (the import), not something to remember to disable.
- The opt-out is a one-line diff. Adding or removing the blank import is the whole operation — no build tags, no separate release artefacts.
- The core stays framework-free. go-keyring never enters the core's dependency
graph, so tools that only import
credentials(not the subpackage) never carry it.