Documentation
¶
Overview ¶
Package secret provides the secret entity types and store interface.
Index ¶
- Constants
- type ListOpts
- type Meta
- type OnAccessFunc
- type OnMutateFunc
- type Secret
- type Service
- func (s *Service) BackfillVersionEncryption(ctx context.Context) (marked int, err error)
- func (s *Service) Delete(ctx context.Context, key, appID string) (err error)
- func (s *Service) Get(ctx context.Context, key, appID string) (sec *Secret, err error)
- func (s *Service) GetMeta(ctx context.Context, key, appID string) (*Meta, error)
- func (s *Service) GetVersion(ctx context.Context, key, appID string, version int64) (sec *Secret, err error)
- func (s *Service) List(ctx context.Context, appID string, opts ListOpts) ([]*Meta, error)
- func (s *Service) ListVersions(ctx context.Context, key, appID string) ([]*Version, error)
- func (s *Service) Set(ctx context.Context, key string, value []byte, appID string, opts ...SetOption) (_ *Meta, err error)
- type ServiceOption
- type SetOption
- type Store
- type Version
- type VersionEncryptionCounts
Constants ¶
const EncryptionAlgorithm = "AES-256-GCM"
EncryptionAlgorithm is the value recorded in Meta.EncryptionAlg for a secret stored encrypted. An unencrypted secret records "".
Variables ¶
This section is empty.
Functions ¶
This section is empty.
Types ¶
type ListOpts ¶
type ListOpts struct {
Limit int
Offset int
AppID string
ExpiresAfter *time.Time
ExpiresBefore *time.Time
// TenantID selects whose secrets Store.ListSecrets returns, by exact
// match: "" lists the app-level secrets only, and any other value lists
// that tenant's secrets only. It never means "every tenant".
//
// Service.List sets it from the tenant in the context and ignores
// whatever the caller put here, so a caller cannot list another
// tenant's secrets by naming it. CountSecretsMatching is app-wide and
// ignores it.
TenantID string
}
ListOpts configures list queries for secrets.
The expiry bounds are half-open so the two views of "expired" and "expiring" never overlap: ExpiresAfter is exclusive (expires_at > t) and ExpiresBefore is inclusive (expires_at <= t). Expired secrets are ExpiresBefore = now. The ones expiring within 30 days are ExpiresAfter = now, ExpiresBefore = now+30d. Setting either bound also excludes secrets with no expiry, and orders the result by expiry, then key.
func (ListOpts) HasExpiryBound ¶
HasExpiryBound reports whether either expiry bound is set.
type Meta ¶
type Meta struct {
ID id.ID `json:"id"`
Key string `json:"key"`
Version int64 `json:"version"`
// EncryptionAlg names the algorithm the stored value was encrypted with.
// Empty means the value is NOT encrypted: it was written while no
// encryption key was configured and is stored as given. A caller that
// displays secrets must treat empty as "not encrypted" rather than as
// "unknown", because it is the state every unconfigured Vault produces.
EncryptionAlg string `json:"encryption_alg,omitempty"`
ExpiresAt *time.Time `json:"expires_at,omitempty"`
AppID string `json:"app_id"`
// TenantID is the tenant the secret belongs to, empty for an app-level
// secret.
TenantID string `json:"tenant_id,omitempty"`
Metadata map[string]string `json:"metadata,omitempty"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
}
Meta is the public metadata for a secret (never includes the value).
type OnAccessFunc ¶
OnAccessFunc is called after a secret is accessed.
type OnMutateFunc ¶
OnMutateFunc is called after a secret is set or deleted.
type Secret ¶
type Secret struct {
core.Entity
ID id.ID `json:"id"`
Key string `json:"key"`
Value []byte `json:"-"` // decrypted value — never serialized
EncryptedValue []byte `json:"-"` // encrypted at rest
Version int64 `json:"version"`
EncryptionAlg string `json:"encryption_alg"`
EncryptionKeyID string `json:"encryption_key_id"`
ExpiresAt *time.Time `json:"expires_at,omitempty"`
AppID string `json:"app_id"`
// TenantID is the tenant the secret belongs to. Empty means an app-level
// secret, which is every secret written without a tenant in scope. A key
// is unique per app and tenant, so two tenants and the app itself can
// each hold a secret under the same key without seeing one another's.
TenantID string `json:"tenant_id,omitempty"`
Metadata map[string]string `json:"metadata,omitempty"`
}
Secret represents a stored secret with its encrypted value.
type Service ¶
type Service struct {
// contains filtered or unexported fields
}
Service provides secret CRUD with encryption, auto-versioning, and audit callbacks.
Every method that reads or writes one tenant's secrets takes the tenant from the context (scope.WithTenantID), never from an argument. A context with no tenant works on the app-level secrets, exactly as it did before secrets were scoped by tenant; a context with a tenant sees that tenant's secrets and nothing else, not even an app-level secret under the same key. The OnAccess and OnMutate callbacks get the same context, so the audit rows they write carry the tenant.
func NewService ¶
func NewService(store Store, encryptor *crypto.Encryptor, opts ...ServiceOption) *Service
NewService creates a secret service.
func (*Service) BackfillVersionEncryption ¶
BackfillVersionEncryption records EncryptionAlgorithm on every version row of the service's default app whose algorithm was never recorded and that the configured key decrypts, and returns how many it marked. It is maintenance, not a read: it covers every tenant of the app whatever the tenant in ctx, and it never returns a value.
It classifies by trying the key: a row the key cannot decrypt is plaintext, or sealed with some other key, and the backfill cannot tell which, so it leaves that row alone. With no key configured nothing can be proven, so it reads nothing and marks nothing. It pages by version id, so rows it leaves unrecorded are not fetched again, and a second run marks nothing new. If the store returns a full page that does not move past the cursor, it returns an error instead of asking again.
It runs under a "backfill version encryption" span that records how many rows it scanned and marked. With no key there is nothing to do and no span.
func (*Service) Delete ¶
Delete removes a secret and all its versions, for the tenant in ctx. The same key under another tenant, or at app level, is left alone.
func (*Service) GetMeta ¶
GetMeta retrieves secret metadata without the value, for the tenant in ctx.
func (*Service) GetVersion ¶
func (s *Service) GetVersion(ctx context.Context, key, appID string, version int64) (sec *Secret, err error)
GetVersion retrieves a specific version of a secret and decrypts it, for the tenant in ctx.
Each version row records the algorithm its own bytes were written with, and the store hands that back, so a version is read the way it was written whatever the secret's current algorithm or this service's key is now:
- A version written without a key reads back as its plaintext once a key is configured.
- A version written encrypted is refused with core.ErrDecryptionFailed when no key is configured. Its ciphertext is never returned as a value.
A row from before versions recorded an algorithm falls back to the secret's current one, which can be wrong across a change of key configuration. BackfillVersionEncryption classifies those rows it can decrypt.
func (*Service) List ¶
List returns secret metadata for an app, for the tenant in ctx only. It overwrites opts.TenantID with that tenant, so the caller's value has no effect.
func (*Service) ListVersions ¶
ListVersions returns all versions of a secret, for the tenant in ctx.
type ServiceOption ¶
type ServiceOption func(*Service)
ServiceOption configures the Service.
func WithAppID ¶
func WithAppID(appID string) ServiceOption
WithAppID sets the default app ID for the service.
func WithMetrics ¶
func WithMetrics(m gometrics.Metrics) ServiceOption
WithMetrics records the service's counters on m: secret operations by op and result, and the encrypt and decrypt calls it makes. In a Forge app pass app.Metrics(); vault's Forge extension does this for you. A nil m records nothing. No label ever carries a key, a tenant or a value.
func WithOnAccess ¶
func WithOnAccess(fn OnAccessFunc) ServiceOption
WithOnAccess registers a callback invoked after Get/GetVersion.
func WithOnMutate ¶
func WithOnMutate(fn OnMutateFunc) ServiceOption
WithOnMutate registers a callback invoked after Set/Delete.
type SetOption ¶
type SetOption func(*setConfig)
SetOption configures a Set operation.
func WithExpiresAt ¶
WithExpiresAt sets an expiration time on the secret.
func WithMetadata ¶
WithMetadata sets metadata on the secret.
type Store ¶
type Store interface {
// GetSecret retrieves the latest version of the secret with this key in
// this app and tenant.
GetSecret(ctx context.Context, key, appID, tenantID string) (*Secret, error)
// SetSecret creates or updates a secret. The secret's own AppID and
// TenantID say whose it is. If the key already exists for that app and
// tenant, a new version is created; the same key under another tenant
// is a different secret.
SetSecret(ctx context.Context, s *Secret) error
// DeleteSecret removes the secret with this key in this app and tenant,
// and all its versions. Other tenants' secrets under the same key stay.
DeleteSecret(ctx context.Context, key, appID, tenantID string) error
// ListSecrets returns secret metadata (never values) for an app, limited
// to the tenant in opts.TenantID by exact match ("" is the app-level
// secrets only). Without an expiry bound in opts the result is ordered
// by key. With one, only secrets that expire within the bounds are
// returned, ordered by expiry and then key.
ListSecrets(ctx context.Context, appID string, opts ListOpts) ([]*Meta, error)
// GetSecretVersion retrieves a specific version of the secret with this
// key in this app and tenant. The returned secret's EncryptionAlg is the
// version's own recorded algorithm when it has one, and the secret's
// current algorithm otherwise (a row written before versions recorded
// theirs).
GetSecretVersion(ctx context.Context, key, appID, tenantID string, version int64) (*Secret, error)
// ListSecretVersions returns all versions of the secret with this key in
// this app and tenant.
ListSecretVersions(ctx context.Context, key, appID, tenantID string) ([]*Version, error)
// CountSecrets returns the total number of secrets for an app, across
// every tenant and the app-level ones, independent of any paging.
CountSecrets(ctx context.Context, appID string) (int64, error)
// CountSecretsMatching returns the number of secrets for an app, across
// every tenant and the app-level ones, that pass the expiry bounds in
// opts, the same bounds ListSecrets applies. TenantID, Limit and Offset
// are ignored.
CountSecretsMatching(ctx context.Context, appID string, opts ListOpts) (int64, error)
// CountSecretsUnencrypted returns the number of secrets for an app,
// across every tenant and the app-level ones, whose stored value carries
// no encryption algorithm, that is, rows written while no encryption key
// was configured. It counts in the store and never loads a value.
CountSecretsUnencrypted(ctx context.Context, appID string) (int64, error)
// ListUnrecordedVersions returns up to limit version rows of an app,
// across every tenant and the app-level ones, whose encryption algorithm
// was never recorded, in ascending version id order, starting after the
// id after (empty means from the start). Callers page by passing the
// last id of the previous page, so rows they decline to classify are
// not fetched again.
ListUnrecordedVersions(ctx context.Context, appID, after string, limit int) ([]*Version, error)
// SetVersionEncryption records the algorithm of one version row,
// whichever tenant it belongs to: version ids are unique across the
// store. An empty alg records "stored without encryption". An unknown
// id returns nil: the row was already set or has been removed.
SetVersionEncryption(ctx context.Context, versionID id.ID, alg string) error
// CountVersionEncryption tallies an app's earlier version rows, across
// every tenant and the app-level ones, by recorded algorithm, without
// loading any value. A secret's current version row is not counted:
// SetSecret writes one for the current value too, and
// CountSecretsUnencrypted already covers current values, so a row
// counted here is history only. A version row is matched to its current
// secret by key, app and tenant together.
CountVersionEncryption(ctx context.Context, appID string) (VersionEncryptionCounts, error)
}
Store defines the persistence interface for secrets.
A secret is identified by its key, app ID and tenant ID together. An empty tenant ID is the app-level secret, not a wildcard: every tenant-scoped method below matches the tenant exactly, so a read under one tenant can never return, overwrite or delete a secret of another tenant or of the app.
The operator-level methods (CountSecrets, CountSecretsMatching, CountSecretsUnencrypted, CountVersionEncryption, ListUnrecordedVersions and SetVersionEncryption) take no tenant and work across every tenant of the app. They serve statistics and maintenance, and none of them returns a secret value: ListUnrecordedVersions hands back stored bytes only so the backfill can test them against the key.
type Version ¶
type Version struct {
ID id.ID `json:"id"`
SecretKey string `json:"secret_key"`
AppID string `json:"app_id"`
// TenantID is the tenant of the secret this version belongs to, empty
// for an app-level secret.
TenantID string `json:"tenant_id,omitempty"`
Version int64 `json:"version"`
EncryptedValue []byte `json:"-"`
// EncryptionAlg is the algorithm this version's bytes were written with.
// nil means the row predates the column and nobody has classified it yet:
// readers fall back to the secret's current algorithm. A non-nil empty
// string means the version was recorded as stored without encryption.
EncryptionAlg *string `json:"-"`
CreatedBy string `json:"created_by"`
CreatedAt time.Time `json:"created_at"`
}
Version represents a historical version of a secret.
type VersionEncryptionCounts ¶
type VersionEncryptionCounts struct {
// Plaintext is the number of versions recorded as stored without
// encryption (algorithm recorded as empty).
Plaintext int64
// Unrecorded is the number of versions whose algorithm was never recorded.
Unrecorded int64
}
VersionEncryptionCounts tallies the earlier version rows of one app (every version row except each secret's current one) by what is known about their encryption. The tally covers every tenant of the app.