Encryption: Add support for data keys re-encryption (#43548)

* Encryption: Add support for data keys re-encryption

* Add tests for data keys re-encryption

* Update code after refactorings

Co-authored-by: Leonard Gram <leo@xlson.com>
This commit is contained in:
Joan López de la Franca Beltran 2022-02-03 09:15:38 +01:00 committed by GitHub
parent 16f0c6617a
commit b2655750e8
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 124 additions and 0 deletions

View File

@ -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),
},
},
},
}

View File

@ -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())
}

View File

@ -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
})
}

View File

@ -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"
}

View File

@ -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
}

View File

@ -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.

View File

@ -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)
})
}

View File

@ -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