secrets

package
v0.8.2 Latest Latest
Warning

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

Go to latest
Published: Oct 22, 2026 License: Apache-2.0 Imports: 27 Imported by: 0

Documentation

Overview

Package secrets implements envelope encryption: per-app data encryption keys, wrapped by a master key held only by the control plane, using filippo.io/age for the crypto primitives.

masterkey.go, dek.go, and value.go are the primitives layer: MasterKey, DEK wrap/unwrap, and value encrypt/decrypt, standalone and fully tested, knowing nothing about storage. manager.go (the secret-storage follow-up, landed 2026-08-12) is the integration layer: Manager combines a MasterKey with internal/store's new service_secrets/ service_secret_values tables to generate-or-reuse a per-app DEK, encrypt and persist a value, and decrypt one back. Callers: internal/deploy.Pipeline checks whether a { secret: true } var has a value before deploying (never the value itself), and internal/reconcile/application.Controller decrypts a value only immediately before docker.Runtime.Create, never persisting it, which is exactly the sequencing envelope encryption is meant to guarantee.

Index

Constants

View Source
const ScopeServiceSecret = "service_secret"

ScopeServiceSecret is the Binding scope of every value Manager stores.

View Source
const TransitName = "vault-transit"

TransitName is the backend name stored in a wrapped master key.

View Source
const WrappedPrefix = "levelrail-wrapped-v1:"

WrappedPrefix marks a master key file or env value that an external key service wrapped, so loading it never falls back to treating it as an age identity.

Variables

View Source
var (
	// ErrDEKRotationBusy means another rotation is already running.
	ErrDEKRotationBusy = errors.New("secrets: a data key rotation is already running")
	// ErrDEKUnwrap means a stored DEK did not unwrap under the active
	// master key, so nothing for that service was changed.
	ErrDEKUnwrap = errors.New("secrets: data key does not unwrap under the active master key")
	// ErrDEKRotationUnsupported means the store cannot rotate DEKs.
	ErrDEKRotationUnsupported = errors.New("secrets: store does not support data key rotation")
)
View Source
var (
	// ErrBindingMismatch means a ciphertext decrypted fine but was sealed
	// for a different slot than the one it was read from.
	ErrBindingMismatch = errors.New("secrets: ciphertext is bound to a different slot")
	// ErrMalformedEnvelope means a bound ciphertext authenticated but its
	// framing did not parse.
	ErrMalformedEnvelope = errors.New("secrets: malformed secret envelope")
	// ErrInvalidBinding means a Binding has an empty or oversized field.
	ErrInvalidBinding = errors.New("secrets: invalid binding")
	// ErrValueTooLarge means a plaintext exceeds maxPlaintextBytes.
	ErrValueTooLarge = errors.New("secrets: value too large")
)
View Source
var ErrLegacyRejected = errors.New("secrets: legacy unbound ciphertext rejected")

ErrLegacyRejected is returned by Resolve for a legacy unbound ciphertext when the Manager was built WithRequireBound(true).

View Source
var ErrSecretLocked = errors.New("secrets: value is locked")

ErrSecretLocked is returned by SetValueGuarded when overwriting an existing, locked value without overwriteLocked=true; callers surface it as a 409.

View Source
var ErrValueNotFound = errors.New("secrets: value not found")

ErrValueNotFound means no secret value has been set for a given (service, env key) pair, whether the DEK or the ciphertext is missing.

View Source
var ErrWrapperRequired = errors.New("secrets: master key is wrapped by an external key service but none is configured")

ErrWrapperRequired means a wrapped master key was found but no key service is configured.

Functions

func CommitPendingMasterKeyFile

func CommitPendingMasterKeyFile(path string) error

CommitPendingMasterKeyFile atomically renames path.new over path.

func DecryptValue

func DecryptValue(dek []byte, b Binding, ciphertext []byte) (plaintext string, legacy bool, err error)

DecryptValue reverses EncryptValue, failing with ErrBindingMismatch when the ciphertext was sealed for a different Binding. legacy is true for a ciphertext written before bindings existed, which carries no slot to check.

func DiscardPendingMasterKeyFile

func DiscardPendingMasterKeyFile(path string)

DiscardPendingMasterKeyFile removes a staged key; a missing file is not an error.

func EncryptValue

func EncryptValue(dek []byte, b Binding, plaintext string) ([]byte, error)

EncryptValue encrypts plaintext under dek (a raw, unwrapped DEK) with AES-256-GCM, sealing b inside so the result only decrypts back through DecryptValue with the same Binding.

func IsWrappedMasterKey

func IsWrappedMasterKey(s string) bool

IsWrappedMasterKey reports whether s is a wrapped blob rather than a plain identity.

func PendingMasterKeyPath

func PendingMasterKeyPath(path string) string

PendingMasterKeyPath is where a rotated master key is staged before the DB rotation commits.

func PersistMasterKeyFile

func PersistMasterKeyFile(path, serialized string) error

PersistMasterKeyFile atomically writes serialized (a MasterKey's String() output) to path: written to a temp file in the same directory first, then renamed into place, so a crash mid-write can never leave a truncated, unloadable key file behind.

func RotateStoredDEKs

func RotateStoredDEKs(ctx context.Context, s Store, oldKey, newKey *MasterKey) error

RotateStoredDEKs re-wraps every DEK s holds from oldKey to newKey, via Store.RotateServiceDEKs's single transaction: if any DEK fails to unwrap under oldKey (wrong key supplied, corrupt data), the callback returns an error, the transaction rolls back, and no row changes. This is the low-level primitive Manager.RotateMasterKey builds on; it takes both keys explicitly (rather than reading Manager state) so it can be tested in isolation, including the wrong-old-key case.

func StagePendingMasterKeyFile

func StagePendingMasterKeyFile(path, serialized string) error

StagePendingMasterKeyFile durably writes serialized next to path as path.new (0600, fsynced).

func UnwrapMasterKey

func UnwrapMasterKey(ctx context.Context, blob string, w KeyWrapper) (string, error)

UnwrapMasterKey opens a blob made by WrapMasterKey and returns the serialized master key. It refuses a blob that a different backend sealed.

func WrapMasterKey

func WrapMasterKey(ctx context.Context, serialized string, w KeyWrapper) (string, error)

WrapMasterKey seals a serialized master key as WrappedPrefix + backend name + ciphertext.

Types

type Binding added in v0.2.0

type Binding struct {
	Scope string
	Owner string
	Key   string
}

Binding is the storage slot a ciphertext belongs to. It is sealed inside the ciphertext and checked on every decrypt, so a ciphertext copied into a different slot fails closed instead of decrypting.

type BindingStatus added in v0.2.0

type BindingStatus struct {
	Total  int
	Bound  int
	Legacy int
}

BindingStatus counts stored values by envelope format.

type DEKRotateAllResult

type DEKRotateAllResult struct {
	RunID       string
	Resumed     bool
	Total       int
	Rotated     int
	Skipped     int
	FailedCount int
	Failed      []DEKRotateFailure
	// Complete is false when the pass was cancelled before it finished; a
	// rerun resumes it.
	Complete bool
}

DEKRotateAllResult reports a rotate-all pass.

type DEKRotateFailure

type DEKRotateFailure struct {
	Service string
	Reason  string
}

DEKRotateFailure names a service that could not be rotated, never why in detail.

type DEKRotateResult

type DEKRotateResult struct {
	Service   string
	Values    int
	RotatedAt time.Time
}

DEKRotateResult reports one service rotation.

type DEKRotationStore

type DEKRotationStore interface {
	ListServiceDEKStatus(ctx context.Context) ([]store.ServiceDEKStatus, error)
	RotateServiceDEK(ctx context.Context, serviceName string, at time.Time,
		rewrite func(wrapped []byte, rows []store.SecretCiphertext) ([]byte, [][]byte, error)) (int, error)
	LatestDEKRotationRun(ctx context.Context) (store.DEKRotationRun, bool, error)
	CreateDEKRotationRun(ctx context.Context, r store.DEKRotationRun) error
	UpdateDEKRotationRun(ctx context.Context, r store.DEKRotationRun) error
}

DEKRotationStore is the storage surface per-app DEK rotation needs.

type DEKRotator

type DEKRotator struct {
	// contains filtered or unexported fields
}

DEKRotator rotates per-app data encryption keys: a fresh DEK per app, every value re-encrypted under it, committed in one transaction.

func NewDEKRotator

func NewDEKRotator(m *Manager, st DEKRotationStore, logger *slog.Logger) *DEKRotator

NewDEKRotator builds a rotator over m's active master key and st.

func (*DEKRotator) RotateAll

func (r *DEKRotator) RotateAll(ctx context.Context, actor string) (DEKRotateAllResult, error)

RotateAll rotates every service whose DEK predates the run. A run that was cut short (restart, cancelled request) is resumed, not restarted, so services already rotated by it are skipped. Finished runs are never reused.

func (*DEKRotator) RotateOne

func (r *DEKRotator) RotateOne(ctx context.Context, serviceName string) (DEKRotateResult, error)

RotateOne rotates serviceName's DEK. A wrong master key, a missing DEK or a row changed mid-run leaves every row exactly as it was.

func (*DEKRotator) Status

func (r *DEKRotator) Status(ctx context.Context) (DEKStatus, error)

Status lists every service's DEK age plus the latest rotate-all run.

type DEKStatus

type DEKStatus struct {
	Services []ServiceKeyStatus
	LastRun  *store.DEKRotationRun
}

DEKStatus is the current key lifecycle picture.

type KeyWrapper

type KeyWrapper interface {
	// Name identifies the backend inside the wrapped blob.
	Name() string
	Wrap(ctx context.Context, plaintext []byte) (string, error)
	Unwrap(ctx context.Context, ciphertext string) ([]byte, error)
}

KeyWrapper seals and opens the serialized master key with an external key service, so the key at rest is useless without that service.

type Manager

type Manager struct {
	// contains filtered or unexported fields
}

Manager combines a MasterKey with a Store to provide per-app envelope encryption end to end, binding every value to its (service, key) slot.

func NewManager

func NewManager(store Store, mk *MasterKey, opts ...ManagerOption) *Manager

NewManager builds a Manager around mk, the control plane's in-memory master key.

func (*Manager) BindingStatus added in v0.2.0

func (m *Manager) BindingStatus(ctx context.Context) (BindingStatus, error)

BindingStatus reports how many stored values still use the legacy unbound format, from a prefix count that never decrypts anything.

func (*Manager) DeleteAll

func (m *Manager) DeleteAll(ctx context.Context, serviceName string) error

DeleteAll permanently removes every value and the wrapped DEK for serviceName, so nothing set under it can ever be decrypted again.

func (*Manager) DeleteValue

func (m *Manager) DeleteValue(ctx context.Context, serviceName, envKey string) error

DeleteValue removes one stored value and keeps the namespace's DEK. Idempotent.

func (*Manager) DeriveKey

func (m *Manager) DeriveKey(purpose string) []byte

DeriveKey returns a 32 byte key bound to purpose, or nil when no master key is loaded. The master key itself never leaves this package.

func (*Manager) Exists

func (m *Manager) Exists(ctx context.Context, serviceName, envKey string) (bool, error)

Exists reports whether a value has been set for (serviceName, envKey), without decrypting it.

func (*Manager) ExistsForServices added in v0.2.0

func (m *Manager) ExistsForServices(ctx context.Context, serviceNames []string, envKey string) (map[string]bool, error)

ExistsForServices reports, for every name in serviceNames, whether a value has been set for (name, envKey), without decrypting it, via one batched store call instead of an Exists call per service.

func (*Manager) GetMasterKeyRotatedAt

func (m *Manager) GetMasterKeyRotatedAt(ctx context.Context) (time.Time, bool, error)

GetMasterKeyRotatedAt returns the last time RotateMasterKey succeeded, or ok=false if it never has.

func (*Manager) ListKeys

func (m *Manager) ListKeys(ctx context.Context, serviceName string) ([]store.SecretKeyInfo, error)

ListKeys returns every secret key set for serviceName with its locked state, never a value.

func (*Manager) Rebind added in v0.2.0

func (m *Manager) Rebind(ctx context.Context) (RebindResult, error)

Rebind re-encrypts every legacy value in place with its slot binding. Each row is swapped on its own, compare-and-swap, so an interrupted run keeps its progress and a rerun picks up where it stopped. Bound rows are verified and left alone, which makes it idempotent. A storage error aborts the run; a row that fails to decrypt is reported and skipped.

func (*Manager) RecoverPendingMasterKey

func (m *Manager) RecoverPendingMasterKey(ctx context.Context, path string, open func(string) (string, error)) (bool, error)

RecoverPendingMasterKey settles a rotation interrupted between the DB commit and the key file rename. open turns the file's raw content into a serialized key. It returns true when path.new was promoted.

func (*Manager) Resolve

func (m *Manager) Resolve(ctx context.Context, serviceName, envKey string) (string, error)

Resolve decrypts and returns the plaintext value for (serviceName, envKey), or ErrValueNotFound if none was ever set. A ciphertext bound to any other slot fails with ErrBindingMismatch. Callers must never persist what this returns.

func (*Manager) RotateMasterKey

func (m *Manager) RotateMasterKey(ctx context.Context, newMasterKey string) (time.Time, error)

RotateMasterKey re-wraps every stored DEK from the active master key to newMasterKey (a serialized age identity), then swaps the active key. On any failure nothing changes: the DB rotation is one transaction and the in-memory key is only swapped after it commits.

func (*Manager) SetLocked

func (m *Manager) SetLocked(ctx context.Context, serviceName, envKey string, locked bool) error

SetLocked toggles (serviceName, envKey)'s locked flag, reversible in either direction. Returns store.ErrSecretValueNotFound if no value has been set for that key yet.

func (*Manager) SetValue

func (m *Manager) SetValue(ctx context.Context, serviceName, envKey, plaintext string) error

SetValue encrypts plaintext under serviceName's DEK, generating one on first use, and persists only the ciphertext.

func (*Manager) SetValueGuarded

func (m *Manager) SetValueGuarded(ctx context.Context, serviceName, envKey, plaintext string, overwriteLocked bool) error

SetValueGuarded is SetValue that refuses, with ErrSecretLocked, to overwrite an existing locked value unless overwriteLocked is true.

type ManagerOption added in v0.2.0

type ManagerOption func(*Manager)

ManagerOption configures a Manager.

func WithLogger added in v0.2.0

func WithLogger(l *slog.Logger) ManagerOption

WithLogger sets the logger Manager uses for legacy-format notices.

func WithRequireBound added in v0.2.0

func WithRequireBound(require bool) ManagerOption

WithRequireBound makes Resolve reject legacy unbound ciphertexts.

type MasterKey

type MasterKey struct {
	// contains filtered or unexported fields
}

MasterKey is the root key this control plane holds. It is never used to encrypt secret values directly, only to wrap and unwrap per-app data encryption keys (see DEK in dek.go). Uses age's Hybrid key type: per the age library's own current documentation, "the standard age private/public key" as of this version, safe against future cryptographically-relevant quantum computers.

func GenerateMasterKey

func GenerateMasterKey() (*MasterKey, error)

GenerateMasterKey creates a new master key. Used for bootstrapping a fresh install and in tests. A real deployment persists the resulting String() output (see LoadMasterKey) rather than regenerating on every start, generating a new key would make every previously-wrapped DEK permanently unwrappable.

func LoadMasterKey

func LoadMasterKey(serialized string) (*MasterKey, error)

LoadMasterKey parses a master key from its serialized identity string (MasterKey.String()'s output). The master key can be sourced from a file, an env var, or an external KMS interface added later. This function only handles the parsing; sourcing the string itself (reading a file, reading an env var) is the caller's job, matching internal/brand.Load's own "load from a path the caller resolved" shape rather than this package reaching into the environment itself.

func (*MasterKey) GenerateDEK

func (mk *MasterKey) GenerateDEK() (raw []byte, wrapped WrappedDEK, err error)

GenerateDEK creates a new random data encryption key and wraps it under mk. The raw key is returned for immediate use, encrypting values right away, and must never be persisted itself, only the wrapped form returned alongside it.

func (*MasterKey) String

func (mk *MasterKey) String() string

String returns the master key's serialized identity, for persisting wherever LoadMasterKey will later read it back from. Treat the result as exactly as sensitive as any other secret: never log it, never write it anywhere other than the deliberately chosen key storage location.

func (*MasterKey) UnwrapDEK

func (mk *MasterKey) UnwrapDEK(wrapped WrappedDEK) ([]byte, error)

UnwrapDEK decrypts a WrappedDEK back to its raw form, for decrypting values that were encrypted under it.

type RebindFailure added in v0.2.0

type RebindFailure struct {
	Owner  string
	Key    string
	Reason string
}

RebindFailure names one slot Rebind could not bind, never its value.

type RebindResult added in v0.2.0

type RebindResult struct {
	Scanned      int
	Rebound      int
	AlreadyBound int
	// Changed counts rows rewritten by someone else mid-run; a concurrent
	// SetValue always writes the bound format, so nothing is lost.
	Changed     int
	FailedCount int
	Failed      []RebindFailure
	Remaining   int
}

RebindResult reports one Rebind run.

type ServiceKeyStatus

type ServiceKeyStatus struct {
	Service    string
	CreatedAt  time.Time
	RotatedAt  time.Time
	ValueCount int
	AgeDays    int
}

ServiceKeyStatus is one app's data key age, never any key bytes.

type Store

type Store interface {
	GetServiceDEK(ctx context.Context, serviceName string) ([]byte, error)
	SaveServiceDEK(ctx context.Context, serviceName string, wrappedDEK []byte) error
	GetSecretValue(ctx context.Context, serviceName, envKey string) ([]byte, error)
	SaveSecretValue(ctx context.Context, serviceName, envKey string, ciphertext []byte) error
	HasSecretValue(ctx context.Context, serviceName, envKey string) (bool, error)
	HasSecretValueForServices(ctx context.Context, serviceNames []string, envKey string) (map[string]bool, error)
	DeleteServiceSecrets(ctx context.Context, serviceName string) error
	DeleteSecretValue(ctx context.Context, serviceName, envKey string) (bool, error)
	ListSecretKeys(ctx context.Context, serviceName string) ([]store.SecretKeyInfo, error)
	GetSecretKeyLocked(ctx context.Context, serviceName, envKey string) (exists, locked bool, err error)
	SetSecretLocked(ctx context.Context, serviceName, envKey string, locked bool) error
	// RotateServiceDEKs rewraps every stored DEK in one transaction; any
	// rewrap error rolls the whole rotation back.
	RotateServiceDEKs(ctx context.Context, rewrap func(serviceName string, wrapped []byte) ([]byte, error)) error
	GetMasterKeyRotatedAt(ctx context.Context) (rotatedAt time.Time, ok bool, err error)
	CountSecretValuesByPrefix(ctx context.Context, prefix []byte) (total, withPrefix int, err error)
	ListSecretCiphertexts(ctx context.Context, afterService, afterKey string, limit int) ([]store.SecretCiphertext, error)
	// ReplaceSecretCiphertext swaps old for replacement only while the row
	// still holds old, so a concurrent SetValue is never clobbered.
	ReplaceSecretCiphertext(ctx context.Context, serviceName, envKey string, old, replacement []byte) (bool, error)
}

Store is the narrow surface Manager needs from internal/store, so tests can fake it without a real database.

type VaultTransit

type VaultTransit struct {
	Addr      string
	Token     string
	Key       string
	Mount     string
	Namespace string
	Client    *http.Client
}

VaultTransit wraps keys with a HashiCorp Vault or OpenBao Transit engine over its HTTP API, so no SDK dependency is needed.

func NewVaultTransit

func NewVaultTransit(addr, token, key, mount, namespace string) (*VaultTransit, error)

NewVaultTransit validates the config. Plain HTTP is allowed only to a loopback address.

func (*VaultTransit) Name

func (v *VaultTransit) Name() string

Name implements KeyWrapper.

func (*VaultTransit) Unwrap

func (v *VaultTransit) Unwrap(ctx context.Context, ciphertext string) ([]byte, error)

Unwrap implements KeyWrapper.

func (*VaultTransit) Wrap

func (v *VaultTransit) Wrap(ctx context.Context, plaintext []byte) (string, error)

Wrap implements KeyWrapper.

type WrappedDEK

type WrappedDEK []byte

WrappedDEK is a per-app data encryption key, encrypted under a MasterKey. Safe to persist at rest, useless without the matching MasterKey to unwrap it, which is the entire point of wrapping it rather than storing the raw key.

Jump to

Keyboard shortcuts

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