Help us shape the future of Go! Take the 2026 Go Developer Survey

credentialposture

package
v0.49.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Oct 25, 2026 License: MIT Imports: 9 Imported by: 0

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

Constants

View Source
const SlotCredential features.Slot = "credential"

SlotCredential is the feature-registry slot a credential descriptor is contributed under, keyed by the feature that consumes it (spec 0199 OQ5). A descriptor with no feature is contributed under features.Global.

Variables

View Source
var ErrMalformedKeychainRef = errors.NewSentinel(
	"gtb.credentialposture.malformed_keychain_ref",
	`a keychain reference is "service/account"`,
)

ErrMalformedKeychainRef is a keychain reference that is not "service/account".

View Source
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 a terminal check 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: the extension point spec 0189 Q6 settled on. A bundle declares the credential it owns, and every reporting surface picks it up from the feature registry without a second list to keep in step.

Three such lists existed before this and none of them knew about the others: doctor's LiteralCredentialKeys, config migrate's knownCredentials (whose own comment asks the reader to keep them in sync by hand) and the forge profiles. A credential stored under a key nobody had added was invisible. A downstream tool built on GTB registers its own credentials the same way, so its secrets are reported rather than silently unexamined.

Registering the same owner and literal key twice replaces the earlier entry rather than duplicating it, so a tool that re-registers during a second wiring pass does not produce two reports for one credential.

func RegisterOn added in v0.43.0

func RegisterOn(r features.Registry, d Descriptor)

RegisterOn is Register against a caller's registry.

func StorageModeOptions

func StorageModeOptions(ctx context.Context, io props.IO, 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
	// Feature is the feature that consumes this credential (a forge feature,
	// the ai feature), as its FeatureID string. A reporting surface uses it
	// to speak only about what the tool has enabled; empty means always.
	Feature string
	// Providers names the chat providers that consume this credential, so a
	// reporting surface can speak only about what the binary links; empty
	// means the feature alone decides (spec 0196 D12).
	Providers []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 DeclaredFor added in v0.43.0

func DeclaredFor(set features.Set) []Descriptor

DeclaredFor returns the credentials of the features enabled in set, plus those declared under no feature, so doctor speaks only about what the tool has (#55) with no predicate of its own.

func Registered

func Registered() []Descriptor

Registered returns every declared credential, ordered by owner then label so a report is stable between runs. It reads a snapshot of the default registry: every credential this binary declared, whichever features the tool enabled. DeclaredFor is the enabled-only view.

func RegisteredIn added in v0.43.0

func RegisteredIn(s features.Snapshot) []Descriptor

RegisteredIn is Registered over a snapshot.

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 DualCredentialKeys added in v0.43.0

type DualCredentialKeys struct {
	User, UserEnv, Password, PasswordEnv, Keychain, Account string
}

DualCredentialKeys is the config layout of a username and password pair under a prefix. Both halves share one keychain entry, which holds a JSON blob rather than a bare secret.

func DualCredential added in v0.43.0

func DualCredential(prefix string) DualCredentialKeys

DualCredential lays out the `<prefix>.username`, `<prefix>.app_password` and shared `<prefix>.keychain` keys Bitbucket uses.

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 a terminal check
	// 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, io props.IO) 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. io is the invocation's streams (Props.GetIO()): a human is present when its stdin is a terminal.

type ModeLabels

type ModeLabels struct {
	Env      string
	Keychain string
	Literal  string
}

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

func Resolve(ctx context.Context, cfg Reader, d Descriptor) (Posture, error)

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

func ResolveCredential(ctx context.Context, cfg Reader, d Descriptor) (string, Posture, error)

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

func (p Posture) Deprecated() bool

Deprecated reports whether the credential in effect is stored in a deprecated mode — a literal in a config file.

func (Posture) String

func (p Posture) String() string

String renders the posture for an operator.

It is deliberately the only rendering path, so the never-log-values rule has one place to hold rather than one per call site.

type Reader

type Reader interface {
	GetString(key string) string
}

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

type Result struct {
	Posture Posture
	Err     error
}

Result pairs one credential's posture with the error, if any, that stopped it resolving.

func Report added in v0.43.0

func Report(ctx context.Context, cfg Reader, descriptors []Descriptor, include func(Descriptor) bool) []Result

Report resolves posture for the given descriptors that include admits.

func ReportAll

func ReportAll(ctx context.Context, cfg Reader) []Result

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.

func ReportFor added in v0.43.0

func ReportFor(ctx context.Context, cfg Reader, set features.Set, include func(Descriptor) bool) []Result

ReportFor resolves posture for the credentials of the features enabled in set (DeclaredFor), further narrowed by include: it is what lets doctor speak only about what the tool has (#55) and, for a chat credential, a provider that consumes it linked (spec 0196 D12).

func ReportWhere added in v0.43.0

func ReportWhere(ctx context.Context, cfg Reader, include func(Descriptor) bool) []Result

ReportWhere is ReportAll over the declared descriptors include admits.

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

func (r Rung) Read(ctx context.Context, cfg Reader, d Descriptor) (string, error)

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.

type SSHPassphraseKeys added in v0.48.0

type SSHPassphraseKeys struct {
	Keychain, Account string
}

SSHPassphraseKeys is where a generated SSH key's passphrase is referenced from config, and the keychain account it is stored under (spec 0210 D1).

func SSHPassphrase added in v0.48.0

func SSHPassphrase(prefix string) SSHPassphraseKeys

SSHPassphrase lays out the `<prefix>.ssh.key.keychain` reference and its `<prefix>.ssh.passphrase` keychain account.

type Shadow

type Shadow struct {
	// Key is the config key still holding a copy.
	Key string
	// Origin is the storage mode that copy uses.
	Origin Origin
}

Shadow is a lower-precedence copy of a credential that is present but not in effect.

type SingleTokenKeys added in v0.43.0

type SingleTokenKeys struct {
	Env, Keychain, Literal, Account string
}

SingleTokenKeys is the config layout of a single-token credential under a prefix: the three storage-mode keys and the keychain account the wizard and the migration write the secret under.

func SingleToken added in v0.43.0

func SingleToken(prefix string) SingleTokenKeys

SingleToken lays out the `<prefix>.auth.*` subtree. Every forge and the direct release source keep their token here, so the shape is stated once.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL