Documentation
¶
Overview ¶
Package secrets resolves a named secret (an API token, a serial, ...) from a fixed, security-conscious chain and nothing else.
Resolution order (first hit wins):
- the process environment — os.Getenv(NAME), trimmed; empty is "unset"
- the user-scope file — $XDG_CONFIG_HOME/bentoo/secrets (else ~/.config/bentoo/secrets)
- the system-scope file — /etc/bentoo/secrets
The files are ".env" style: NAME=value per line, "# ..." comments, an optional "export " prefix, and matched surrounding quotes stripped from the value. A missing file is a miss, never an error; a present-but-unreadable user file is ErrUnreadable, while a permission error on the root-owned system file is a silent miss (a normal user cannot read it by design).
Config files (config.yaml) are deliberately NOT part of this chain: a secret never belongs in a checked-in or world-readable config, so this package will not read them.
One deliberate exception lives OUTSIDE this package: the ${VAR} header expansion in packages.toml is an env-only substitution and stays on os.Getenv by design — it must never consult the secrets files, so it does not route through this package.
The package depends only on the Go standard library.
Index ¶
Constants ¶
This section is empty.
Variables ¶
var ErrUnreadable = errors.New("secrets: file present but unreadable")
ErrUnreadable wraps the underlying cause when a secrets file exists but its contents cannot be read or parsed. Callers match it with errors.Is. A missing file is deliberately NOT this error — absence is a normal "no secret set".
Functions ¶
func Lookup ¶
Lookup resolves name across the fixed chain env → user file → system file and returns the first hit.
- found=false, err=nil → not set anywhere (the normal "no secret" case)
- found=true, err=nil → value holds the resolved secret
- err != nil → a file exists but could not be read or parsed
The environment is consulted first: a non-empty os.Getenv(name) (trimmed) is returned without touching any file; an empty or whitespace-only value is treated as unset and falls through. A missing file is a miss, never an error. A present-but-unreadable user-scope file — including EISDIR or a permission failure — yields ErrUnreadable so a chmod-000 token can never silently degrade to "anonymous"; a permission error on the root-owned system-scope file is instead a silent miss.
func Paths ¶
func Paths() []string
Paths returns the secrets file locations Lookup searches, in order. Callers use it to build actionable "set NAME or add it to one of: ..." messages. It reflects exactly what Lookup consults — the same entries in the same order, with the internal scope tag dropped.
func Resolved ¶
func Resolved() []string
Resolved returns a copy of every non-empty value Lookup has returned in this process, each once, longest first (ties in lexical order), so that a scrubber replacing them in order redacts a value whole before any shorter value it contains. It is safe to call while Lookup runs on other goroutines.
func Scrub ¶
Scrub removes secret from s before it is logged or wrapped into an error, so a transport error echoing a request URL (or any string built from the secret) cannot leak it. Moved verbatim from autoupdate.scrubSecret.
func UserPath ¶
UserPath returns the location of the user-scope secrets file — the one an unprivileged user can actually create and chmod 600 — and whether the chain has one at all. When $HOME is unresolvable pathsFn drops the user-scope entry entirely and UserPath reports ("", false).
Callers building a "put your secret here" message MUST use this instead of Paths()[0]. Paths mirrors the RESOLUTION ORDER, so index 0 is the user file only WHEN a user file exists; with $HOME unresolvable — precisely the case of the snapshot systemd timer running as root — index 0 is the root-owned /etc/bentoo/secrets, and the message then tells an unprivileged user to write their secret into a 0600 file they cannot open. That failure is silent (Paths() is never empty) and the bad path looks plausible, which is what makes it worth a dedicated accessor.
The scope is read from each entry's own tag and never from its position, so this stays correct if the chain ever grows — the same rule that fixed F-1 inside Lookup.
Types ¶
type Logger ¶
type Logger interface {
Warn(format string, args ...interface{})
}
Logger is the minimal logging surface secrets needs. It is defined locally so this package keeps depending on the standard library alone: the log/slog logger (internal/common/logging) reads secrets.Resolved, so secrets cannot import it back without a cycle.