Documentation
¶
Overview ¶
Package credentialposture reports where a credential comes from, without ever reporting what it is.
A credential's posture is three separate facts, and confusing them is what made the previous checks hard to act on:
- where it is STORED — an environment reference, a keychain entry, or a literal in a config file;
- where it RESOLVES FROM — which of those actually supplied the value, given a fixed precedence;
- what is SHADOWED — the lower-precedence copies that are still present and would win if the one above them went away.
`doctor` previously reported only the first, and only for a hardcoded list of keys. "A literal credential is in use" and "a literal credential is dead configuration underneath a working environment reference" therefore read identically, while being very different situations: one is an active exposure, the other is untidy. See spec 0189.
Nothing here returns or renders a credential value. Every type is designed so that reporting posture cannot leak the secret whose posture it describes.
Index ¶
- Variables
- func DefaultStorageMode(env ModeEnvironment) credentials.Mode
- func RecommendedLabel(env ModeEnvironment, mode credentials.Mode) string
- func Register(d Descriptor)
- func StorageModeOptions(ctx context.Context, labels ModeLabels) ([]credentials.ModeChoice, credentials.Mode)
- type Descriptor
- type ModeEnvironment
- type ModeLabels
- type Origin
- type Posture
- type Reader
- type Result
- type Rung
- type Shadow
Constants ¶
This section is empty.
Variables ¶
var ErrSecureStoreRegressed = errors.NewSentinel(
"gtb.credentialposture.secure_store_regressed",
"keychain unavailable; refusing to fall back to a plaintext credential",
)
ErrSecureStoreRegressed reports a credential that was established in a secure store and has silently fallen back to a plaintext copy.
The precedence order — env reference, keychain, literal, fallback variable — is what makes incremental migration safe, and it stays. But it also means a tool resolving from the keychain drops to a literal the moment the keychain is unavailable: a locked session, a container without the Secret Service, a rebuild without the backend. Nothing distinguished "always was a literal" from "regressed to one", so the safest configuration failed the most quietly.
Functions ¶
func DefaultStorageMode ¶
func DefaultStorageMode(env ModeEnvironment) credentials.Mode
DefaultStorageMode picks the storage mode to use when the operator has not said which they want.
An environment-variable reference is an excellent CI interface and a poor interactive default: an exported variable is inherited by every process the shell spawns, so the secret is readable by everything the developer runs. A keychain entry stays put until something asks for it. Where a keychain is actually usable and a human is present, that is the better default — and the operator should not have to know to ask for it. Spec 0189 R6/D8.
The test is deliberately the same one the setup wizard already applies when deciding whether to offer keychain at all, rather than a second rule that can disagree with it — plus an explicit CI exclusion.
That exclusion is not belt-and-braces. This design originally rested on "CI needs no special case, because a pipeline has no terminal", and that is false: GitLab's runners allocate a TTY, so IsInteractive reports true inside a pipeline. The keychain probe happened to save it — a runner has no keychain — but a rule that is right only because a second condition rescues it is a rule waiting to be wrong. A CI run takes the CI default outright.
An explicit choice always wins over this, and so does a configured default — this is only consulted when nothing has been stated.
func RecommendedLabel ¶
func RecommendedLabel(env ModeEnvironment, mode credentials.Mode) string
RecommendedLabel returns the suffix marking whichever mode DefaultStorageMode would pick, so a prompt's "(recommended)" follows the actual recommendation instead of being pinned to one option.
A prompt that recommends one thing while defaulting to another is worse than either alone: it tells the user their considered choice is wrong.
func Register ¶
func Register(d Descriptor)
Register declares a credential for posture reporting.
Registering the same owner and literal key twice replaces the earlier entry rather than duplicating it, so a tool that re-registers during a test or a second wiring pass does not produce two reports for one credential.
func StorageModeOptions ¶
func StorageModeOptions(ctx context.Context, labels ModeLabels) ([]credentials.ModeChoice, credentials.Mode)
StorageModeOptions builds a credential wizard's storage-mode choices and the mode it should pre-select.
It establishes the environment once — one keychain probe, not one per question — and uses it for both decisions, so the option list and the pre-selection cannot disagree. Every GTB wizard went through the same three steps with the recommendation hardcoded onto the environment-variable option; this is those steps, with the recommendation following what is actually recommended here.
The probe is a live round-trip and the caller's context should be bounded; credentials.KeychainOpTimeout is the bound the wizards already use.
Types ¶
type Descriptor ¶
type Descriptor struct {
// Owner names the declaring bundle, e.g. "forge:github". It appears in
// reports so a finding can be traced back to what declared it.
Owner string
// Label is the human name used in a report, e.g. "GitHub".
Label string
// EnvKey holds the NAME of an environment variable.
EnvKey string
// KeychainKey holds a "service/account" reference.
KeychainKey string
// LiteralKey holds the secret itself, in plaintext. Deprecated storage.
LiteralKey string
// FallbackEnv is the well-known variable tried when config says nothing.
FallbackEnv string
}
Descriptor declares one credential: which keys hold it, in which storage mode, and the well-known variable to fall back to.
A bundle declares its own credential and the assembling code supplies the descriptor, so a new credential is covered by declaring it rather than by editing a list somewhere else. Three such lists existed before this — doctor's LiteralCredentialKeys, migrate's knownCredentials (whose own comment asks the reader to keep them in sync by hand), and the forge profiles.
func Registered ¶
func Registered() []Descriptor
Registered returns every declared credential, ordered by owner then label so a report is stable between runs.
func (Descriptor) Rungs ¶
func (d Descriptor) Rungs() []Rung
Rungs returns the precedence chain, declared once.
Stating it here and having both the resolver and the shadow report walk this slice is what stops the two disagreeing — the property pkg/vcs's forgeRungs already relies on, kept when the logic was generalised.
type ModeEnvironment ¶
type ModeEnvironment struct {
// CI reports whether this is an automated run.
//
// It is checked separately from Interactive because a terminal is NOT
// evidence of a human: GitLab's runners allocate a TTY, so IsInteractive
// reports true inside a pipeline. Relying on the terminal alone made a
// pipeline pick the interactive default, which is exactly backwards.
CI bool
// Interactive reports whether a human is at the terminal.
Interactive bool
// KeychainUsable reports whether a keychain backend is registered AND
// accepted a live round-trip. credentials.Probe answers this; a backend
// that is merely linked is not enough, because an option that fails the
// moment it is chosen is worse than one never offered.
KeychainUsable bool
}
ModeEnvironment describes the facts that decide the default storage mode.
It is data rather than something this package discovers for itself, so the rule below is a pure function: the callers already establish both facts — every wizard calls credentials.Probe to decide whether to *offer* keychain — and passing what they know avoids a second probe and keeps the decision testable without mocking anything.
func DiscoverModeEnvironment ¶
func DiscoverModeEnvironment(ctx context.Context) ModeEnvironment
DiscoverModeEnvironment establishes the environment from the running process.
It lives at the edge on purpose. Library code takes a ModeEnvironment as data so its behaviour is a function of its inputs; only a command, which is already the process, discovers what the process looks like. A library that probed for itself would behave differently under `go test`, under a pipe and under a terminal — and would be untestable without faking the world.
The caller should bound ctx: the probe is a live keychain round-trip.
type ModeLabels ¶
ModeLabels are the human-facing option labels a wizard shows. The "(recommended)" marker is appended by StorageModeOptions rather than being written into these, so it can follow the environment.
type Origin ¶
type Origin string
Origin names the rung that supplied a credential. It is a key name or a variable role — never a value — so it is safe to print.
const ( // OriginNone means no rung produced a credential. OriginNone Origin = "none" // OriginEnvRef is the config-named environment variable, dereferenced. OriginEnvRef Origin = "auth.env" // OriginKeychain is the config-named keychain entry, dereferenced. OriginKeychain Origin = "auth.keychain" // OriginLiteral is the literal value in config. OriginLiteral Origin = "auth.value" // OriginFallbackEnv is the well-known fallback variable (e.g. GITHUB_TOKEN). OriginFallbackEnv Origin = "fallback environment variable" )
type Posture ¶
type Posture struct {
// Owner and Label come from the descriptor that declared the credential.
Owner string
Label string
// Origin is the rung that supplied the credential, or OriginNone.
Origin Origin
// Key is the config key the winning rung read. Empty when the fallback
// variable won or nothing resolved.
Key string
// Shadowed lists the lower-precedence copies still present, highest
// precedence first.
Shadowed []Shadow
}
Posture is one credential's resolved state, reported without its value.
func Resolve ¶
Resolve walks the precedence chain and reports which rung supplies the credential and which lower rungs still hold one.
Error handling mirrors the forge resolver deliberately: a rung that fails does not stop the walk, because a later rung may still supply a working credential — and in that case the configuration genuinely does work. The retained error is returned only when nothing resolved, which is exactly when a bare "no credential" would otherwise hide the reason. That is what keeps a configured-but-broken credential diagnosed rather than reported as absent.
func ResolveCredential ¶
ResolveCredential walks the precedence chain and returns the credential alongside its posture, enforcing the secure-store invariant.
Error handling mirrors the forge resolver deliberately: a rung that fails does not stop the walk, because a later rung may still supply a working credential — and in that case the configuration genuinely does work. The retained error is returned only when nothing resolved, which is exactly when a bare "no credential" would otherwise hide the reason. That is what keeps a configured-but-broken credential diagnosed rather than reported as absent.
The one exception is the invariant below, where falling through IS the fault.
func (Posture) Deprecated ¶
Deprecated reports whether the credential in effect is stored in a deprecated mode — a literal in a config file.
type Reader ¶
Reader is the narrow config surface a posture walk needs. Both `config.View` and `forge.Config` satisfy it, which is what lets this package serve forges and AI providers without depending on either.
type Result ¶
Result pairs one credential's posture with the error, if any, that stopped it resolving.
func ReportAll ¶
ReportAll resolves posture for every registered credential.
The keychain rung bounds its own read (see readKeychain), so there is no deadline around the whole walk here. There used to be, with the same duration — and it meant a locked keychain consumed the credential's entire budget, so the walk aborted on a context error before it could judge the rungs below. The invariant that refuses a plaintext fallback could never fire in the one case it exists for.
A resolution error is attached to its own credential rather than aborting the run: "this one is configured but broken" is a finding, and losing the other nine to it would be a worse report than any of them.
type Rung ¶
type Rung struct {
Origin Origin
// Key is the config key this rung reads, empty for the fallback variable
// (which is not configured anywhere).
Key string
// contains filtered or unexported fields
}
Rung is one step of the precedence chain: what it is, and how to read it.
func (Rung) Read ¶
Read returns the credential this rung supplies, or "" if it supplies none.
It exists so a caller that must actually *obtain* the credential — rather than report on it — can compose the same rungs in the same order instead of declaring the precedence a second time. pkg/vcs builds its forge credential chain this way, which is what stops the supplying path and the reporting path disagreeing about precedence.