diff --git a/pkg/cmd/grafana-cli/commands/commands.go b/pkg/cmd/grafana-cli/commands/commands.go index 53730ec033d..55f381ebec3 100644 --- a/pkg/cmd/grafana-cli/commands/commands.go +++ b/pkg/cmd/grafana-cli/commands/commands.go @@ -188,6 +188,11 @@ var adminCommands = []*cli.Command{ Usage: "Rolls back secrets to legacy encryption. Returns ok unless there is an error. Safe to execute multiple times.", Action: runRunnerCommand(secretsmigrations.RollBackSecrets), }, + { + Name: "re-encrypt-data-keys", + Usage: "Rotates persisted data encryption keys. Returns ok unless there is an error. Safe to execute multiple times.", + Action: runRunnerCommand(secretsmigrations.ReEncryptDEKS), + }, }, }, } diff --git a/pkg/cmd/grafana-cli/commands/secretsmigrations/reencrypt_deks.go b/pkg/cmd/grafana-cli/commands/secretsmigrations/reencrypt_deks.go new file mode 100644 index 00000000000..0d97599c491 --- /dev/null +++ b/pkg/cmd/grafana-cli/commands/secretsmigrations/reencrypt_deks.go @@ -0,0 +1,19 @@ +package secretsmigrations + +import ( + "context" + + "github.com/grafana/grafana/pkg/cmd/grafana-cli/logger" + "github.com/grafana/grafana/pkg/cmd/grafana-cli/runner" + "github.com/grafana/grafana/pkg/cmd/grafana-cli/utils" + "github.com/grafana/grafana/pkg/services/featuremgmt" +) + +func ReEncryptDEKS(_ utils.CommandLine, runner runner.Runner) error { + if !runner.Features.IsEnabled(featuremgmt.FlagEnvelopeEncryption) { + logger.Warn("Envelope encryption is not enabled, quitting...") + return nil + } + + return runner.SecretsService.ReEncryptDataKeys(context.Background()) +} diff --git a/pkg/services/secrets/database/database.go b/pkg/services/secrets/database/database.go index b9e0ac81863..19a5e509e93 100644 --- a/pkg/services/secrets/database/database.go +++ b/pkg/services/secrets/database/database.go @@ -87,3 +87,42 @@ func (ss *SecretsStoreImpl) DeleteDataKey(ctx context.Context, name string) erro return err }) } + +func (ss *SecretsStoreImpl) ReEncryptDataKeys( + ctx context.Context, + providers map[secrets.ProviderID]secrets.Provider, + currProvider secrets.ProviderID, +) error { + return ss.sqlStore.WithTransactionalDbSession(ctx, func(sess *sqlstore.DBSession) error { + keys := make([]*secrets.DataKey, 0) + if err := sess.Table(dataKeysTable).Find(&keys); err != nil { + return err + } + + for _, k := range keys { + provider, ok := providers[k.Provider] + if !ok { + return fmt.Errorf("could not find encryption provider '%s'", k.Provider) + } + + decrypted, err := provider.Decrypt(ctx, k.EncryptedData) + if err != nil { + return err + } + + // Updating current data key by re-encrypting it with current provider. + // Accessing the current provider within providers map should be safe. + k.Provider = currProvider + k.EncryptedData, err = providers[currProvider].Encrypt(ctx, decrypted) + if err != nil { + return err + } + + if _, err := sess.Table(dataKeysTable).Where("name = ?", k.Name).Update(k); err != nil { + return err + } + } + + return nil + }) +} diff --git a/pkg/services/secrets/fakes/fake_service.go b/pkg/services/secrets/fakes/fake_service.go index b743c3af7ec..9a9b2c1c35a 100644 --- a/pkg/services/secrets/fakes/fake_service.go +++ b/pkg/services/secrets/fakes/fake_service.go @@ -40,6 +40,10 @@ func (f FakeSecretsService) GetDecryptedValue(_ context.Context, sjd map[string] return fallback } +func (f FakeSecretsService) ReEncryptDataKeys(_ context.Context) error { + return nil +} + func (f FakeSecretsService) CurrentProviderID() string { return "fakeProvider" } diff --git a/pkg/services/secrets/fakes/fake_store.go b/pkg/services/secrets/fakes/fake_store.go index 6ac990f6d1f..53a89956bcd 100644 --- a/pkg/services/secrets/fakes/fake_store.go +++ b/pkg/services/secrets/fakes/fake_store.go @@ -45,3 +45,7 @@ func (f FakeSecretsStore) DeleteDataKey(_ context.Context, name string) error { delete(f.store, name) return nil } + +func (f FakeSecretsStore) ReEncryptDataKeys(_ context.Context, _ map[secrets.ProviderID]secrets.Provider, _ secrets.ProviderID) error { + return nil +} diff --git a/pkg/services/secrets/manager/manager.go b/pkg/services/secrets/manager/manager.go index 14cf179537c..b31f0c7f73b 100644 --- a/pkg/services/secrets/manager/manager.go +++ b/pkg/services/secrets/manager/manager.go @@ -353,6 +353,17 @@ func (s *SecretsService) GetProviders() map[secrets.ProviderID]secrets.Provider return s.providers } +func (s *SecretsService) ReEncryptDataKeys(ctx context.Context) error { + err := s.store.ReEncryptDataKeys(ctx, s.providers, s.currentProviderID) + if err != nil { + return nil + } + + // Invalidate cache + s.dataKeyCache = make(map[string]dataKeyCacheItem) + return err +} + // These variables are used to test the code // responsible for periodically cleaning up // data encryption keys cache. diff --git a/pkg/services/secrets/manager/manager_test.go b/pkg/services/secrets/manager/manager_test.go index 7080c008dcf..ada2608c712 100644 --- a/pkg/services/secrets/manager/manager_test.go +++ b/pkg/services/secrets/manager/manager_test.go @@ -319,3 +319,42 @@ func TestSecretsService_Run(t *testing.T) { assert.True(t, svc.dataKeyCache[dataKeyID].expiry.After(time.Now().Add(dekTTL))) }) } + +func TestSecretsService_ReEncryptDataKeys(t *testing.T) { + ctx := context.Background() + sql := sqlstore.InitTestDB(t) + store := database.ProvideSecretsStore(sql) + svc := SetupTestService(t, store) + + // Encrypt to generate data encryption key + withoutScope := secrets.WithoutScope() + ciphertext, err := svc.Encrypt(ctx, []byte("grafana"), withoutScope) + require.NoError(t, err) + + t.Run("existing key should be re-encrypted", func(t *testing.T) { + prevDataKeys, err := store.GetAllDataKeys(ctx) + require.NoError(t, err) + require.Len(t, prevDataKeys, 1) + + err = svc.ReEncryptDataKeys(ctx) + require.NoError(t, err) + + reEncryptedDataKeys, err := store.GetAllDataKeys(ctx) + require.NoError(t, err) + require.Len(t, reEncryptedDataKeys, 1) + + assert.NotEqual(t, prevDataKeys[0].EncryptedData, reEncryptedDataKeys[0].EncryptedData) + }) + + t.Run("data keys cache should be invalidated", func(t *testing.T) { + // Decrypt to ensure data key is cached + _, err := svc.Decrypt(ctx, ciphertext) + require.NoError(t, err) + require.NotEmpty(t, svc.dataKeyCache) + + err = svc.ReEncryptDataKeys(ctx) + require.NoError(t, err) + + assert.Empty(t, svc.dataKeyCache) + }) +} diff --git a/pkg/services/secrets/secrets.go b/pkg/services/secrets/secrets.go index d1d3ee3cf68..f4b2fa7cc8f 100644 --- a/pkg/services/secrets/secrets.go +++ b/pkg/services/secrets/secrets.go @@ -24,6 +24,8 @@ type Service interface { DecryptJsonData(ctx context.Context, sjd map[string][]byte) (map[string]string, error) GetDecryptedValue(ctx context.Context, sjd map[string][]byte, key, fallback string) string + + ReEncryptDataKeys(ctx context.Context) error } // Store defines methods to interact with secrets storage @@ -33,6 +35,7 @@ type Store interface { CreateDataKey(ctx context.Context, dataKey DataKey) error CreateDataKeyWithDBSession(ctx context.Context, dataKey DataKey, sess *xorm.Session) error DeleteDataKey(ctx context.Context, name string) error + ReEncryptDataKeys(ctx context.Context, providers map[ProviderID]Provider, currProvider ProviderID) error } // Provider is a key encryption key provider for envelope encryption