Documentation
¶
Overview ¶
Package rotation provides secret rotation policy entities and store interface.
Index ¶
- type ListOpts
- type Manager
- type ManagerOption
- func WithAppID(appID string) ManagerOption
- func WithCheckInterval(d time.Duration) ManagerOption
- func WithClaimLease(d time.Duration) ManagerOption
- func WithLogger(l log.Logger) ManagerOption
- func WithMetrics(m gometrics.Metrics) ManagerOption
- func WithOnRotate(fn func(ctx context.Context, key, appID string, err error)) ManagerOption
- type Policy
- type Record
- type Rotator
- type Store
Constants ¶
This section is empty.
Variables ¶
This section is empty.
Functions ¶
This section is empty.
Types ¶
type Manager ¶
type Manager struct {
// contains filtered or unexported fields
}
Manager handles scheduled secret rotation with registered rotator functions.
func NewManager ¶
func NewManager(store Store, secretSvc *secret.Service, opts ...ManagerOption) *Manager
NewManager creates a rotation manager.
func (*Manager) RegisterRotator ¶
RegisterRotator registers a rotator function for the given secret key.
If r fails, its error text is stored verbatim in the audit log's metadata and shown to dashboard viewers, so r must never put a secret value in its error.
func (*Manager) RotateNow ¶
RotateNow performs an immediate rotation for the given secret key. When a WithOnRotate callback is set it hears the outcome, success or failure.
Rotation works on app-level secrets. A policy and a rotation record carry no tenant, so RotateNow reads and writes the app-level secret under secretKey whatever tenant ctx carries, and hands the callback a context scoped the same way. Rotating one tenant's secret here would move the app-level policy's schedule for a secret it never touched. The other scope values in ctx (app, user, IP) are kept.
func (*Manager) RotatorKeys ¶
RotatorKeys returns the secret keys that have a registered rotator, sorted.
RotateNow fails for any key not in this list, because a rotator is Go code an application registers and nothing outside the process can supply one. A caller that offers rotation as an action should gate on this rather than offering a button that always fails.
func (*Manager) Start ¶
Start begins the background rotation check loop. The loop runs until Stop is called or the context is cancelled. It keeps ctx's values and cancellation but not its span, so the policy list it reads on every tick never lands in the trace of whoever called Start.
Start is idempotent: calling it while a loop is already running is a no-op, because starting a second loop would overwrite the cancel func and done channel that reach the first one, orphaning its goroutine with nothing left able to cancel it. Call Stop first to restart the loop.
type ManagerOption ¶
type ManagerOption func(*Manager)
ManagerOption configures the Manager.
func WithAppID ¶
func WithAppID(appID string) ManagerOption
WithAppID sets the default app ID for the manager.
func WithCheckInterval ¶
func WithCheckInterval(d time.Duration) ManagerOption
WithCheckInterval sets how often the manager checks for due rotations.
func WithClaimLease ¶
func WithClaimLease(d time.Duration) ManagerOption
WithClaimLease sets how long the scheduled loop's claim on a due policy lasts. Before rotating a due policy the loop claims it by moving its next rotation time to now plus the lease, so another replica running the same loop finds it not due and leaves it alone. A d of zero or less keeps the default of five minutes.
A successful rotation moves the next rotation time on to now plus the policy's interval, which replaces the lease. A failed rotation leaves the lease in place, so the next attempt comes once the lease runs out rather than on every tick. A rotation must finish inside the lease: once it runs out, another replica may claim the same policy and rotate it again.
Manual rotation through RotateNow does not claim.
func WithLogger ¶
func WithLogger(l log.Logger) ManagerOption
WithLogger sets the logger for the manager.
func WithMetrics ¶
func WithMetrics(m gometrics.Metrics) ManagerOption
WithMetrics records rotation attempts and their duration on m, by trigger (scheduled or manual) and result. 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 secret key.
func WithOnRotate ¶
func WithOnRotate(fn func(ctx context.Context, key, appID string, err error)) ManagerOption
WithOnRotate registers a callback invoked once for every RotateNow attempt, whether it came from the scheduled loop or a manual call. err is nil after a successful rotation: it is nil once the new value is stored, and a failed write of the rotation record is logged, not reported. Any other outcome (no rotator, a failed read, a rotator error, a failed store of the new value) passes the error RotateNow returns. The callback runs under the context RotateNow was given, so a caller's scope reaches it.
The vault's own callback stores the error text verbatim in the audit log, where dashboard viewers read it, so a Rotator must never put a secret value in its error.
type Policy ¶
type Policy struct {
core.Entity
ID id.ID `json:"id"`
SecretKey string `json:"secret_key"`
AppID string `json:"app_id"`
Interval time.Duration `json:"interval"`
Enabled bool `json:"enabled"`
LastRotatedAt *time.Time `json:"last_rotated_at,omitempty"`
NextRotationAt *time.Time `json:"next_rotation_at,omitempty"`
}
Policy represents a secret rotation policy.
type Record ¶
type Record struct {
ID id.ID `json:"id"`
SecretKey string `json:"secret_key"`
AppID string `json:"app_id"`
OldVersion int64 `json:"old_version"`
NewVersion int64 `json:"new_version"`
RotatedBy string `json:"rotated_by"`
RotatedAt time.Time `json:"rotated_at"`
}
Record records a completed rotation event.
type Rotator ¶
Rotator produces a new secret value from the current one.
The error a Rotator returns is stored verbatim in the audit log's metadata and shown to dashboard viewers, so a Rotator must never put a secret value in its error text: not the current value, not the new one, not a credential it used along the way. Describe what failed, not what it held.
type Store ¶
type Store interface {
// SaveRotationPolicy creates or updates a rotation policy.
SaveRotationPolicy(ctx context.Context, p *Policy) error
// GetRotationPolicy retrieves a rotation policy by secret key and app ID.
GetRotationPolicy(ctx context.Context, key, appID string) (*Policy, error)
// ListRotationPolicies returns all rotation policies for an app.
ListRotationPolicies(ctx context.Context, appID string) ([]*Policy, error)
// DeleteRotationPolicy removes a rotation policy.
DeleteRotationPolicy(ctx context.Context, key, appID string) error
// RecordRotation records a completed rotation event.
RecordRotation(ctx context.Context, r *Record) error
// ListRotationRecords returns rotation history for a secret.
ListRotationRecords(ctx context.Context, key, appID string, opts ListOpts) ([]*Record, error)
// CountRotationPolicies returns the total number of rotation policies for
// an app, independent of any paging.
CountRotationPolicies(ctx context.Context, appID string) (int64, error)
// ClaimDueRotation atomically claims a due policy for one rotation run.
// If the policy for key and appID exists, is enabled, and its
// NextRotationAt is set and before now, it sets NextRotationAt to until
// and returns true. Otherwise it changes nothing and returns false.
// Exactly one of several concurrent callers can win a given due time.
//
// "Before now" is the scheduled loop's own test, now.After(next), so a
// policy due at exactly now is not yet due. A missing policy is not an
// error: it returns false and a nil error, like any other policy that is
// not due. Implementations compare and store now and until in UTC.
ClaimDueRotation(ctx context.Context, key, appID string, now, until time.Time) (bool, error)
}
Store defines the persistence interface for rotation policies.