secret

package
v0.16.2 Latest Latest
Warning

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

Go to latest
Published: Sep 29, 2026 License: Apache-2.0 Imports: 7 Imported by: 0

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

View Source
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.

View Source
const SentinelScheme = "miren+secret://"

SentinelScheme prefixes an environment value that names a secret rather than holding one.

Variables

View Source
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

func EnvValue(backend, value string) string

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

func FormatRef(path, version string) string

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

func FormatSentinel(backend, ref string) string

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 IsPinned

func IsPinned(ref string) bool

IsPinned reports whether a reference already names a concrete version.

func IsSentinel

func IsSentinel(value string) bool

IsSentinel reports whether a value names a secret rather than holding one.

func MaterializeEnv

func MaterializeEnv(ctx context.Context, resolver Resolver, env []string) ([]string, error)

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

func ParseRef(ref string) (path, version string, err error)

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

func ParseSentinel(value string) (backend, ref string, ok bool)

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

func Pin(ctx context.Context, resolver Resolver, refs []Reference) ([]string, error)

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

func ValidateBackendName(name string) error

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.

func (Reference) String

func (r Reference) String() string

String renders a reference the way it is written and displayed.

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 NewRegistry

func NewRegistry() *Registry

NewRegistry creates an empty registry.

func (*Registry) Get

func (r *Registry) Get(name string) (SecretBackend, bool)

Get returns the backend registered under name.

func (*Registry) Names

func (r *Registry) Names() []string

Names returns the registered instance names, sorted, for display.

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

func (r *Registry) ResolveRef(ctx context.Context, backend, ref string) (SecretValue, error)

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.

Jump to

Keyboard shortcuts

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