Documentation
¶
Index ¶
- Constants
- Variables
- func LoadRef(ref string) ([]byte, error)
- func ValidateRef(ref string) error
- type MemoryStore
- type Pool
- func (p *Pool) Add(v Version) error
- func (p *Pool) List() []Version
- func (p *Pool) ListMetadata() []VersionMetadata
- func (p *Pool) MaxVersions() int
- func (p *Pool) Name() string
- func (p *Pool) PruneExpired(now time.Time) []string
- func (p *Pool) Remove(id string) (Version, bool)
- func (p *Pool) Replace(versions []Version) error
- func (p *Pool) Runtime() bool
- func (p *Pool) Size() int
- func (p *Pool) StateAt(t time.Time) PoolState
- func (p *Pool) ValidAt(t time.Time) []Version
- type PoolState
- type Record
- type Registry
- type Sealer
- type Set
- type Store
- type Version
- type VersionMetadata
Constants ¶
const DefaultMaxVersions = 32
DefaultMaxVersions caps how many versions a Pool may hold before Add returns ErrPoolFull. Operators can override per-pool via the `max_versions` DSL flag.
Variables ¶
var ( ErrSealKeyMissing = errors.New("secrets: encryption key is empty") ErrSealKeyLength = errors.New("secrets: encryption key must decode to 32 bytes") ErrSealedTruncated = errors.New("secrets: sealed payload is truncated") ErrSealedVersion = errors.New("secrets: unsupported sealed version") ErrSealedAuthFailure = errors.New("secrets: sealed payload failed authentication") )
var ( ErrDuplicateID = errors.New("secrets: version id already present in pool") ErrPoolFull = errors.New("secrets: pool is full") ErrInvalidWindow = errors.New("secrets: valid_until must be after valid_from") ErrEmptyID = errors.New("secrets: version id is empty") ErrEmptyValue = errors.New("secrets: version value is empty") ErrMissingFrom = errors.New("secrets: version valid_from is missing") )
var ( ErrPoolExists = errors.New("secrets: pool already registered") ErrPoolNotFound = errors.New("secrets: pool not found") )
var (
ErrInvalidSecret = errors.New("invalid secret")
)
var ErrSecretRef = errors.New("invalid secret reference")
Functions ¶
func LoadRef ¶
LoadRef loads a secret value from a reference string.
Supported forms: - env:NAME - file:/path/to/secret - raw:literal-value (intended for tests/dev only) - vault:secret/path[#field] (reads from Vault HTTP API using env-configured address/token)
func ValidateRef ¶
ValidateRef validates a secret reference format without loading its value.
Supported forms: - env:NAME - file:/path/to/secret - raw:literal-value - vault:secret/path[#field]
Types ¶
type MemoryStore ¶
type MemoryStore struct {
// contains filtered or unexported fields
}
MemoryStore is an in-memory Store. It is the default when no persistent backend is configured, and it is used throughout tests.
func NewMemoryStore ¶
func NewMemoryStore() *MemoryStore
func (*MemoryStore) ListAll ¶
func (m *MemoryStore) ListAll() ([]Record, error)
func (*MemoryStore) ListByPool ¶
func (m *MemoryStore) ListByPool(poolName string) ([]Record, error)
func (*MemoryStore) Upsert ¶
func (m *MemoryStore) Upsert(rec Record) error
type Pool ¶
type Pool struct {
// contains filtered or unexported fields
}
Pool holds a named, mutable, thread-safe collection of secret Versions.
Reads (ValidAt, List, ListMetadata, Size) use RLock and are cheap. Writes (Add, Remove, Replace) use Lock.
Pointer identity matters: route-level closures built in internal/app/run.go capture *Pool and call ValidAt on every inbound request. Config reloads must mutate the pool in place (Add, Remove, Replace) rather than creating a new Pool, so that in-flight verification still sees fresh state.
func NewPool ¶
NewPool constructs a pool with an initial set of versions. If maxVersions <= 0, DefaultMaxVersions is used. The seed slice is validated and copied.
func (*Pool) Add ¶
Add inserts v. Returns ErrDuplicateID, ErrPoolFull, or a validation error. If the pool is at capacity, Add first tries to prune versions whose ValidUntil is in the past (relative to time.Now()).
func (*Pool) ListMetadata ¶
func (p *Pool) ListMetadata() []VersionMetadata
ListMetadata returns id + validity window for each version, without the secret value. Admin GET handlers use this.
func (*Pool) MaxVersions ¶
func (*Pool) PruneExpired ¶
PruneExpired removes every version whose ValidUntil is non-zero and not after now, and returns the removed version IDs. Versions with zero ValidUntil (unbounded) are never pruned. Safe for concurrent callers; uses the pool's write lock.
Used by the periodic secret-GC sweeper (internal/app) so that expired-but- not-at-cap versions do not accumulate indefinitely in memory or in the persisted runtime_secrets table. Add() already prunes opportunistically when the pool is at max_versions; PruneExpired is the unconditional variant.
func (*Pool) Remove ¶
Remove deletes the version with the given id. Returns (removed, true) if found.
func (*Pool) Replace ¶
Replace swaps the entire version set. It validates each entry and enforces the maxVersions cap. Used at startup (seeding from Store) and during config reload for non-runtime pools.
type PoolState ¶ added in v2.15.0
type PoolState struct {
Name string
Runtime bool
// Total is every version held, whatever its window. Valid + Pending +
// Expired equals it.
Total int
Valid int
Pending int
Expired int
// NextExpiry is the earliest ValidUntil among the versions valid at the
// census time. Zero when no valid version expires -- either because the
// pool has none left, or because every valid one is unbounded.
NextExpiry time.Time
// Exhaustion is when the pool stops having any valid version: the latest
// ValidUntil among the versions valid at the census time. Zero when at
// least one valid version is unbounded (it never runs dry) or when there is
// no valid version left (it already has).
//
// This is the one to alert on. NextExpiry moves on every handover of a
// healthy overlapping rotation, so a warning threshold on it is noise;
// Exhaustion moves only when the last credential is about to lapse.
Exhaustion time.Time
// Unbounded reports that at least one version valid at the census time has
// no ValidUntil, which is what distinguishes "never runs dry" from "already
// dry" in a zero Exhaustion.
Unbounded bool
}
PoolState is a point-in-time census of a pool's versions.
It exists because "how many versions does this pool hold" is not the question an operator needs answered. Validity is a function of the wall clock, so a pool holding three versions can still reject every request, and Size() cannot tell that apart from the middle of a healthy rotation. Everything that reports on a pool from the outside -- the Prometheus gauges, the /healthz?details=1 rollup, the startup and sweeper warnings -- reads this, so all three agree by construction.
type Record ¶
type Record struct {
PoolName string
ID string
Sealed []byte
NotBefore time.Time
NotAfter time.Time // zero = unbounded
CreatedAt time.Time
}
Record is the persisted shape of a single Pool entry. The Sealed field must already be AES-GCM wrapped (see Sealer); Store implementations never see the plaintext secret.
type Registry ¶
type Registry struct {
// contains filtered or unexported fields
}
Registry maps secret names to *Pool. Route-level closures capture pointers from here, so the registry is the single source of truth for "which pool does this secret_ref resolve to".
Pointer identity of a Pool must survive config reload for runtime-mutable pools. Callers achieve this by calling Pool(name) and mutating via Pool.Add/Remove/Replace, rather than creating a fresh Pool and re-registering.
func NewRegistry ¶
func NewRegistry() *Registry
func (*Registry) Register ¶
Register inserts a pool under its name. Returns ErrPoolExists if the name is already taken by a different pool. Re-registering the exact same *Pool is a no-op (idempotent).
func (*Registry) StatesAt ¶ added in v2.15.0
StatesAt censuses every registered pool as of t, ordered by pool name.
One snapshot per reporting pass, taken through the registry rather than by the caller walking Names() and Pool() itself: that walk can observe a pool registered or dropped halfway through, and a report that lists a pool it then cannot describe is worse than one taken a moment earlier.
func (*Registry) Unregister ¶
Unregister removes a pool. Returns false if the name was not present. In-flight closures that captured the *Pool pointer continue to see the (now unreferenced-by-registry) pool until they are rebuilt on reload.
type Sealer ¶
type Sealer struct {
// contains filtered or unexported fields
}
Sealer wraps plaintext secrets in AES-256-GCM. The output layout is:
[1B version=0x01][12B nonce][N-byte ciphertext || 16B tag]
A version prefix is included so that future algorithm changes (e.g. key rotation via KMS, a different AEAD) can be rolled out without breaking stored records.
func NewSealer ¶
NewSealer decodes a base64-encoded 32-byte key and constructs an AES-256-GCM AEAD. Whitespace around the key is tolerated; both standard and URL encodings are accepted, with or without padding.
type Set ¶
type Set struct {
Versions []Version
}
func (Set) SigningAt ¶
SigningAt returns the newest (most recent ValidFrom) secret that is valid at time t.
type Store ¶
type Store interface {
// Upsert inserts or replaces the record keyed by (PoolName, ID).
Upsert(rec Record) error
// Delete removes the record with the given (poolName, id). Returns true if
// something was removed, false if not found. A missing record is not an error.
Delete(poolName, id string) (bool, error)
// ListByPool returns all records for the pool. Order is unspecified.
ListByPool(poolName string) ([]Record, error)
// ListAll returns records for every pool. Used on startup to hydrate all
// registered pools in one round trip.
ListAll() ([]Record, error)
}
Store persists runtime-added secrets so they survive process restart.
Implementations must be safe for concurrent use. Upsert is idempotent on (PoolName, ID): a second call with the same key replaces the previous record. This keeps the store tolerant of admin-API retries and reload replays.
The Store does not know about Version validity windows at the application layer — those live in Pool. The Store only stores and returns Records.
type Version ¶
Version is a single secret value with a validity window.
Semantics: - ValidFrom is inclusive. - ValidUntil is exclusive; a zero value means "no end".
type VersionMetadata ¶
type VersionMetadata struct {
ID string `json:"id"`
ValidFrom time.Time `json:"not_before"`
ValidUntil time.Time `json:"not_after,omitempty"`
}
VersionMetadata is the safe-to-expose subset of a Version: everything except the secret material itself. Admin GET endpoints return this shape.