mirror of
https://github.com/coder/coder.git
synced 2026-09-24 15:04:27 +08:00
feat: enable key rotation (#15066)
This PR contains the remaining logic necessary to hook up key rotation to the product.
This commit is contained in:
+61
-31
@@ -3,6 +3,7 @@ package cryptokeys
|
||||
import (
|
||||
"context"
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"io"
|
||||
"strconv"
|
||||
"sync"
|
||||
@@ -12,7 +13,7 @@ import (
|
||||
|
||||
"cdr.dev/slog"
|
||||
"github.com/coder/coder/v2/coderd/database"
|
||||
"github.com/coder/coder/v2/coderd/database/db2sdk"
|
||||
"github.com/coder/coder/v2/coderd/database/dbauthz"
|
||||
"github.com/coder/coder/v2/codersdk"
|
||||
"github.com/coder/quartz"
|
||||
)
|
||||
@@ -25,7 +26,7 @@ var (
|
||||
)
|
||||
|
||||
type Fetcher interface {
|
||||
Fetch(ctx context.Context) ([]codersdk.CryptoKey, error)
|
||||
Fetch(ctx context.Context, feature codersdk.CryptoKeyFeature) ([]codersdk.CryptoKey, error)
|
||||
}
|
||||
|
||||
type EncryptionKeycache interface {
|
||||
@@ -62,27 +63,26 @@ const (
|
||||
)
|
||||
|
||||
type DBFetcher struct {
|
||||
DB database.Store
|
||||
Feature database.CryptoKeyFeature
|
||||
DB database.Store
|
||||
}
|
||||
|
||||
func (d *DBFetcher) Fetch(ctx context.Context) ([]codersdk.CryptoKey, error) {
|
||||
keys, err := d.DB.GetCryptoKeysByFeature(ctx, d.Feature)
|
||||
func (d *DBFetcher) Fetch(ctx context.Context, feature codersdk.CryptoKeyFeature) ([]codersdk.CryptoKey, error) {
|
||||
keys, err := d.DB.GetCryptoKeysByFeature(ctx, database.CryptoKeyFeature(feature))
|
||||
if err != nil {
|
||||
return nil, xerrors.Errorf("get crypto keys by feature: %w", err)
|
||||
}
|
||||
|
||||
return db2sdk.CryptoKeys(keys), nil
|
||||
return toSDKKeys(keys), nil
|
||||
}
|
||||
|
||||
// cache implements the caching functionality for both signing and encryption keys.
|
||||
type cache struct {
|
||||
clock quartz.Clock
|
||||
refreshCtx context.Context
|
||||
refreshCancel context.CancelFunc
|
||||
fetcher Fetcher
|
||||
logger slog.Logger
|
||||
feature codersdk.CryptoKeyFeature
|
||||
ctx context.Context
|
||||
cancel context.CancelFunc
|
||||
clock quartz.Clock
|
||||
fetcher Fetcher
|
||||
logger slog.Logger
|
||||
feature codersdk.CryptoKeyFeature
|
||||
|
||||
mu sync.Mutex
|
||||
keys map[int32]codersdk.CryptoKey
|
||||
@@ -109,7 +109,8 @@ func NewSigningCache(ctx context.Context, logger slog.Logger, fetcher Fetcher,
|
||||
if !isSigningKeyFeature(feature) {
|
||||
return nil, xerrors.Errorf("invalid feature: %s", feature)
|
||||
}
|
||||
return newCache(ctx, logger, fetcher, feature, opts...)
|
||||
logger = logger.Named(fmt.Sprintf("%s_signing_keycache", feature))
|
||||
return newCache(ctx, logger, fetcher, feature, opts...), nil
|
||||
}
|
||||
|
||||
func NewEncryptionCache(ctx context.Context, logger slog.Logger, fetcher Fetcher,
|
||||
@@ -118,10 +119,11 @@ func NewEncryptionCache(ctx context.Context, logger slog.Logger, fetcher Fetcher
|
||||
if !isEncryptionKeyFeature(feature) {
|
||||
return nil, xerrors.Errorf("invalid feature: %s", feature)
|
||||
}
|
||||
return newCache(ctx, logger, fetcher, feature, opts...)
|
||||
logger = logger.Named(fmt.Sprintf("%s_encryption_keycache", feature))
|
||||
return newCache(ctx, logger, fetcher, feature, opts...), nil
|
||||
}
|
||||
|
||||
func newCache(ctx context.Context, logger slog.Logger, fetcher Fetcher, feature codersdk.CryptoKeyFeature, opts ...func(*cache)) (*cache, error) {
|
||||
func newCache(ctx context.Context, logger slog.Logger, fetcher Fetcher, feature codersdk.CryptoKeyFeature, opts ...func(*cache)) *cache {
|
||||
cache := &cache{
|
||||
clock: quartz.NewReal(),
|
||||
logger: logger,
|
||||
@@ -134,16 +136,16 @@ func newCache(ctx context.Context, logger slog.Logger, fetcher Fetcher, feature
|
||||
}
|
||||
|
||||
cache.cond = sync.NewCond(&cache.mu)
|
||||
cache.refreshCtx, cache.refreshCancel = context.WithCancel(ctx)
|
||||
//nolint:gocritic // We need to be able to read the keys in order to cache them.
|
||||
cache.ctx, cache.cancel = context.WithCancel(dbauthz.AsKeyReader(ctx))
|
||||
cache.refresher = cache.clock.AfterFunc(refreshInterval, cache.refresh)
|
||||
|
||||
keys, err := cache.cryptoKeys(ctx)
|
||||
keys, err := cache.cryptoKeys(cache.ctx)
|
||||
if err != nil {
|
||||
cache.refreshCancel()
|
||||
return nil, xerrors.Errorf("initial fetch: %w", err)
|
||||
cache.logger.Critical(cache.ctx, "failed initial fetch", slog.Error(err))
|
||||
}
|
||||
cache.keys = keys
|
||||
return cache, nil
|
||||
return cache
|
||||
}
|
||||
|
||||
func (c *cache) EncryptingKey(ctx context.Context) (string, interface{}, error) {
|
||||
@@ -151,6 +153,8 @@ func (c *cache) EncryptingKey(ctx context.Context) (string, interface{}, error)
|
||||
return "", nil, ErrInvalidFeature
|
||||
}
|
||||
|
||||
//nolint:gocritic // cache can only read crypto keys.
|
||||
ctx = dbauthz.AsKeyReader(ctx)
|
||||
return c.cryptoKey(ctx, latestSequence)
|
||||
}
|
||||
|
||||
@@ -164,6 +168,8 @@ func (c *cache) DecryptingKey(ctx context.Context, id string) (interface{}, erro
|
||||
return nil, xerrors.Errorf("parse id: %w", err)
|
||||
}
|
||||
|
||||
//nolint:gocritic // cache can only read crypto keys.
|
||||
ctx = dbauthz.AsKeyReader(ctx)
|
||||
_, secret, err := c.cryptoKey(ctx, int32(seq))
|
||||
if err != nil {
|
||||
return nil, xerrors.Errorf("crypto key: %w", err)
|
||||
@@ -176,6 +182,8 @@ func (c *cache) SigningKey(ctx context.Context) (string, interface{}, error) {
|
||||
return "", nil, ErrInvalidFeature
|
||||
}
|
||||
|
||||
//nolint:gocritic // cache can only read crypto keys.
|
||||
ctx = dbauthz.AsKeyReader(ctx)
|
||||
return c.cryptoKey(ctx, latestSequence)
|
||||
}
|
||||
|
||||
@@ -188,7 +196,8 @@ func (c *cache) VerifyingKey(ctx context.Context, id string) (interface{}, error
|
||||
if err != nil {
|
||||
return nil, xerrors.Errorf("parse id: %w", err)
|
||||
}
|
||||
|
||||
//nolint:gocritic // cache can only read crypto keys.
|
||||
ctx = dbauthz.AsKeyReader(ctx)
|
||||
_, secret, err := c.cryptoKey(ctx, int32(seq))
|
||||
if err != nil {
|
||||
return nil, xerrors.Errorf("crypto key: %w", err)
|
||||
@@ -198,12 +207,12 @@ func (c *cache) VerifyingKey(ctx context.Context, id string) (interface{}, error
|
||||
}
|
||||
|
||||
func isEncryptionKeyFeature(feature codersdk.CryptoKeyFeature) bool {
|
||||
return feature == codersdk.CryptoKeyFeatureWorkspaceApp
|
||||
return feature == codersdk.CryptoKeyFeatureWorkspaceAppsAPIKey
|
||||
}
|
||||
|
||||
func isSigningKeyFeature(feature codersdk.CryptoKeyFeature) bool {
|
||||
switch feature {
|
||||
case codersdk.CryptoKeyFeatureTailnetResume, codersdk.CryptoKeyFeatureOIDCConvert:
|
||||
case codersdk.CryptoKeyFeatureTailnetResume, codersdk.CryptoKeyFeatureOIDCConvert, codersdk.CryptoKeyFeatureWorkspaceAppsToken:
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
@@ -292,14 +301,15 @@ func checkKey(key codersdk.CryptoKey, sequence int32, now time.Time) (string, []
|
||||
func (c *cache) refresh() {
|
||||
now := c.clock.Now("CryptoKeyCache", "refresh")
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
|
||||
if c.closed {
|
||||
c.mu.Unlock()
|
||||
return
|
||||
}
|
||||
|
||||
// If something's already fetching, we don't need to do anything.
|
||||
if c.fetching {
|
||||
c.mu.Unlock()
|
||||
return
|
||||
}
|
||||
|
||||
@@ -307,20 +317,21 @@ func (c *cache) refresh() {
|
||||
// is ongoing but prior to the timer getting reset. In this case we want to
|
||||
// avoid double fetching.
|
||||
if now.Sub(c.lastFetch) < refreshInterval {
|
||||
c.mu.Unlock()
|
||||
return
|
||||
}
|
||||
|
||||
c.fetching = true
|
||||
|
||||
c.mu.Unlock()
|
||||
keys, err := c.cryptoKeys(c.refreshCtx)
|
||||
keys, err := c.cryptoKeys(c.ctx)
|
||||
if err != nil {
|
||||
c.logger.Error(c.refreshCtx, "fetch crypto keys", slog.Error(err))
|
||||
c.logger.Error(c.ctx, "fetch crypto keys", slog.Error(err))
|
||||
return
|
||||
}
|
||||
|
||||
// We don't defer an unlock here due to the deferred unlock at the top of the function.
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
|
||||
c.lastFetch = c.clock.Now()
|
||||
c.refresher.Reset(refreshInterval)
|
||||
@@ -332,9 +343,9 @@ func (c *cache) refresh() {
|
||||
// cryptoKeys queries the control plane for the crypto keys.
|
||||
// Outside of initialization, this should only be called by fetch.
|
||||
func (c *cache) cryptoKeys(ctx context.Context) (map[int32]codersdk.CryptoKey, error) {
|
||||
keys, err := c.fetcher.Fetch(ctx)
|
||||
keys, err := c.fetcher.Fetch(ctx, c.feature)
|
||||
if err != nil {
|
||||
return nil, xerrors.Errorf("crypto keys: %w", err)
|
||||
return nil, xerrors.Errorf("fetch: %w", err)
|
||||
}
|
||||
cache := toKeyMap(keys, c.clock.Now())
|
||||
return cache, nil
|
||||
@@ -361,9 +372,28 @@ func (c *cache) Close() error {
|
||||
}
|
||||
|
||||
c.closed = true
|
||||
c.refreshCancel()
|
||||
c.cancel()
|
||||
c.refresher.Stop()
|
||||
c.cond.Broadcast()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// We have to do this to avoid a circular dependency on db2sdk (cryptokeys -> db2sdk -> tailnet -> cryptokeys)
|
||||
func toSDKKeys(keys []database.CryptoKey) []codersdk.CryptoKey {
|
||||
into := make([]codersdk.CryptoKey, 0, len(keys))
|
||||
for _, key := range keys {
|
||||
into = append(into, toSDK(key))
|
||||
}
|
||||
return into
|
||||
}
|
||||
|
||||
func toSDK(key database.CryptoKey) codersdk.CryptoKey {
|
||||
return codersdk.CryptoKey{
|
||||
Feature: codersdk.CryptoKeyFeature(key.Feature),
|
||||
Sequence: key.Sequence,
|
||||
StartsAt: key.StartsAt,
|
||||
DeletesAt: key.DeletesAt.Time,
|
||||
Secret: key.Secret.String,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -488,7 +488,7 @@ type fakeFetcher struct {
|
||||
called int
|
||||
}
|
||||
|
||||
func (f *fakeFetcher) Fetch(_ context.Context) ([]codersdk.CryptoKey, error) {
|
||||
func (f *fakeFetcher) Fetch(_ context.Context, _ codersdk.CryptoKeyFeature) ([]codersdk.CryptoKey, error) {
|
||||
f.called++
|
||||
return f.keys, nil
|
||||
}
|
||||
|
||||
@@ -11,6 +11,7 @@ import (
|
||||
|
||||
"cdr.dev/slog"
|
||||
"github.com/coder/coder/v2/coderd/database"
|
||||
"github.com/coder/coder/v2/coderd/database/dbauthz"
|
||||
"github.com/coder/coder/v2/coderd/database/dbtime"
|
||||
"github.com/coder/quartz"
|
||||
)
|
||||
@@ -53,10 +54,12 @@ func WithKeyDuration(keyDuration time.Duration) RotatorOption {
|
||||
// StartRotator starts a background process that rotates keys in the database.
|
||||
// It ensures there's at least one valid key per feature prior to returning.
|
||||
// Canceling the provided context will stop the background process.
|
||||
func StartRotator(ctx context.Context, logger slog.Logger, db database.Store, opts ...RotatorOption) error {
|
||||
func StartRotator(ctx context.Context, logger slog.Logger, db database.Store, opts ...RotatorOption) {
|
||||
//nolint:gocritic // KeyRotator can only rotate crypto keys.
|
||||
ctx = dbauthz.AsKeyRotator(ctx)
|
||||
kr := &rotator{
|
||||
db: db,
|
||||
logger: logger,
|
||||
logger: logger.Named("keyrotator"),
|
||||
clock: quartz.NewReal(),
|
||||
keyDuration: DefaultKeyDuration,
|
||||
features: database.AllCryptoKeyFeatureValues(),
|
||||
@@ -68,12 +71,10 @@ func StartRotator(ctx context.Context, logger slog.Logger, db database.Store, op
|
||||
|
||||
err := kr.rotateKeys(ctx)
|
||||
if err != nil {
|
||||
return xerrors.Errorf("rotate keys: %w", err)
|
||||
kr.logger.Critical(ctx, "failed to rotate keys", slog.Error(err))
|
||||
}
|
||||
|
||||
go kr.start(ctx)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// start begins the process of rotating keys.
|
||||
@@ -227,9 +228,11 @@ func (k *rotator) rotateKey(ctx context.Context, tx database.Store, key database
|
||||
|
||||
func generateNewSecret(feature database.CryptoKeyFeature) (string, error) {
|
||||
switch feature {
|
||||
case database.CryptoKeyFeatureWorkspaceApps:
|
||||
case database.CryptoKeyFeatureWorkspaceAppsAPIKey:
|
||||
return generateKey(32)
|
||||
case database.CryptoKeyFeatureOidcConvert:
|
||||
case database.CryptoKeyFeatureWorkspaceAppsToken:
|
||||
return generateKey(64)
|
||||
case database.CryptoKeyFeatureOIDCConvert:
|
||||
return generateKey(64)
|
||||
case database.CryptoKeyFeatureTailnetResume:
|
||||
return generateKey(64)
|
||||
@@ -248,9 +251,11 @@ func generateKey(length int) (string, error) {
|
||||
|
||||
func tokenDuration(feature database.CryptoKeyFeature) time.Duration {
|
||||
switch feature {
|
||||
case database.CryptoKeyFeatureWorkspaceApps:
|
||||
case database.CryptoKeyFeatureWorkspaceAppsAPIKey:
|
||||
return WorkspaceAppsTokenDuration
|
||||
case database.CryptoKeyFeatureOidcConvert:
|
||||
case database.CryptoKeyFeatureWorkspaceAppsToken:
|
||||
return WorkspaceAppsTokenDuration
|
||||
case database.CryptoKeyFeatureOIDCConvert:
|
||||
return OIDCConvertTokenDuration
|
||||
case database.CryptoKeyFeatureTailnetResume:
|
||||
return TailnetResumeTokenDuration
|
||||
|
||||
@@ -38,7 +38,7 @@ func Test_rotateKeys(t *testing.T) {
|
||||
clock: clock,
|
||||
logger: logger,
|
||||
features: []database.CryptoKeyFeature{
|
||||
database.CryptoKeyFeatureWorkspaceApps,
|
||||
database.CryptoKeyFeatureWorkspaceAppsAPIKey,
|
||||
},
|
||||
}
|
||||
|
||||
@@ -46,7 +46,7 @@ func Test_rotateKeys(t *testing.T) {
|
||||
|
||||
// Seed the database with an existing key.
|
||||
oldKey := dbgen.CryptoKey(t, db, database.CryptoKey{
|
||||
Feature: database.CryptoKeyFeatureWorkspaceApps,
|
||||
Feature: database.CryptoKeyFeatureWorkspaceAppsAPIKey,
|
||||
StartsAt: now,
|
||||
Sequence: 15,
|
||||
})
|
||||
@@ -69,11 +69,11 @@ func Test_rotateKeys(t *testing.T) {
|
||||
|
||||
// The new key should be created and have a starts_at of the old key's expires_at.
|
||||
newKey, err := db.GetCryptoKeyByFeatureAndSequence(ctx, database.GetCryptoKeyByFeatureAndSequenceParams{
|
||||
Feature: database.CryptoKeyFeatureWorkspaceApps,
|
||||
Feature: database.CryptoKeyFeatureWorkspaceAppsAPIKey,
|
||||
Sequence: oldKey.Sequence + 1,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
requireKey(t, newKey, database.CryptoKeyFeatureWorkspaceApps, oldKey.ExpiresAt(keyDuration), nullTime, oldKey.Sequence+1)
|
||||
requireKey(t, newKey, database.CryptoKeyFeatureWorkspaceAppsAPIKey, oldKey.ExpiresAt(keyDuration), nullTime, oldKey.Sequence+1)
|
||||
|
||||
// Advance the clock just before the keys delete time.
|
||||
clock.Advance(oldKey.DeletesAt.Time.UTC().Sub(now) - time.Second)
|
||||
@@ -123,7 +123,7 @@ func Test_rotateKeys(t *testing.T) {
|
||||
clock: clock,
|
||||
logger: logger,
|
||||
features: []database.CryptoKeyFeature{
|
||||
database.CryptoKeyFeatureWorkspaceApps,
|
||||
database.CryptoKeyFeatureWorkspaceAppsAPIKey,
|
||||
},
|
||||
}
|
||||
|
||||
@@ -131,7 +131,7 @@ func Test_rotateKeys(t *testing.T) {
|
||||
|
||||
// Seed the database with an existing key
|
||||
existingKey := dbgen.CryptoKey(t, db, database.CryptoKey{
|
||||
Feature: database.CryptoKeyFeatureWorkspaceApps,
|
||||
Feature: database.CryptoKeyFeatureWorkspaceAppsAPIKey,
|
||||
StartsAt: now,
|
||||
Sequence: 123,
|
||||
})
|
||||
@@ -179,7 +179,7 @@ func Test_rotateKeys(t *testing.T) {
|
||||
clock: clock,
|
||||
logger: logger,
|
||||
features: []database.CryptoKeyFeature{
|
||||
database.CryptoKeyFeatureWorkspaceApps,
|
||||
database.CryptoKeyFeatureWorkspaceAppsAPIKey,
|
||||
},
|
||||
}
|
||||
|
||||
@@ -187,7 +187,7 @@ func Test_rotateKeys(t *testing.T) {
|
||||
|
||||
// Seed the database with an existing key
|
||||
deletingKey := dbgen.CryptoKey(t, db, database.CryptoKey{
|
||||
Feature: database.CryptoKeyFeatureWorkspaceApps,
|
||||
Feature: database.CryptoKeyFeatureWorkspaceAppsAPIKey,
|
||||
StartsAt: now.Add(-keyDuration),
|
||||
Sequence: 789,
|
||||
DeletesAt: sql.NullTime{
|
||||
@@ -232,7 +232,7 @@ func Test_rotateKeys(t *testing.T) {
|
||||
clock: clock,
|
||||
logger: logger,
|
||||
features: []database.CryptoKeyFeature{
|
||||
database.CryptoKeyFeatureWorkspaceApps,
|
||||
database.CryptoKeyFeatureWorkspaceAppsAPIKey,
|
||||
},
|
||||
}
|
||||
|
||||
@@ -240,7 +240,7 @@ func Test_rotateKeys(t *testing.T) {
|
||||
|
||||
// Seed the database with an existing key
|
||||
deletingKey := dbgen.CryptoKey(t, db, database.CryptoKey{
|
||||
Feature: database.CryptoKeyFeatureWorkspaceApps,
|
||||
Feature: database.CryptoKeyFeatureWorkspaceAppsAPIKey,
|
||||
StartsAt: now,
|
||||
Sequence: 456,
|
||||
DeletesAt: sql.NullTime{
|
||||
@@ -281,7 +281,7 @@ func Test_rotateKeys(t *testing.T) {
|
||||
clock: clock,
|
||||
logger: logger,
|
||||
features: []database.CryptoKeyFeature{
|
||||
database.CryptoKeyFeatureWorkspaceApps,
|
||||
database.CryptoKeyFeatureWorkspaceAppsAPIKey,
|
||||
},
|
||||
}
|
||||
|
||||
@@ -291,7 +291,7 @@ func Test_rotateKeys(t *testing.T) {
|
||||
keys, err := db.GetCryptoKeys(ctx)
|
||||
require.NoError(t, err)
|
||||
require.Len(t, keys, 1)
|
||||
requireKey(t, keys[0], database.CryptoKeyFeatureWorkspaceApps, clock.Now().UTC(), nullTime, 1)
|
||||
requireKey(t, keys[0], database.CryptoKeyFeatureWorkspaceAppsAPIKey, clock.Now().UTC(), nullTime, 1)
|
||||
})
|
||||
|
||||
// Assert we insert a new key when the only key was manually deleted.
|
||||
@@ -312,14 +312,14 @@ func Test_rotateKeys(t *testing.T) {
|
||||
clock: clock,
|
||||
logger: logger,
|
||||
features: []database.CryptoKeyFeature{
|
||||
database.CryptoKeyFeatureWorkspaceApps,
|
||||
database.CryptoKeyFeatureWorkspaceAppsAPIKey,
|
||||
},
|
||||
}
|
||||
|
||||
now := dbnow(clock)
|
||||
|
||||
deletedkey := dbgen.CryptoKey(t, db, database.CryptoKey{
|
||||
Feature: database.CryptoKeyFeatureWorkspaceApps,
|
||||
Feature: database.CryptoKeyFeatureWorkspaceAppsAPIKey,
|
||||
StartsAt: now,
|
||||
Sequence: 19,
|
||||
DeletesAt: sql.NullTime{
|
||||
@@ -338,7 +338,7 @@ func Test_rotateKeys(t *testing.T) {
|
||||
keys, err := db.GetCryptoKeys(ctx)
|
||||
require.NoError(t, err)
|
||||
require.Len(t, keys, 1)
|
||||
requireKey(t, keys[0], database.CryptoKeyFeatureWorkspaceApps, now, nullTime, deletedkey.Sequence+1)
|
||||
requireKey(t, keys[0], database.CryptoKeyFeatureWorkspaceAppsAPIKey, now, nullTime, deletedkey.Sequence+1)
|
||||
})
|
||||
|
||||
// This tests ensures that rotation works with multiple
|
||||
@@ -365,9 +365,11 @@ func Test_rotateKeys(t *testing.T) {
|
||||
|
||||
now := dbnow(clock)
|
||||
|
||||
// We'll test a scenario where one feature has no valid keys.
|
||||
// Another has a key that should be rotate. And one that
|
||||
// has a valid key that shouldn't trigger an action.
|
||||
// We'll test a scenario where:
|
||||
// - One feature has no valid keys.
|
||||
// - One has a key that should be rotated.
|
||||
// - One has a valid key that shouldn't trigger an action.
|
||||
// - One has no keys at all.
|
||||
_ = dbgen.CryptoKey(t, db, database.CryptoKey{
|
||||
Feature: database.CryptoKeyFeatureTailnetResume,
|
||||
StartsAt: now.Add(-keyDuration),
|
||||
@@ -377,6 +379,7 @@ func Test_rotateKeys(t *testing.T) {
|
||||
Valid: false,
|
||||
},
|
||||
})
|
||||
// Generate another deleted key to ensure we insert after the latest sequence.
|
||||
deletedKey := dbgen.CryptoKey(t, db, database.CryptoKey{
|
||||
Feature: database.CryptoKeyFeatureTailnetResume,
|
||||
StartsAt: now.Add(-keyDuration),
|
||||
@@ -389,14 +392,14 @@ func Test_rotateKeys(t *testing.T) {
|
||||
|
||||
// Insert a key that should be rotated.
|
||||
rotatedKey := dbgen.CryptoKey(t, db, database.CryptoKey{
|
||||
Feature: database.CryptoKeyFeatureWorkspaceApps,
|
||||
Feature: database.CryptoKeyFeatureWorkspaceAppsAPIKey,
|
||||
StartsAt: now.Add(-keyDuration + time.Hour),
|
||||
Sequence: 42,
|
||||
})
|
||||
|
||||
// Insert a key that should not trigger an action.
|
||||
validKey := dbgen.CryptoKey(t, db, database.CryptoKey{
|
||||
Feature: database.CryptoKeyFeatureOidcConvert,
|
||||
Feature: database.CryptoKeyFeatureOIDCConvert,
|
||||
StartsAt: now,
|
||||
Sequence: 17,
|
||||
})
|
||||
@@ -406,26 +409,28 @@ func Test_rotateKeys(t *testing.T) {
|
||||
|
||||
keys, err := db.GetCryptoKeys(ctx)
|
||||
require.NoError(t, err)
|
||||
require.Len(t, keys, 4)
|
||||
require.Len(t, keys, 5)
|
||||
|
||||
kbf, err := keysByFeature(keys, database.AllCryptoKeyFeatureValues())
|
||||
require.NoError(t, err)
|
||||
|
||||
// No actions on OIDC convert.
|
||||
require.Len(t, kbf[database.CryptoKeyFeatureOidcConvert], 1)
|
||||
require.Len(t, kbf[database.CryptoKeyFeatureOIDCConvert], 1)
|
||||
// Workspace apps should have been rotated.
|
||||
require.Len(t, kbf[database.CryptoKeyFeatureWorkspaceApps], 2)
|
||||
require.Len(t, kbf[database.CryptoKeyFeatureWorkspaceAppsAPIKey], 2)
|
||||
// No existing key for tailnet resume should've
|
||||
// caused a key to be inserted.
|
||||
require.Len(t, kbf[database.CryptoKeyFeatureTailnetResume], 1)
|
||||
require.Len(t, kbf[database.CryptoKeyFeatureWorkspaceAppsToken], 1)
|
||||
|
||||
oidcKey := kbf[database.CryptoKeyFeatureOidcConvert][0]
|
||||
oidcKey := kbf[database.CryptoKeyFeatureOIDCConvert][0]
|
||||
tailnetKey := kbf[database.CryptoKeyFeatureTailnetResume][0]
|
||||
requireKey(t, oidcKey, database.CryptoKeyFeatureOidcConvert, now, nullTime, validKey.Sequence)
|
||||
appTokenKey := kbf[database.CryptoKeyFeatureWorkspaceAppsToken][0]
|
||||
requireKey(t, oidcKey, database.CryptoKeyFeatureOIDCConvert, now, nullTime, validKey.Sequence)
|
||||
requireKey(t, tailnetKey, database.CryptoKeyFeatureTailnetResume, now, nullTime, deletedKey.Sequence+1)
|
||||
|
||||
newKey := kbf[database.CryptoKeyFeatureWorkspaceApps][0]
|
||||
oldKey := kbf[database.CryptoKeyFeatureWorkspaceApps][1]
|
||||
requireKey(t, appTokenKey, database.CryptoKeyFeatureWorkspaceAppsToken, now, nullTime, 1)
|
||||
newKey := kbf[database.CryptoKeyFeatureWorkspaceAppsAPIKey][0]
|
||||
oldKey := kbf[database.CryptoKeyFeatureWorkspaceAppsAPIKey][1]
|
||||
if newKey.Sequence == rotatedKey.Sequence {
|
||||
oldKey, newKey = newKey, oldKey
|
||||
}
|
||||
@@ -433,8 +438,8 @@ func Test_rotateKeys(t *testing.T) {
|
||||
Time: rotatedKey.ExpiresAt(keyDuration).Add(WorkspaceAppsTokenDuration + time.Hour),
|
||||
Valid: true,
|
||||
}
|
||||
requireKey(t, oldKey, database.CryptoKeyFeatureWorkspaceApps, rotatedKey.StartsAt.UTC(), deletesAt, rotatedKey.Sequence)
|
||||
requireKey(t, newKey, database.CryptoKeyFeatureWorkspaceApps, rotatedKey.ExpiresAt(keyDuration), nullTime, rotatedKey.Sequence+1)
|
||||
requireKey(t, oldKey, database.CryptoKeyFeatureWorkspaceAppsAPIKey, rotatedKey.StartsAt.UTC(), deletesAt, rotatedKey.Sequence)
|
||||
requireKey(t, newKey, database.CryptoKeyFeatureWorkspaceAppsAPIKey, rotatedKey.ExpiresAt(keyDuration), nullTime, rotatedKey.Sequence+1)
|
||||
})
|
||||
|
||||
t.Run("UnknownFeature", func(t *testing.T) {
|
||||
@@ -478,11 +483,11 @@ func Test_rotateKeys(t *testing.T) {
|
||||
keyDuration: keyDuration,
|
||||
clock: clock,
|
||||
logger: logger,
|
||||
features: []database.CryptoKeyFeature{database.CryptoKeyFeatureWorkspaceApps},
|
||||
features: []database.CryptoKeyFeature{database.CryptoKeyFeatureWorkspaceAppsAPIKey},
|
||||
}
|
||||
|
||||
expiringKey := dbgen.CryptoKey(t, db, database.CryptoKey{
|
||||
Feature: database.CryptoKeyFeatureWorkspaceApps,
|
||||
Feature: database.CryptoKeyFeatureWorkspaceAppsAPIKey,
|
||||
StartsAt: now.Add(-keyDuration),
|
||||
Sequence: 345,
|
||||
})
|
||||
@@ -522,19 +527,19 @@ func Test_rotateKeys(t *testing.T) {
|
||||
keyDuration: keyDuration,
|
||||
clock: clock,
|
||||
logger: logger,
|
||||
features: []database.CryptoKeyFeature{database.CryptoKeyFeatureWorkspaceApps},
|
||||
features: []database.CryptoKeyFeature{database.CryptoKeyFeatureWorkspaceAppsAPIKey},
|
||||
}
|
||||
|
||||
now := dbnow(clock)
|
||||
|
||||
expiredKey := dbgen.CryptoKey(t, db, database.CryptoKey{
|
||||
Feature: database.CryptoKeyFeatureWorkspaceApps,
|
||||
Feature: database.CryptoKeyFeatureWorkspaceAppsAPIKey,
|
||||
StartsAt: now.Add(-keyDuration - 2*time.Hour),
|
||||
Sequence: 19,
|
||||
})
|
||||
|
||||
deletedKey := dbgen.CryptoKey(t, db, database.CryptoKey{
|
||||
Feature: database.CryptoKeyFeatureWorkspaceApps,
|
||||
Feature: database.CryptoKeyFeatureWorkspaceAppsAPIKey,
|
||||
StartsAt: now,
|
||||
Sequence: 20,
|
||||
Secret: sql.NullString{
|
||||
@@ -587,9 +592,11 @@ func requireKey(t *testing.T, key database.CryptoKey, feature database.CryptoKey
|
||||
require.NoError(t, err)
|
||||
|
||||
switch key.Feature {
|
||||
case database.CryptoKeyFeatureOidcConvert:
|
||||
case database.CryptoKeyFeatureOIDCConvert:
|
||||
require.Len(t, secret, 64)
|
||||
case database.CryptoKeyFeatureWorkspaceApps:
|
||||
case database.CryptoKeyFeatureWorkspaceAppsToken:
|
||||
require.Len(t, secret, 64)
|
||||
case database.CryptoKeyFeatureWorkspaceAppsAPIKey:
|
||||
require.Len(t, secret, 32)
|
||||
case database.CryptoKeyFeatureTailnetResume:
|
||||
require.Len(t, secret, 64)
|
||||
|
||||
@@ -34,8 +34,7 @@ func TestRotator(t *testing.T) {
|
||||
require.NoError(t, err)
|
||||
require.Len(t, dbkeys, 0)
|
||||
|
||||
err = cryptokeys.StartRotator(ctx, logger, db, cryptokeys.WithClock(clock))
|
||||
require.NoError(t, err)
|
||||
cryptokeys.StartRotator(ctx, logger, db, cryptokeys.WithClock(clock))
|
||||
|
||||
// Fetch the keys from the database and ensure they
|
||||
// are as expected.
|
||||
@@ -58,7 +57,7 @@ func TestRotator(t *testing.T) {
|
||||
now := clock.Now().UTC()
|
||||
|
||||
rotatingKey := dbgen.CryptoKey(t, db, database.CryptoKey{
|
||||
Feature: database.CryptoKeyFeatureWorkspaceApps,
|
||||
Feature: database.CryptoKeyFeatureWorkspaceAppsAPIKey,
|
||||
StartsAt: now.Add(-cryptokeys.DefaultKeyDuration + time.Hour + time.Minute),
|
||||
Sequence: 12345,
|
||||
})
|
||||
@@ -66,8 +65,7 @@ func TestRotator(t *testing.T) {
|
||||
trap := clock.Trap().TickerFunc()
|
||||
t.Cleanup(trap.Close)
|
||||
|
||||
err := cryptokeys.StartRotator(ctx, logger, db, cryptokeys.WithClock(clock))
|
||||
require.NoError(t, err)
|
||||
cryptokeys.StartRotator(ctx, logger, db, cryptokeys.WithClock(clock))
|
||||
|
||||
initialKeyLen := len(database.AllCryptoKeyFeatureValues())
|
||||
// Fetch the keys from the database and ensure they
|
||||
@@ -85,7 +83,7 @@ func TestRotator(t *testing.T) {
|
||||
require.NoError(t, err)
|
||||
require.Len(t, keys, initialKeyLen+1)
|
||||
|
||||
newKey, err := db.GetLatestCryptoKeyByFeature(ctx, database.CryptoKeyFeatureWorkspaceApps)
|
||||
newKey, err := db.GetLatestCryptoKeyByFeature(ctx, database.CryptoKeyFeatureWorkspaceAppsAPIKey)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, rotatingKey.Sequence+1, newKey.Sequence)
|
||||
require.Equal(t, rotatingKey.ExpiresAt(cryptokeys.DefaultKeyDuration), newKey.StartsAt.UTC())
|
||||
|
||||
Reference in New Issue
Block a user