Skip to content

Trust model

This page explains what each storage mode protects against, so you can reason about which to offer for a given deployment rather than memorising a table. The short version: the package's job is to keep a secret out of your config file, and to make the one mode that does not do that (ModeLiteral) a deliberate, CI-refused choice.

The threat the package addresses

The primary threat is a plaintext secret committed to, or left in, a config file. Config files get checked into git, copied between machines, attached to bug reports, and synced to backups. A long-lived API token or VCS credential sitting in one is the classic credential-leak vector.

The three modes are three answers to "where does the secret live instead?":

Mode Config records Secret lives in Leak surface
ModeEnvVar a variable name the process environment (shell profile, CI secret store, mounted secret) whatever can read the environment
ModeKeychain a service/account reference the OS keychain (macOS Keychain, Linux Secret Service, Windows Credential Manager) the OS keychain's own access control
ModeLiteral the secret itself the config file anyone who can read the file

ModeEnvVar is the default because it has the best portability-to-safety ratio: the secret never touches the config, and the same setup works identically in local dev and CI.

Why literal mode is refused under CI

A literal write in CI almost always ends badly: config files in CI are frequently echoed to build logs, cached between jobs, or uploaded as artefacts — any of which turns "plaintext on disk" into "plaintext in a shared, retained log". So IsCI (true when CI=true) gates two things:

  1. ModeChoices omits literal from the menu under CI, so it is never offered.
  2. RefuseLiteralUnderCI returns a hinted error if literal is selected under CI anyway — via a config file, a script, or a future code path that bypassed the menu.

The second is the actual control; the first is a UX nicety. Both funnel through the same helper so the rule lives in exactly one place and cannot drift.

What the package does not protect against

Being clear about the boundaries matters more than a longer feature list:

  • A compromised host. If an attacker can read your process environment or unlock your keychain, no storage mode helps — the secret is retrievable by design, because your tool must retrieve it too. The modes reduce the at-rest, in-config surface, not the live process surface.
  • Secret rotation, audit, or policy. The package stores and resolves; it does not rotate, expire, or log access. A backend that needs those (Vault, a cloud KMS) is a custom Backend you supply.
  • Memory scrubbing. Go strings are immutable and may be copied by the runtime, so reliable in-memory zeroing is not something the package promises.
  • Encrypting the config file. ModeLiteral is plaintext by definition; the package's answer to "don't store plaintext" is "use a different mode", not "encrypt the literal".
Deployment Recommended Why
Local desktop env-var reference, or OS keychain secret out of config; keychain adds OS-level access control
CI/CD pipeline env-var reference only populated by the platform's secret injection; literal refused
Container / Kubernetes env-var reference external secret (K8s Secret, CSI) mounted as an env var
Throwaway / air-gapped literal accepted no external secret store; the plaintext-on-disk risk is accepted knowingly
Regulated / audited build env-var reference only do not import the keychain subpackage — see the keychain opt-out

Sentinel errors and safe fall-through

Resolution is a fall-through chain, and two sentinels let callers distinguish the cases without ever leaking secret material:

Error Meaning Caller does
ErrCredentialUnsupported no keychain backend registered fall through to the next source
ErrCredentialNotFound backend healthy, entry absent fall through, or treat as "not configured"

Both wrap cleanly with errors.Is, and neither embeds credential material in its message — a discipline every Backend implementation must uphold too.