Keysmith

Key Rotation

Zero-downtime API key rotation with configurable grace periods.

The rotation subsystem enables zero-downtime API key rotation. When a key is rotated, both the old and new keys validate during a configurable grace period, giving clients time to migrate.

Rotating a key

import "github.com/xraph/keysmith/rotation"

newResult, err := eng.RotateKey(ctx,
    keyID,
    rotation.ReasonScheduled,
    24*time.Hour, // grace period
)
if err != nil {
    log.Fatal(err)
}

fmt.Println("New Key:", newResult.RawKey) // save this — shown once

Rotation reasons

ReasonConstantWhen to use
scheduledrotation.ReasonScheduledRegular rotation schedule
compromisedrotation.ReasonCompromisedKey may have been leaked
expiringrotation.ReasonExpiringKey approaching expiration
manualrotation.ReasonManualManual rotation by admin

Grace period behavior

During the grace period:

  1. The new key validates normally (state: active)
  2. The old key still validates (state: rotated, within grace window)
  3. After grace expiry, the old key stops validating
Time ─────────────────────────────────────────────►
     │ Rotation    │ Grace period      │ Grace expired
     │             │                   │
Old: │ active      │ rotated (valid)   │ rotated (invalid)
New: │ active      │ active            │ active

Viewing rotation history

records, err := eng.ListRotations(ctx, keyID, &rotation.ListFilter{
    Limit: 10,
})

for _, rec := range records {
    fmt.Printf("Rotated at %s, reason: %s, grace: %s\n",
        rec.CreatedAt, rec.Reason, rec.GraceTTL)
}

Rotation record fields

FieldTypeDescription
IDid.RotationIDUnique record identifier
KeyIDid.KeyIDRotated key
OldHashstringPrevious key hash
NewHashstringNew key hash
ReasonReasonWhy the rotation happened
GraceTTLtime.DurationGrace period duration
GraceExpirytime.TimeWhen the grace period ends

Rotation store interface

type Store interface {
    Create(ctx context.Context, r *Record) error
    GetByID(ctx context.Context, id id.RotationID) (*Record, error)
    ListByKeyID(ctx context.Context, keyID id.KeyID, filter *ListFilter) ([]*Record, error)
    GetActiveGrace(ctx context.Context, oldHash string) (*Record, error)
    DeleteByKeyID(ctx context.Context, keyID id.KeyID) error
}

On this page