Documentation
¶
Overview ¶
Package rotator rotates the high-value secrets in tmi-secrets one idempotent phase at a time, recording progress in Secret annotations (#965).
Index ¶
- Constants
- Variables
- func AgeDays(s *Secret, name string, now time.Time) int
- func IsDue(s *Secret, name string, now time.Time) bool
- func NewHexKey() (string, error)
- func NewPassword() (string, error)
- func ParseRotateEvery(v string) (time.Duration, error)
- func RedisPasswordFromSecret(store SecretStore, secretName string) func(context.Context) (string, error)
- func RegionFromSecretARN(arn string) (string, error)
- func Run(ctx context.Context, env *Env, rotations []Rotation, force string) error
- type Env
- type Escrow
- type FakeRolloutWaiter
- type GoRedisACL
- type GormSettingsStore
- type Keyring
- type KubeRolloutWaiter
- type KubeSecretStore
- type MemorySecretStore
- type NoopEscrow
- type RedisACL
- type RedisPasswordRotation
- type RolloutWaiter
- type Rotation
- type Secret
- type SecretStore
- type SecretsManagerEscrow
- type SecretsManagerPutter
- type SettingsKeyRotation
- type SettingsStore
Constants ¶
const ( // #nosec G101 -- Secret data key name, not a credential RedisPasswordKey = "TMI_REDIS_PASSWORD" AnnRetire = "tmi.dev/rotation-retire." )
const ( AnnRotatedAt = "tmi.dev/rotated-at." AnnEvery = "tmi.dev/rotate-every." AnnPhase = "tmi.dev/rotation-phase." AnnGeneration = "tmi.dev/rotation-generation." DefaultRotateEvery = 90 * 24 * time.Hour )
Annotation keys (suffix = Rotation.Name()).
const ( SettingsKeyKey = "TMI_SECRET_SETTINGS_ENCRYPTION_KEY" SettingsKeyIDKey = "TMI_SECRET_SETTINGS_ENCRYPTION_CONTEXT_ID" SettingsPrevKeyKey = "TMI_SECRET_SETTINGS_ENCRYPTION_PREVIOUS_KEY" SettingsPrevKeyIDKey = "TMI_SECRET_SETTINGS_ENCRYPTION_PREVIOUS_CONTEXT_ID" AnnPromotedAt = "tmi.dev/rotation-promoted-at." )
Variables ¶
var ErrConflict = errors.New("secret changed since it was read")
ErrConflict is returned by SecretStore.Update when the Secret changed after it was read.
Functions ¶
func AgeDays ¶
AgeDays returns whole days since the last rotation, or -1 if never rotated. SEM@3b682947: compute a secret's age in days since last rotation (pure)
func IsDue ¶
IsDue reports whether the named rotation should start now: never rotated, an unreadable rotated-at, or older than rotate-every (default 90d; an unparsable interval is logged and falls back to the default, never to "now"). SEM@3b682947: determine whether a secret's rotation is due (pure)
func NewHexKey ¶
NewHexKey returns 32 random bytes as 64 hex characters (AES-256 key). SEM@3b682947: generate a random 256-bit key as hex (reads random source)
func NewPassword ¶
NewPassword returns a 32-character alphanumeric password (URL- and shell-safe, so it needs no escaping in TMI_DATABASE_URL or redis-cli). int(b)%62 biases the first 8 letters by 4/256; acceptable for ~190 bits and simpler than rejection sampling. SEM@3b682947: generate a random password (reads random source)
func ParseRotateEvery ¶
ParseRotateEvery accepts "<n>d" or any Go duration; zero or negative is an error. SEM@3b682947: parse a rotation interval like 90d or a duration (pure)
func RedisPasswordFromSecret ¶
func RedisPasswordFromSecret(store SecretStore, secretName string) func(context.Context) (string, error)
RedisPasswordFromSecret returns a go-redis credentials func that reads the current password from the Secret on every (re)connect, so a Redis restart mid-rotation reconnects with whatever the Secret holds now. Redis accepts the Secret's password in every phase: OLD before the swap, then OLD+NEW (and finally only NEW) once the Secret holds NEW. SEM@32eda6c88eee02283fa8feb03bb2d42252451618: build a func that reads the current Redis password from the secret on each call (reads secret)
func RegionFromSecretARN ¶
RegionFromSecretARN returns the region field of a Secrets Manager secret ARN. SEM@070c69a19a7fed18f17f2bd3475d508172778494: parse the AWS region out of a Secrets Manager secret ARN (pure)
Types ¶
type Env ¶
type Env struct {
Secrets SecretStore
Rollouts RolloutWaiter
SecretName string // "tmi-secrets"
ServerDeployment string // "tmi-server"
RolloutTimeout time.Duration // 10m
Now func() time.Time
}
Env is everything a Rotation needs from the cluster. SEM@3b682947: hold the dependencies and settings shared by secret rotations
func (*Env) Transition ¶
func (e *Env) Transition(ctx context.Context, name, fromPhase, nextPhase string, mutate func(s *Secret)) error
Transition reads the Secret, applies mutate, records the phase (or, for nextPhase == "", clears it and stamps rotated-at) together with the server Deployment's generation before the write, and writes everything in one Update. It is a compare-and-swap on the phase: if the recorded phase (missing == "") is not fromPhase, another run advanced it, so nothing is written and the error wraps ErrConflict. A stale resourceVersion conflict is likewise not retried. SEM@3b682947: advance a rotation phase atomically in the secret, rejecting concurrent runs
type Escrow ¶
Escrow stores an off-cluster copy of key material before a rotation makes it the only live copy (#1009). Implementations must never echo the payload in errors or logs. SEM@070c69a19a7fed18f17f2bd3475d508172778494: store an off-cluster copy of rotated key material
type FakeRolloutWaiter ¶
type FakeRolloutWaiter struct {
Waits int
// contains filtered or unexported fields
}
FakeRolloutWaiter reports a generation equal to the store's data-write count, so every data write "rolls" the server immediately. SEM@3b682947: fake rollout waiter driven by an in-memory secret store
func NewFakeRolloutWaiter ¶
func NewFakeRolloutWaiter(store *MemorySecretStore, deployment string) *FakeRolloutWaiter
NewFakeRolloutWaiter binds a fake waiter to a MemorySecretStore. SEM@3b682947: build a fake rollout waiter
func (*FakeRolloutWaiter) Generation ¶
Generation reports the fake Deployment generation. SEM@3b682947: return the in-memory store's generation as write count (pure)
func (*FakeRolloutWaiter) WaitRolled ¶
func (f *FakeRolloutWaiter) WaitRolled(ctx context.Context, deployment string, since int64, _ time.Duration) error
WaitRolled succeeds when a data write happened after since. SEM@3b682947: succeed if the store has been written since the given generation
type GoRedisACL ¶
type GoRedisACL struct {
// contains filtered or unexported fields
}
GoRedisACL implements RedisACL with ACL SETUSER. SEM@3b682947: Redis ACL manager backed by a go-redis client
func NewGoRedisACL ¶
func NewGoRedisACL(client *redis.Client) *GoRedisACL
SEM@3b682947: build a go-redis ACL manager
func (*GoRedisACL) AddPassword ¶
func (a *GoRedisACL) AddPassword(ctx context.Context, pw string) error
SEM@3b682947: add a password to the Redis default user (writes Redis)
func (*GoRedisACL) SetOnlyPassword ¶
func (a *GoRedisACL) SetOnlyPassword(ctx context.Context, pw string) error
SetOnlyPassword makes pw the default user's only password in one atomic ACL SETUSER (resetpass clears the password list; flags and permissions stay). Idempotent: unlike "!hash", it does not fail when an old password is already gone. SEM@3b682947: set the sole password of the Redis default user (writes Redis)
type GormSettingsStore ¶
type GormSettingsStore struct {
// contains filtered or unexported fields
}
GormSettingsStore runs the real SettingsService against the database. SEM@3b682947: settings store backed by the settings service over GORM (writes DB)
func NewGormSettingsStore ¶
func NewGormSettingsStore(gormDB *gorm.DB, redis *db.RedisDB) *GormSettingsStore
SEM@3b682947: build a GORM-backed settings store (pure)
func (*GormSettingsStore) CountWithID ¶
SEM@3b682947: count settings rows still under a key id (reads DB)
type Keyring ¶
Keyring is the settings-encryption key material held in tmi-secrets. SEM@3b682947: current and previous settings encryption keys with their ids (pure)
func KeyringFromSecret ¶
KeyringFromSecret reads and validates the keyring; a missing context id is 1 (the id every pre-rotation value carries). SEM@3b682947: parse and validate the settings keyring from secret data (pure)
type KubeRolloutWaiter ¶
KubeRolloutWaiter polls a Deployment until Reloader's rollout completes. SEM@3b682947: hold a Kubernetes client for waiting on deployment rollouts
func NewKubeRolloutWaiter ¶
func NewKubeRolloutWaiter(cs kubernetes.Interface, namespace string) *KubeRolloutWaiter
NewKubeRolloutWaiter builds a waiter polling every 5s. SEM@3b682947: build a Kubernetes rollout waiter with default poll interval
func (*KubeRolloutWaiter) Generation ¶
Generation reads a Deployment's metadata.generation. SEM@3b682947: fetch a deployment's current generation from Kubernetes (reads cluster)
func (*KubeRolloutWaiter) WaitRolled ¶
func (w *KubeRolloutWaiter) WaitRolled(ctx context.Context, deployment string, since int64, timeout time.Duration) error
WaitRolled first waits for the generation to pass since (Reloader has patched the pod template), then for the rollout to complete. A Recreate Deployment dips to zero available replicas in between; that is progress, not failure. SEM@3b682947: wait until a deployment rollout completes after a generation or time out (reads cluster)
type KubeSecretStore ¶
type KubeSecretStore struct {
// contains filtered or unexported fields
}
KubeSecretStore is the SecretStore for a real cluster. SEM@3b682947: hold a Kubernetes client and namespace for reading and writing secrets
func NewKubeSecretStore ¶
func NewKubeSecretStore(cs kubernetes.Interface, namespace string) *KubeSecretStore
NewKubeSecretStore builds a KubeSecretStore for one namespace. SEM@3b682947: build a Kubernetes-backed secret store
type MemorySecretStore ¶
type MemorySecretStore struct {
DataWrites int
// contains filtered or unexported fields
}
MemorySecretStore is an in-memory SecretStore for tests. It bumps resourceVersion on every Update and counts data writes so a FakeRolloutWaiter can imitate Reloader (which rolls on data changes only). SEM@3b682947: in-memory secret store with optimistic concurrency for tests
func NewMemorySecretStore ¶
func NewMemorySecretStore(seed ...*Secret) *MemorySecretStore
NewMemorySecretStore builds a store seeded with the given Secrets. SEM@3b682947: build an in-memory secret store seeded with secrets
type NoopEscrow ¶
type NoopEscrow struct{}
NoopEscrow is used when no escrow is configured (dev clusters). SEM@070c69a19a7fed18f17f2bd3475d508172778494: discard escrow writes when no escrow target is configured (pure)
type RedisACL ¶
type RedisACL interface {
AddPassword(ctx context.Context, pw string) error
SetOnlyPassword(ctx context.Context, pw string) error
}
RedisACL manages the passwords accepted for the default Redis user. Returned errors must never contain a password (Redis echoes ACL modifiers). SEM@3b682947: define Redis ACL operations for adding a password and keeping only one
type RedisPasswordRotation ¶
type RedisPasswordRotation struct {
// contains filtered or unexported fields
}
RedisPasswordRotation rotates TMI_REDIS_PASSWORD without a Redis restart. SEM@3b682947: rotation of the Redis password
func NewRedisPasswordRotation ¶
func NewRedisPasswordRotation(acl RedisACL) *RedisPasswordRotation
SEM@3b682947: build a Redis password rotation
func (*RedisPasswordRotation) Name ¶
func (r *RedisPasswordRotation) Name() string
SEM@3b682947: return the rotation's name (pure)
type RolloutWaiter ¶
type RolloutWaiter interface {
Generation(ctx context.Context, deployment string) (int64, error)
// WaitRolled returns once the Deployment's generation exceeds since and its rollout is complete.
WaitRolled(ctx context.Context, deployment string, since int64, timeout time.Duration) error
}
RolloutWaiter observes Deployment rollouts triggered by Secret changes (Reloader). SEM@3b682947: define waiting for deployment rollouts
type Rotation ¶
type Rotation interface {
Name() string
// Run resumes from the phase recorded on the Secret and returns when the
// rotation is complete or parked (waiting on a later run). Idempotent.
// Returned errors must never contain secret values: implementations wrap
// credential-bearing errors (e.g. Redis echoing an ACL password) with fixed messages.
Run(ctx context.Context, env *Env) error
}
Rotation is one secret's phased, resumable rotation. SEM@3b682947: define a resumable, idempotent secret rotation
type Secret ¶
type Secret struct {
Name string
Data map[string]string
Annotations map[string]string
ResourceVersion string
}
Secret is a snapshot of one Kubernetes Secret with decoded data. SEM@3b682947: in-memory representation of a secret with data, annotations and version
type SecretStore ¶
type SecretStore interface {
Get(ctx context.Context, name string) (*Secret, error)
// Update replaces data and annotations; returns ErrConflict when s.ResourceVersion is stale.
Update(ctx context.Context, s *Secret) error
}
SecretStore reads and writes whole Secrets with optimistic concurrency. SEM@3b682947: define storing and fetching secrets with optimistic concurrency
type SecretsManagerEscrow ¶
type SecretsManagerEscrow struct {
// contains filtered or unexported fields
}
SecretsManagerEscrow writes each payload as a new version of one secret. SEM@070c69a19a7fed18f17f2bd3475d508172778494: escrow key material as a new Secrets Manager secret version
func NewSecretsManagerEscrow ¶
func NewSecretsManagerEscrow(client SecretsManagerPutter, secretARN string) *SecretsManagerEscrow
SEM@070c69a19a7fed18f17f2bd3475d508172778494: build a Secrets Manager escrow for one secret (pure)
type SecretsManagerPutter ¶
type SecretsManagerPutter interface {
PutSecretValue(ctx context.Context, in *secretsmanager.PutSecretValueInput, optFns ...func(*secretsmanager.Options)) (*secretsmanager.PutSecretValueOutput, error)
}
SecretsManagerPutter is the slice of the Secrets Manager client the escrow uses. SEM@070c69a19a7fed18f17f2bd3475d508172778494: write a new secret version to Secrets Manager
type SettingsKeyRotation ¶
type SettingsKeyRotation struct {
// contains filtered or unexported fields
}
SettingsKeyRotation rotates the settings-encryption key: stage, promote, re-encrypt, and (after previousGrace, once nothing references the old id) drop. SEM@7f1038af: phased settings encryption key rotation with injected escrow and deferred previous-key drop
func NewSettingsKeyRotation ¶
func NewSettingsKeyRotation(store SettingsStore, previousGrace time.Duration, escrow Escrow) *SettingsKeyRotation
SEM@070c69a19a7fed18f17f2bd3475d508172778494: build a settings key rotation with escrow, defaulting to a no-op (pure)
func (*SettingsKeyRotation) Name ¶
func (r *SettingsKeyRotation) Name() string
SEM@3b682947: return the rotation's name (pure)
type SettingsStore ¶
type SettingsStore interface {
ReEncrypt(ctx context.Context, kr Keyring) (int, error)
CountWithID(ctx context.Context, kr Keyring, id int) (int64, error)
}
SettingsStore is the database side of the settings-key rotation. SEM@3b682947: define re-encrypting settings under a keyring and counting rows per key id