mirror of
https://github.com/zitadel/zitadel.git
synced 2025-10-20 16:29:00 +00:00
fix: improve key rotation (#1107)
* key rotation * fix: rotate signing key * cleanup * introspect * testingapplication key * date * client keys * fix client keys * fix client keys * access tokens only for users * AuthMethodPrivateKeyJWT * client keys * set introspection info correctly * managae apis * update oidc pkg * cleanup * merge msater * set current sequence in migration * set current sequence in migration * set current sequence in migration * ensure authn keys uptodate * improve key rotation * fix: return api config in ApplicationView * fix mocks for tests * fix(mock): corrected unit tests for updated mock package Co-authored-by: Stefan Benz <stefan@caos.ch>
This commit is contained in:
@@ -2,12 +2,18 @@ package eventstore
|
||||
|
||||
import (
|
||||
"context"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"github.com/caos/logging"
|
||||
"gopkg.in/square/go-jose.v2"
|
||||
|
||||
"github.com/caos/zitadel/internal/api/authz"
|
||||
"github.com/caos/zitadel/internal/auth/repository/eventsourcing/view"
|
||||
"github.com/caos/zitadel/internal/crypto"
|
||||
"github.com/caos/zitadel/internal/errors"
|
||||
"github.com/caos/zitadel/internal/eventstore/spooler"
|
||||
"github.com/caos/zitadel/internal/id"
|
||||
"github.com/caos/zitadel/internal/key/model"
|
||||
key_event "github.com/caos/zitadel/internal/key/repository/eventsourcing"
|
||||
)
|
||||
@@ -15,12 +21,21 @@ import (
|
||||
const (
|
||||
oidcUser = "OIDC"
|
||||
iamOrg = "IAM"
|
||||
|
||||
signingKey = "signing_key"
|
||||
)
|
||||
|
||||
type KeyRepository struct {
|
||||
KeyEvents *key_event.KeyEventstore
|
||||
View *view.View
|
||||
SigningKeyRotation time.Duration
|
||||
KeyEvents *key_event.KeyEventstore
|
||||
View *view.View
|
||||
SigningKeyRotationCheck time.Duration
|
||||
SigningKeyGracefulPeriod time.Duration
|
||||
KeyAlgorithm crypto.EncryptionAlgorithm
|
||||
KeyChan <-chan *model.KeyView
|
||||
Locker spooler.Locker
|
||||
lockID string
|
||||
currentKeyID string
|
||||
currentKeyExpiration time.Time
|
||||
}
|
||||
|
||||
func (k *KeyRepository) GenerateSigningKeyPair(ctx context.Context, algorithm string) error {
|
||||
@@ -29,15 +44,23 @@ func (k *KeyRepository) GenerateSigningKeyPair(ctx context.Context, algorithm st
|
||||
return err
|
||||
}
|
||||
|
||||
func (k *KeyRepository) GetSigningKey(ctx context.Context, keyCh chan<- jose.SigningKey, errCh chan<- error, renewTimer <-chan time.Time) {
|
||||
func (k *KeyRepository) GetSigningKey(ctx context.Context, keyCh chan<- jose.SigningKey, algorithm string) {
|
||||
renewTimer := time.After(0)
|
||||
go func() {
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return
|
||||
case key := <-k.KeyChan:
|
||||
refreshed, err := k.refreshSigningKey(ctx, key, keyCh, algorithm)
|
||||
logging.Log("KEY-asd5g").OnError(err).Error("could not refresh signing key on key channel push")
|
||||
k.setRenewTimer(renewTimer, refreshed)
|
||||
case <-renewTimer:
|
||||
k.refreshSigningKey(keyCh, errCh)
|
||||
renewTimer = time.After(k.SigningKeyRotation)
|
||||
key, err := k.latestSigningKey()
|
||||
logging.Log("KEY-DAfh4").OnError(err).Error("could not check for latest signing key")
|
||||
refreshed, err := k.refreshSigningKey(ctx, key, keyCh, algorithm)
|
||||
logging.Log("KEY-DAfh4").OnError(err).Error("could not refresh signing key when ensuring key")
|
||||
k.setRenewTimer(renewTimer, refreshed)
|
||||
}
|
||||
}
|
||||
}()
|
||||
@@ -55,19 +78,96 @@ func (k *KeyRepository) GetKeySet(ctx context.Context) (*jose.JSONWebKeySet, err
|
||||
return &jose.JSONWebKeySet{Keys: webKeys}, nil
|
||||
}
|
||||
|
||||
func (k *KeyRepository) refreshSigningKey(keyCh chan<- jose.SigningKey, errCh chan<- error) {
|
||||
key, err := k.View.GetSigningKey()
|
||||
if err != nil {
|
||||
errCh <- err
|
||||
return
|
||||
func (k *KeyRepository) setRenewTimer(timer <-chan time.Time, refreshed bool) {
|
||||
duration := k.SigningKeyRotationCheck
|
||||
if refreshed {
|
||||
duration = k.currentKeyExpiration.Sub(time.Now().Add(k.SigningKeyGracefulPeriod + k.SigningKeyRotationCheck*2))
|
||||
}
|
||||
timer = time.After(duration)
|
||||
}
|
||||
|
||||
func (k *KeyRepository) latestSigningKey() (shortRefresh *model.KeyView, err error) {
|
||||
key, errView := k.View.GetActivePrivateKeyForSigning(time.Now().UTC().Add(k.SigningKeyGracefulPeriod))
|
||||
if errView != nil && !errors.IsNotFound(errView) {
|
||||
logging.Log("EVENT-GEd4h").WithError(errView).Warn("could not get signing key")
|
||||
return nil, errView
|
||||
}
|
||||
return key, nil
|
||||
}
|
||||
|
||||
func (k *KeyRepository) ensureIsLatestKey(ctx context.Context) (bool, error) {
|
||||
sequence, err := k.View.GetLatestKeySequence()
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
events, err := k.KeyEvents.LatestKeyEvents(ctx, sequence.CurrentSequence)
|
||||
if err != nil {
|
||||
logging.Log("EVENT-der5g").WithError(err).Warn("error retrieving new events")
|
||||
return false, err
|
||||
}
|
||||
if len(events) > 0 {
|
||||
logging.Log("EVENT-GBD23").Warn("view not up to date, retrying later")
|
||||
return false, nil
|
||||
}
|
||||
return true, nil
|
||||
}
|
||||
|
||||
func (k *KeyRepository) refreshSigningKey(ctx context.Context, key *model.KeyView, keyCh chan<- jose.SigningKey, algorithm string) (refreshed bool, err error) {
|
||||
if key == nil {
|
||||
if k.currentKeyExpiration.Before(time.Now().UTC()) {
|
||||
keyCh <- jose.SigningKey{}
|
||||
}
|
||||
if ok, err := k.ensureIsLatestKey(ctx); !ok && err == nil {
|
||||
return false, err
|
||||
}
|
||||
err = k.lockAndGenerateSigningKeyPair(ctx, algorithm)
|
||||
logging.Log("EVENT-B4d21").OnError(err).Warn("could not create signing key")
|
||||
return false, err
|
||||
}
|
||||
|
||||
if k.currentKeyID == key.ID {
|
||||
return false, nil
|
||||
}
|
||||
if ok, err := k.ensureIsLatestKey(ctx); !ok && err == nil {
|
||||
return false, err
|
||||
}
|
||||
signingKey, err := model.SigningKeyFromKeyView(key, k.KeyAlgorithm)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
k.currentKeyID = signingKey.ID
|
||||
k.currentKeyExpiration = key.Expiry
|
||||
keyCh <- jose.SigningKey{
|
||||
Algorithm: jose.SignatureAlgorithm(key.Algorithm),
|
||||
Algorithm: jose.SignatureAlgorithm(signingKey.Algorithm),
|
||||
Key: jose.JSONWebKey{
|
||||
KeyID: key.ID,
|
||||
Key: key.Key,
|
||||
KeyID: signingKey.ID,
|
||||
Key: signingKey.Key,
|
||||
},
|
||||
}
|
||||
return true, nil
|
||||
}
|
||||
|
||||
func (k *KeyRepository) lockAndGenerateSigningKeyPair(ctx context.Context, algorithm string) error {
|
||||
err := k.Locker.Renew(k.lockerID(), signingKey, k.SigningKeyRotationCheck*2)
|
||||
if err != nil {
|
||||
if errors.IsErrorAlreadyExists(err) {
|
||||
return nil
|
||||
}
|
||||
return err
|
||||
}
|
||||
return k.GenerateSigningKeyPair(ctx, algorithm)
|
||||
}
|
||||
|
||||
func (k *KeyRepository) lockerID() string {
|
||||
if k.lockID == "" {
|
||||
var err error
|
||||
k.lockID, err = os.Hostname()
|
||||
if err != nil || k.lockID == "" {
|
||||
k.lockID, err = id.SonyFlakeGenerator.Next()
|
||||
logging.Log("EVENT-bsdf6").OnError(err).Panic("unable to generate lockID")
|
||||
}
|
||||
}
|
||||
return k.lockID
|
||||
}
|
||||
|
||||
func setOIDCCtx(ctx context.Context) context.Context {
|
||||
|
@@ -7,6 +7,7 @@ import (
|
||||
"github.com/caos/zitadel/internal/eventstore"
|
||||
"github.com/caos/zitadel/internal/eventstore/query"
|
||||
iam_events "github.com/caos/zitadel/internal/iam/repository/eventsourcing"
|
||||
key_model "github.com/caos/zitadel/internal/key/model"
|
||||
org_events "github.com/caos/zitadel/internal/org/repository/eventsourcing"
|
||||
proj_event "github.com/caos/zitadel/internal/project/repository/eventsourcing"
|
||||
|
||||
@@ -41,7 +42,7 @@ type EventstoreRepos struct {
|
||||
IamEvents *iam_events.IAMEventstore
|
||||
}
|
||||
|
||||
func Register(configs Configs, bulkLimit, errorCount uint64, view *view.View, es eventstore.Eventstore, repos EventstoreRepos, systemDefaults sd.SystemDefaults) []query.Handler {
|
||||
func Register(configs Configs, bulkLimit, errorCount uint64, view *view.View, es eventstore.Eventstore, repos EventstoreRepos, systemDefaults sd.SystemDefaults, keyChan chan<- *key_model.KeyView) []query.Handler {
|
||||
return []query.Handler{
|
||||
newUser(
|
||||
handler{view, bulkLimit, configs.cycleDuration("User"), errorCount, es},
|
||||
@@ -59,7 +60,8 @@ func Register(configs Configs, bulkLimit, errorCount uint64, view *view.View, es
|
||||
handler{view, bulkLimit, configs.cycleDuration("Token"), errorCount, es},
|
||||
repos.ProjectEvents),
|
||||
newKey(
|
||||
handler{view, bulkLimit, configs.cycleDuration("Key"), errorCount, es}),
|
||||
handler{view, bulkLimit, configs.cycleDuration("Key"), errorCount, es},
|
||||
keyChan),
|
||||
newApplication(handler{view, bulkLimit, configs.cycleDuration("Application"), errorCount, es},
|
||||
repos.ProjectEvents),
|
||||
newOrg(
|
||||
|
@@ -4,10 +4,12 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/caos/logging"
|
||||
|
||||
"github.com/caos/zitadel/internal/eventstore"
|
||||
"github.com/caos/zitadel/internal/eventstore/models"
|
||||
"github.com/caos/zitadel/internal/eventstore/query"
|
||||
"github.com/caos/zitadel/internal/eventstore/spooler"
|
||||
"github.com/caos/zitadel/internal/key/model"
|
||||
"github.com/caos/zitadel/internal/key/repository/eventsourcing"
|
||||
es_model "github.com/caos/zitadel/internal/key/repository/eventsourcing/model"
|
||||
view_model "github.com/caos/zitadel/internal/key/repository/view/model"
|
||||
@@ -20,11 +22,13 @@ const (
|
||||
type Key struct {
|
||||
handler
|
||||
subscription *eventstore.Subscription
|
||||
keyChan chan<- *model.KeyView
|
||||
}
|
||||
|
||||
func newKey(handler handler) *Key {
|
||||
func newKey(handler handler, keyChan chan<- *model.KeyView) *Key {
|
||||
h := &Key{
|
||||
handler: handler,
|
||||
keyChan: keyChan,
|
||||
}
|
||||
|
||||
h.subscribe()
|
||||
@@ -75,7 +79,12 @@ func (k *Key) Reduce(event *models.Event) error {
|
||||
if privateKey.Expiry.Before(time.Now()) && publicKey.Expiry.Before(time.Now()) {
|
||||
return k.view.ProcessedKeySequence(event)
|
||||
}
|
||||
return k.view.PutKeys(privateKey, publicKey, event)
|
||||
err = k.view.PutKeys(privateKey, publicKey, event)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
k.keyChan <- view_model.KeyViewToModel(privateKey)
|
||||
return nil
|
||||
default:
|
||||
return k.view.ProcessedKeySequence(event)
|
||||
}
|
||||
|
@@ -17,6 +17,7 @@ import (
|
||||
es_spol "github.com/caos/zitadel/internal/eventstore/spooler"
|
||||
es_iam "github.com/caos/zitadel/internal/iam/repository/eventsourcing"
|
||||
"github.com/caos/zitadel/internal/id"
|
||||
key_model "github.com/caos/zitadel/internal/key/model"
|
||||
es_key "github.com/caos/zitadel/internal/key/repository/eventsourcing"
|
||||
es_org "github.com/caos/zitadel/internal/org/repository/eventsourcing"
|
||||
es_proj "github.com/caos/zitadel/internal/project/repository/eventsourcing"
|
||||
@@ -83,6 +84,7 @@ func Start(conf Config, authZ authz.Config, systemDefaults sd.SystemDefaults, au
|
||||
return nil, err
|
||||
}
|
||||
|
||||
keyChan := make(chan *key_model.KeyView)
|
||||
key, err := es_key.StartKey(es, conf.KeyConfig, keyAlgorithm, idGenerator)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -112,7 +114,9 @@ func Start(conf Config, authZ authz.Config, systemDefaults sd.SystemDefaults, au
|
||||
org := es_org.StartOrg(es_org.OrgConfig{Eventstore: es, IAMDomain: conf.Domain}, systemDefaults)
|
||||
|
||||
repos := handler.EventstoreRepos{UserEvents: user, ProjectEvents: project, OrgEvents: org, IamEvents: iam}
|
||||
spool := spooler.StartSpooler(conf.Spooler, es, view, sqlClient, repos, systemDefaults)
|
||||
spool := spooler.StartSpooler(conf.Spooler, es, view, sqlClient, repos, systemDefaults, keyChan)
|
||||
|
||||
locker := spooler.NewLocker(sqlClient)
|
||||
|
||||
return &EsRepository{
|
||||
spool,
|
||||
@@ -150,9 +154,13 @@ func Start(conf Config, authZ authz.Config, systemDefaults sd.SystemDefaults, au
|
||||
View: view,
|
||||
},
|
||||
eventstore.KeyRepository{
|
||||
KeyEvents: key,
|
||||
View: view,
|
||||
SigningKeyRotation: conf.KeyConfig.SigningKeyRotation.Duration,
|
||||
KeyEvents: key,
|
||||
View: view,
|
||||
SigningKeyRotationCheck: conf.KeyConfig.SigningKeyRotationCheck.Duration,
|
||||
SigningKeyGracefulPeriod: conf.KeyConfig.SigningKeyGracefulPeriod.Duration,
|
||||
KeyAlgorithm: keyAlgorithm,
|
||||
Locker: locker,
|
||||
KeyChan: keyChan,
|
||||
},
|
||||
eventstore.ApplicationRepo{
|
||||
View: view,
|
||||
|
@@ -15,6 +15,10 @@ type locker struct {
|
||||
dbClient *sql.DB
|
||||
}
|
||||
|
||||
func NewLocker(client *sql.DB) *locker {
|
||||
return &locker{dbClient: client}
|
||||
}
|
||||
|
||||
func (l *locker) Renew(lockerID, viewModel string, waitTime time.Duration) error {
|
||||
return es_locker.Renew(l.dbClient, lockTable, lockerID, viewModel, waitTime)
|
||||
}
|
||||
|
@@ -4,6 +4,7 @@ import (
|
||||
"database/sql"
|
||||
|
||||
sd "github.com/caos/zitadel/internal/config/systemdefaults"
|
||||
key_model "github.com/caos/zitadel/internal/key/model"
|
||||
|
||||
"github.com/caos/zitadel/internal/auth/repository/eventsourcing/handler"
|
||||
"github.com/caos/zitadel/internal/auth/repository/eventsourcing/view"
|
||||
@@ -19,12 +20,12 @@ type SpoolerConfig struct {
|
||||
Handlers handler.Configs
|
||||
}
|
||||
|
||||
func StartSpooler(c SpoolerConfig, es eventstore.Eventstore, view *view.View, client *sql.DB, repos handler.EventstoreRepos, systemDefaults sd.SystemDefaults) *spooler.Spooler {
|
||||
func StartSpooler(c SpoolerConfig, es eventstore.Eventstore, view *view.View, client *sql.DB, repos handler.EventstoreRepos, systemDefaults sd.SystemDefaults, keyChan chan<- *key_model.KeyView) *spooler.Spooler {
|
||||
spoolerConfig := spooler.Config{
|
||||
Eventstore: es,
|
||||
Locker: &locker{dbClient: client},
|
||||
ConcurrentWorkers: c.ConcurrentWorkers,
|
||||
ViewHandlers: handler.Register(c.Handlers, c.BulkLimit, c.FailureCountUntilSkip, view, es, repos, systemDefaults),
|
||||
ViewHandlers: handler.Register(c.Handlers, c.BulkLimit, c.FailureCountUntilSkip, view, es, repos, systemDefaults, keyChan),
|
||||
}
|
||||
spool := spoolerConfig.New()
|
||||
spool.Start()
|
||||
|
@@ -1,6 +1,8 @@
|
||||
package view
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/caos/zitadel/internal/errors"
|
||||
"github.com/caos/zitadel/internal/eventstore/models"
|
||||
key_model "github.com/caos/zitadel/internal/key/model"
|
||||
@@ -17,12 +19,21 @@ func (v *View) KeyByIDAndType(keyID string, private bool) (*model.KeyView, error
|
||||
return view.KeyByIDAndType(v.Db, keyTable, keyID, private)
|
||||
}
|
||||
|
||||
func (v *View) GetSigningKey() (*key_model.SigningKey, error) {
|
||||
key, err := view.GetSigningKey(v.Db, keyTable)
|
||||
func (v *View) GetActivePrivateKeyForSigning(expiry time.Time) (*key_model.KeyView, error) {
|
||||
key, err := view.GetSigningKey(v.Db, keyTable, expiry)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return key_model.SigningKeyFromKeyView(model.KeyViewToModel(key), v.keyAlgorithm)
|
||||
return model.KeyViewToModel(key), nil
|
||||
}
|
||||
|
||||
func (v *View) GetSigningKey(expiry time.Time) (*key_model.SigningKey, time.Time, error) {
|
||||
key, err := view.GetSigningKey(v.Db, keyTable, expiry)
|
||||
if err != nil {
|
||||
return nil, time.Time{}, err
|
||||
}
|
||||
signingKey, err := key_model.SigningKeyFromKeyView(model.KeyViewToModel(key), v.keyAlgorithm)
|
||||
return signingKey, key.Expiry, err
|
||||
}
|
||||
|
||||
func (v *View) GetActiveKeySet() ([]*key_model.PublicKey, error) {
|
||||
|
@@ -2,13 +2,12 @@ package repository
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
|
||||
"gopkg.in/square/go-jose.v2"
|
||||
)
|
||||
|
||||
type KeyRepository interface {
|
||||
GenerateSigningKeyPair(ctx context.Context, algorithm string) error
|
||||
GetSigningKey(ctx context.Context, keyCh chan<- jose.SigningKey, errCh chan<- error, timer <-chan time.Time)
|
||||
GetSigningKey(ctx context.Context, keyCh chan<- jose.SigningKey, algorithm string)
|
||||
GetKeySet(ctx context.Context) (*jose.JSONWebKeySet, error)
|
||||
}
|
||||
|
Reference in New Issue
Block a user