Documentation
¶
Overview ¶
Package secret models secrets that are referenced rather than copied.
A reference names the backend a secret lives in and a backend-relative path to the value. Resolving a reference produces the bytes in memory and the fully-qualified reference — always carrying a concrete version — that produced them. Callers materialize those bytes into a process environment or an in-memory file and never persist them.
The backend interface is deliberately small so that the in-cluster store and external managers (Vault, AWS Secrets Manager, GCP Secret Manager) look the same to every consumer. Writes are gated on the separate WritableBackend interface, which an external manager is expected to leave unimplemented so it stays the source of truth for its own secrets — the shape RFD-81 asks for. That gate is a convention an adapter opts into, not something the type system can prove; see WritableBackend.
Index ¶
- Constants
- Variables
- func EnvValue(backend, value string) string
- func FormatRef(path, version string) string
- func FormatSentinel(backend, ref string) string
- func IsPinned(ref string) bool
- func IsSentinel(value string) bool
- func MaterializeEnv(ctx context.Context, resolver Resolver, env []string) ([]string, error)
- func ParseRef(ref string) (path, version string, err error)
- func ParseSentinel(value string) (backend, ref string, ok bool)
- func Pin(ctx context.Context, resolver Resolver, refs []Reference) ([]string, error)
- func ValidateBackendName(name string) error
- type KeyState
- type KeyringReport
- type KeyringReporter
- type ListableBackend
- type MalformedSentinelError
- type Reference
- type Registry
- func (r *Registry) Get(name string) (SecretBackend, bool)
- func (r *Registry) Names() []string
- func (r *Registry) Register(b SecretBackend) error
- func (r *Registry) ResolveRef(ctx context.Context, backend, ref string) (SecretValue, error)
- func (r *Registry) Writable(name string) (WritableBackend, error)
- type Resolver
- type SecretBackend
- type SecretValue
- type Summary
- type VersionState
- type VersionSummary
- type WritableBackend
Constants ¶
const ClusterBackendName = "cluster"
ClusterBackendName is the instance name of the built-in in-cluster backend. It is always registered, so a cluster can hold secrets without an operator standing up an external manager first.
const SentinelScheme = "miren+secret://"
SentinelScheme prefixes an environment value that names a secret rather than holding one.
Variables ¶
var ( // ErrUnknownBackend means no backend instance is registered under the name // a reference asked for. ErrUnknownBackend = errors.New("unknown secret backend") // ErrNotFound means the backend has no secret at that path, or no such // version of it. ErrNotFound = errors.New("secret not found") // ErrVersionNotEnabled means the version exists but has been disabled or // destroyed. Resolving it fails closed rather than falling back to another // version. ErrVersionNotEnabled = errors.New("secret version is not enabled") // ErrReadOnlyBackend means a write was attempted against a backend that // only implements SecretBackend — an external manager Miren references but // does not own. ErrReadOnlyBackend = errors.New("secret backend is read-only") )
Errors callers can branch on. A failure names the reference it was for and never the value. It does wrap the backend's own error, so callers can test these sentinels with errors.Is — which means a backend must not put secret material in the errors it returns. The backends here quote only the reference; an adapter for an external manager has to hold to the same rule.
Functions ¶
func EnvValue ¶
EnvValue renders what a config variable contributes to a sandbox spec: its literal value, or a reference standing in for one when it is backend-sourced.
Every path that builds a sandbox spec from a config goes through this, so there is one answer to "does a secret ever get written into a spec?" rather than one per call site.
func FormatRef ¶
FormatRef builds the fully-qualified reference for a path at a version. Every Resolve returns one of these regardless of what it was handed, so that what a ConfigVersion records is always concrete.
func FormatSentinel ¶
FormatSentinel renders a reference as the placeholder that stands in for a value inside a sandbox spec.
A sandbox spec is a persisted entity, so a resolved value written into it would sit in etcd in plaintext — exactly what referencing a secret is meant to avoid. The spec therefore carries this placeholder, and the value is substituted in memory at the moment the container is created.
It doubles as the unit of change detection for pool reuse: the reference carries a concrete version, so it differs precisely when the bytes behind it would differ.
func IsSentinel ¶
IsSentinel reports whether a value names a secret rather than holding one.
func MaterializeEnv ¶
MaterializeEnv replaces every secret placeholder in a KEY=VALUE environment slice with the resolved value, returning a new slice and leaving the input untouched.
This is the point of use: the value exists in the returned slice, in memory, and goes straight into the container's process environment. Nothing here writes to disk, and the spec these values came from keeps carrying only references.
It fails rather than degrading. An app started with a placeholder where its credential should be does not fail at boot in a way anyone can read — it fails later, deep in whatever the credential was for. A missing resolver, an unknown backend, a revoked version, and an unreadable reference are all errors here, so the failure lands where it can name the variable.
func ParseRef ¶
ParseRef splits a backend-relative reference into its path and version. A version-less reference ("payments/stripe-key") returns an empty version, meaning "whatever is current"; a pinned one ("payments/stripe-key@x1A") returns the handle.
The separator is matched from the right so a path may itself contain "@" — external backends address secrets in shapes Miren does not control.
func ParseSentinel ¶
ParseSentinel splits a placeholder back into its backend and reference. The second return is false for an ordinary value, which is the common case.
func Pin ¶
Pin resolves a set of references and returns the fully-qualified reference each one resolves to, keyed by the same index as the input.
It is what freezes a config: an authored reference usually floats, and recording what it resolved to at mint time is how a ConfigVersion becomes answerable for the most sensitive input it carries. Because Resolve returns a concrete version even for an already-pinned input, pinning is idempotent — which is what lets a hand-set reference stay at the version it was set to while an app.toml reference re-pins on every deploy, with no second piece of state to keep in sync.
The resolved bytes are deliberately discarded. Pinning happens in the control plane at config-mint time; materializing the value belongs at the point of use, so nothing here can leak into a persisted config.
A failure names the variable and the backend, and never the value or the backend's own error text, which can quote surrounding context.
func ValidateBackendName ¶
ValidateBackendName rejects names that cannot survive a round trip through a sentinel.
The backend and the reference share one string, separated by the first "/", so a name containing a slash would parse back as a different backend and a different reference — and if both names happened to be registered, resolve a different secret entirely. Rejecting the character is cheaper than encoding around it, and costs nothing real: a backend name is an operator-chosen instance label, not a path.
Types ¶
type KeyState ¶
type KeyState struct {
ID string
Current bool
CreatedAt time.Time
// Versions is how many stored values are still wrapped by this key. A
// non-current key with a non-zero count is a rotation that has not finished
// — and the reason the key cannot be dropped yet.
Versions int
}
KeyState describes one key in a backend's keyring.
type KeyringReport ¶
type KeyringReport struct {
Keys []KeyState
// Rotating reports whether a rotation is in flight, with RotatingFrom
// naming the key being retired and Rewrapped counting progress so far.
// Without this a stalled backfill and a finished one look the same.
Rotating bool
RotatingFrom string
Rewrapped int
}
KeyringReport is the operator's view of a backend's key hierarchy.
type KeyringReporter ¶
type KeyringReporter interface {
SecretBackend
KeyringReport(ctx context.Context) (KeyringReport, error)
}
A KeyringReporter can describe the keys it holds. Only backends that own their key material implement it: an external manager's keys are its own business, and Miren has nothing to report about them.
type ListableBackend ¶
type ListableBackend interface {
SecretBackend
// List returns every secret the backend holds, without their values.
List(ctx context.Context) ([]Summary, error)
// ListVersions returns one secret's versions, without their values.
ListVersions(ctx context.Context, path string) (Summary, error)
}
A ListableBackend can enumerate what it holds. It is optional: an external manager may not expose enumeration, or may not expose it to the identity Miren reads with, in which case Miren can still resolve known references without being able to list them.
type MalformedSentinelError ¶
type MalformedSentinelError struct {
Key string
}
MalformedSentinelError reports a placeholder that carries the scheme but does not parse. It is deliberately distinct from "not a placeholder": a value that looks like a reference and cannot be read is a bug or a tampered spec, and starting the container with it verbatim would hand the app a URL where its credential should be.
func (MalformedSentinelError) Error ¶
func (e MalformedSentinelError) Error() string
type Reference ¶
type Reference struct {
// Backend is the registered instance name the value comes from.
Backend string
// Ref is the backend-relative reference, floating or already pinned.
Ref string
// Key is the variable this reference feeds, for error messages.
Key string
// Service scopes the variable to one service, or is empty for a variable
// shared by all of them.
Service string
}
Reference is one backend-sourced value inside a config, identified well enough to name it in an error without quoting the value.
type Registry ¶
type Registry struct {
// contains filtered or unexported fields
}
Registry maps a backend instance name to the code that resolves it. Runtime and build-time materialization both go through it, so neither has its own notion of where secrets come from.
A backend *type* is the code that talks to a kind of store; a backend *instance* is a named, configured registration of one. A cluster might run the built-in "cluster" instance alongside "prod-vault" and "staging-vault", two instances of the same Vault type. A reference names the instance.
func (*Registry) Get ¶
func (r *Registry) Get(name string) (SecretBackend, bool)
Get returns the backend registered under name.
func (*Registry) Register ¶
func (r *Registry) Register(b SecretBackend) error
Register adds a backend under its own name, replacing any previous registration for that instance.
A name that cannot round-trip through a reference is refused here rather than at resolve time, so a misconfigured instance fails at startup instead of silently resolving the wrong secret later.
func (*Registry) ResolveRef ¶
ResolveRef resolves a reference against the named backend, so a Registry can serve as a Resolver wherever the key material is local.
A failure names the backend and the reference, and wraps the backend's error so callers can branch on ErrNotFound and friends. That wrapping means a backend's error text reaches the caller: implementations must not put secret material in their errors, which is why the ones here quote only the reference.
func (*Registry) Writable ¶
func (r *Registry) Writable(name string) (WritableBackend, error)
Writable returns the backend registered under name if it supports writes. External managers resolve but do not accept writes, so this is how a caller asks "may Miren store into this one?" without asserting the type itself.
type Resolver ¶
type Resolver interface {
ResolveRef(ctx context.Context, backend, ref string) (SecretValue, error)
}
A Resolver resolves a fully-qualified reference that names its own backend, as carried in a sandbox spec. Consumers that materialize secrets depend on this rather than on Registry directly, so a node that holds no key material can satisfy it over RPC against the control plane instead.
type SecretBackend ¶
type SecretBackend interface {
// Name returns the instance name this backend was registered under (e.g.
// "cluster", "prod-vault"). It matches a variable's backend field.
Name() string
// Resolve returns the value for a backend-relative reference along with the
// fully-qualified reference it resolved to. The input may float ("path") or
// already be pinned ("path@version"); either way the returned
// SecretValue.Ref carries a concrete version. Resolve never writes to disk.
Resolve(ctx context.Context, ref string) (SecretValue, error)
}
A SecretBackend resolves references to secret values. Implementations talk to a specific kind of store — the in-cluster store, Vault, a cloud manager.
type SecretValue ¶
type SecretValue struct {
// Ref is the fully-resolved reference, always with a concrete version on
// the end (e.g. "payments/stripe-key@x1A"), whether or not the input ref
// carried one. ConfigVersion formulation stores Ref as the variable's
// value, freezing which version that config resolves — so every sandbox
// built from it sees the same bytes even after the secret rotates, and the
// version advances only when a new ConfigVersion is minted.
Ref string
// Bytes is the raw secret value for Ref. Callers must not persist it.
Bytes []byte
}
SecretValue is a resolved secret held in memory.
type Summary ¶
type Summary struct {
Path string
Backend string
CurrentVersion string
Versions []VersionSummary
}
Summary describes a secret and its versions, for operators deciding whether a rotation or a revocation is safe. It never carries a value.
type VersionState ¶
type VersionState string
VersionState is the lifecycle state of a single secret version. Only StateEnabled resolves; the other two fail closed.
const ( StateEnabled VersionState = "enabled" StateDisabled VersionState = "disabled" StateDestroyed VersionState = "destroyed" )
type VersionSummary ¶
type VersionSummary struct {
// Version is the handle a reference names this version by, after the "@".
Version string
// State is the version's lifecycle state. Only StateEnabled resolves.
State VersionState
// CreatedAt is when the version was stored.
CreatedAt time.Time
// Current reports whether a floating reference resolves to this version.
Current bool
}
VersionSummary describes one version of a secret without its payload.
type WritableBackend ¶
type WritableBackend interface {
SecretBackend
// Put stores a new version of the secret at path.
//
// Concurrent writers are serialized internally: the pointer to the current
// version moves under a compare-and-set on the state the write was prepared
// against, and a loser re-reads and retries rather than clobbering. This is
// not a caller-visible precondition — there is no way to ask for "only if
// current is still @x1A", so two racing writes both land and the later one
// wins.
//
// Storing a value identical to the current version reuses it rather than
// minting a duplicate, which is what reused reports. Callers should take
// that from here rather than comparing versions around the call, since
// anything read beforehand can be stale by the time the write lands.
Put(ctx context.Context, path string, value []byte) (version string, reused bool, err error)
// SetState transitions a specific version between enabled, disabled and
// destroyed. Anything still pinned to a version that leaves enabled fails
// closed on its next resolve.
SetState(ctx context.Context, ref string, state VersionState) error
}
A WritableBackend additionally supports storing and managing secrets. External backends are expected to leave it unimplemented: their source of truth is managed outside Miren, per RFD-81's "reference, don't copy".
Registry.Writable gates writes on this, so nothing writes to a backend that did not opt in. Note what that is not: Go interfaces are structural, so an adapter that happens to define Put and SetState satisfies it whether or not Miren owns the store behind it. The split keeps a generic write path from reaching a read-only backend; it does not police what an adapter author chooses to implement.
Directories
¶
| Path | Synopsis |
|---|---|
|
Package cluster implements the in-cluster secret backend.
|
Package cluster implements the in-cluster secret backend. |
|
Package keyring holds the cluster's key hierarchy for secrets at rest and performs the envelope encryption around it.
|
Package keyring holds the cluster's key hierarchy for secrets at rest and performs the envelope encryption around it. |
|
Package remote resolves secret references over RPC, for nodes that hold no key material.
|
Package remote resolves secret references over RPC, for nodes that hold no key material. |