Encryption: Add support for multiple data keys per day (#47765)

* Add database migrations

* Use short uids as data key ids

* Add support for manual data key rotation

* Fix duplicated mutex unlocks

* Fix migration

* Manage current data keys per name

* Adjust key re-encryption and test

* Modify rename column migration for MySQL compatibility

* Refactor secrets manager and data keys cache

* Multiple o11y adjustments

* Fix stats query

* Apply suggestions from code review

Co-authored-by: Tania <yalyna.ts@gmail.com>

* Fix linter

* Docs: Rotate data encryption keys API endpoint

Co-authored-by: Tania <yalyna.ts@gmail.com>
This commit is contained in:
Joan López de la Franca Beltran
2022-05-23 13:13:55 +02:00
committed by GitHub
parent ae8c11bfa4
commit e43879e55d
21 changed files with 498 additions and 170 deletions

View File

@@ -704,3 +704,28 @@ Content-Type: application/json
"message": "LDAP config reloaded"
}
```
## Rotate data encryption keys
`POST /api/admin/encryption/rotate-data-keys`
Rotates data encryption keys, so all the active keys are disabled
and no longer used for encryption but kept for decryption operations.
Secrets encrypted with one of the deactivated keys need to be re-encrypted
to actually stop using those keys for both encryption and decryption.
**Example Request**:
```http
POST /api/admin/encryption/rotate-data-keys HTTP/1.1
Accept: application/json
Content-Type: application/json
```
**Example Response**:
```http
HTTP/1.1 204
Content-Type: application/json
```

View File

@@ -0,0 +1,16 @@
package api
import (
"net/http"
"github.com/grafana/grafana/pkg/api/response"
"github.com/grafana/grafana/pkg/models"
)
func (hs *HTTPServer) AdminRotateDataEncryptionKeys(c *models.ReqContext) response.Response {
if err := hs.SecretsService.RotateDataKeys(c.Req.Context()); err != nil {
return response.Error(http.StatusInternalServerError, "Failed to rotate data key", err)
}
return response.Respond(http.StatusNoContent, "")
}

View File

@@ -558,6 +558,8 @@ func (hs *HTTPServer) registerRoutes() {
adminRoute.Post("/export", reqGrafanaAdmin, routing.Wrap(hs.ExportService.HandleRequestExport))
}
adminRoute.Post("/encryption/rotate-data-keys", reqGrafanaAdmin, routing.Wrap(hs.AdminRotateDataEncryptionKeys))
adminRoute.Post("/provisioning/dashboards/reload", authorize(reqGrafanaAdmin, ac.EvalPermission(ActionProvisioningReload, ScopeProvisionersDashboards)), routing.Wrap(hs.AdminProvisioningReloadDashboards))
adminRoute.Post("/provisioning/plugins/reload", authorize(reqGrafanaAdmin, ac.EvalPermission(ActionProvisioningReload, ScopeProvisionersPlugins)), routing.Wrap(hs.AdminProvisioningReloadPlugins))
adminRoute.Post("/provisioning/datasources/reload", authorize(reqGrafanaAdmin, ac.EvalPermission(ActionProvisioningReload, ScopeProvisionersDatasources)), routing.Wrap(hs.AdminProvisioningReloadDatasources))

View File

@@ -192,7 +192,7 @@ var (
StatsTotalLibraryVariables prometheus.Gauge
// StatsTotalDataKeys is a metric of total number of data keys stored in Grafana.
StatsTotalDataKeys prometheus.Gauge
StatsTotalDataKeys *prometheus.GaugeVec
)
func init() {
@@ -568,11 +568,11 @@ func init() {
Namespace: ExporterName,
})
StatsTotalDataKeys = prometheus.NewGauge(prometheus.GaugeOpts{
StatsTotalDataKeys = prometheus.NewGaugeVec(prometheus.GaugeOpts{
Name: "stat_totals_data_keys",
Help: "total amount of data keys in the database",
Namespace: ExporterName,
})
}, []string{"active"})
}
// SetBuildInformation sets the build information for this binary

View File

@@ -18,6 +18,7 @@ import (
"github.com/grafana/grafana/pkg/services/featuremgmt"
"github.com/grafana/grafana/pkg/services/sqlstore"
"github.com/grafana/grafana/pkg/setting"
"github.com/prometheus/client_golang/prometheus"
)
type Service struct {
@@ -139,6 +140,7 @@ func (s *Service) collect(ctx context.Context) (map[string]interface{}, error) {
m["stats.folders_viewers_can_admin.count"] = statsQuery.Result.FoldersViewersCanAdmin
m["stats.api_keys.count"] = statsQuery.Result.APIKeys
m["stats.data_keys.count"] = statsQuery.Result.DataKeys
m["stats.active_data_keys.count"] = statsQuery.Result.ActiveDataKeys
ossEditionCount := 1
enterpriseEditionCount := 0
@@ -327,7 +329,10 @@ func (s *Service) updateTotalStats(ctx context.Context) bool {
metrics.StatsTotalAlertRules.Set(float64(statsQuery.Result.AlertRules))
metrics.StatsTotalLibraryPanels.Set(float64(statsQuery.Result.LibraryPanels))
metrics.StatsTotalLibraryVariables.Set(float64(statsQuery.Result.LibraryVariables))
metrics.StatsTotalDataKeys.Set(float64(statsQuery.Result.DataKeys))
metrics.StatsTotalDataKeys.With(prometheus.Labels{"active": "true"}).Set(float64(statsQuery.Result.ActiveDataKeys))
inactiveDataKeys := statsQuery.Result.DataKeys - statsQuery.Result.ActiveDataKeys
metrics.StatsTotalDataKeys.With(prometheus.Labels{"active": "false"}).Set(float64(inactiveDataKeys))
dsStats := models.GetDataSourceStatsQuery{}
if err := s.sqlstore.GetDataSourceStats(ctx, &dsStats); err != nil {

View File

@@ -281,6 +281,7 @@ func TestCollectingUsageStats(t *testing.T) {
assert.EqualValues(t, 1, metrics["stats.distributor.hosted-grafana.count"])
assert.EqualValues(t, 11, metrics["stats.data_keys.count"])
assert.EqualValues(t, 3, metrics["stats.active_data_keys.count"])
assert.InDelta(t, int64(65), metrics["stats.uptime"], 6)
}
@@ -325,6 +326,7 @@ func mockSystemStats(sqlStore *mockstore.SQLStoreMock) {
FoldersViewersCanEdit: 5,
APIKeys: 2,
DataKeys: 11,
ActiveDataKeys: 3,
}
}

View File

@@ -40,6 +40,7 @@ type SystemStats struct {
DailyActiveViewers int64
DailyActiveSessions int64
DataKeys int64
ActiveDataKeys int64
}
type DataSourceStats struct {

View File

@@ -26,7 +26,30 @@ func ProvideSecretsStore(sqlStore *sqlstore.SQLStore) *SecretsStoreImpl {
}
}
func (ss *SecretsStoreImpl) GetDataKey(ctx context.Context, name string) (*secrets.DataKey, error) {
func (ss *SecretsStoreImpl) GetDataKey(ctx context.Context, id string) (*secrets.DataKey, error) {
dataKey := &secrets.DataKey{}
var exists bool
err := ss.sqlStore.WithDbSession(ctx, func(sess *sqlstore.DBSession) error {
var err error
exists, err = sess.Table(dataKeysTable).
Where("id = ?", id).
Get(dataKey)
return err
})
if !exists {
return nil, secrets.ErrDataKeyNotFound
}
if err != nil {
return nil, fmt.Errorf("failed getting data key: %w", err)
}
return dataKey, nil
}
func (ss *SecretsStoreImpl) GetCurrentDataKey(ctx context.Context, name string) (*secrets.DataKey, error) {
dataKey := &secrets.DataKey{}
var exists bool
@@ -43,7 +66,6 @@ func (ss *SecretsStoreImpl) GetDataKey(ctx context.Context, name string) (*secre
}
if err != nil {
ss.log.Error("Failed to get data key", "err", err, "name", name)
return nil, fmt.Errorf("failed getting data key: %w", err)
}
@@ -59,13 +81,13 @@ func (ss *SecretsStoreImpl) GetAllDataKeys(ctx context.Context) ([]*secrets.Data
return result, err
}
func (ss *SecretsStoreImpl) CreateDataKey(ctx context.Context, dataKey secrets.DataKey) error {
func (ss *SecretsStoreImpl) CreateDataKey(ctx context.Context, dataKey *secrets.DataKey) error {
return ss.sqlStore.WithDbSession(ctx, func(sess *sqlstore.DBSession) error {
return ss.CreateDataKeyWithDBSession(ctx, dataKey, sess.Session)
})
}
func (ss *SecretsStoreImpl) CreateDataKeyWithDBSession(_ context.Context, dataKey secrets.DataKey, sess *xorm.Session) error {
func (ss *SecretsStoreImpl) CreateDataKeyWithDBSession(_ context.Context, dataKey *secrets.DataKey, sess *xorm.Session) error {
if !dataKey.Active {
return fmt.Errorf("cannot insert deactivated data keys")
}
@@ -73,17 +95,26 @@ func (ss *SecretsStoreImpl) CreateDataKeyWithDBSession(_ context.Context, dataKe
dataKey.Created = time.Now()
dataKey.Updated = dataKey.Created
_, err := sess.Table(dataKeysTable).Insert(&dataKey)
_, err := sess.Table(dataKeysTable).Insert(dataKey)
return err
}
func (ss *SecretsStoreImpl) DeleteDataKey(ctx context.Context, name string) error {
if len(name) == 0 {
return fmt.Errorf("data key name is missing")
func (ss *SecretsStoreImpl) DisableDataKeys(ctx context.Context) error {
return ss.sqlStore.WithTransactionalDbSession(ctx, func(sess *sqlstore.DBSession) error {
_, err := sess.Table(dataKeysTable).
Where("active = ?", ss.sqlStore.Dialect.BooleanStr(true)).
UseBool("active").Update(&secrets.DataKey{Active: false})
return err
})
}
func (ss *SecretsStoreImpl) DeleteDataKey(ctx context.Context, id string) error {
if len(id) == 0 {
return fmt.Errorf("data key id is missing")
}
return ss.sqlStore.WithDbSession(ctx, func(sess *sqlstore.DBSession) error {
_, err := sess.Table(dataKeysTable).Delete(&secrets.DataKey{Name: name})
_, err := sess.Table(dataKeysTable).Delete(&secrets.DataKey{Id: id})
return err
})
@@ -94,60 +125,71 @@ func (ss *SecretsStoreImpl) ReEncryptDataKeys(
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
}
keys := make([]*secrets.DataKey, 0)
if err := ss.sqlStore.NewSession(ctx).Table(dataKeysTable).Find(&keys); err != nil {
return err
}
for _, k := range keys {
for _, k := range keys {
err := ss.sqlStore.WithTransactionalDbSession(ctx, func(sess *sqlstore.DBSession) error {
provider, ok := providers[kmsproviders.NormalizeProviderID(k.Provider)]
if !ok {
ss.log.Warn(
"Could not find provider to re-encrypt data encryption key",
"key_id", k.Name,
"id", k.Id,
"name", k.Name,
"provider", k.Provider,
)
continue
return nil
}
decrypted, err := provider.Decrypt(ctx, k.EncryptedData)
if err != nil {
ss.log.Warn(
"Error while decrypting data encryption key to re-encrypt it",
"key_id", k.Name,
"id", k.Id,
"name", k.Name,
"provider", k.Provider,
"err", err,
)
continue
return nil
}
// 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.Name = secrets.KeyName(k.Scope, currProvider)
k.Updated = time.Now()
k.EncryptedData, err = providers[currProvider].Encrypt(ctx, decrypted)
if err != nil {
ss.log.Warn(
"Error while re-encrypting data encryption key",
"key_id", k.Name,
"id", k.Id,
"name", k.Name,
"provider", k.Provider,
"err", err,
)
continue
return nil
}
if _, err := sess.Table(dataKeysTable).Where("name = ?", k.Name).Update(k); err != nil {
if _, err := sess.Table(dataKeysTable).Where("id = ?", k.Id).Update(k); err != nil {
ss.log.Warn(
"Error while re-encrypting data encryption key",
"key_id", k.Name,
"id", k.Id,
"name", k.Name,
"provider", k.Provider,
"err", err,
)
continue
return nil
}
}
return nil
})
return nil
})
if 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) RotateDataKeys(_ context.Context) error {
return nil
}
func (f FakeSecretsService) ReEncryptDataKeys(_ context.Context) error {
return nil
}

View File

@@ -15,14 +15,25 @@ func NewFakeSecretsStore() FakeSecretsStore {
return FakeSecretsStore{store: make(map[string]*secrets.DataKey)}
}
func (f FakeSecretsStore) GetDataKey(_ context.Context, name string) (*secrets.DataKey, error) {
key, ok := f.store[name]
func (f FakeSecretsStore) GetDataKey(_ context.Context, id string) (*secrets.DataKey, error) {
key, ok := f.store[id]
if !ok {
return nil, secrets.ErrDataKeyNotFound
}
return key, nil
}
func (f FakeSecretsStore) GetCurrentDataKey(_ context.Context, name string) (*secrets.DataKey, error) {
for _, key := range f.store {
if key.Name == name && key.Active {
return key, nil
}
}
return nil, secrets.ErrDataKeyNotFound
}
func (f FakeSecretsStore) GetAllDataKeys(_ context.Context) ([]*secrets.DataKey, error) {
result := make([]*secrets.DataKey, 0)
for _, key := range f.store {
@@ -31,18 +42,25 @@ func (f FakeSecretsStore) GetAllDataKeys(_ context.Context) ([]*secrets.DataKey,
return result, nil
}
func (f FakeSecretsStore) CreateDataKey(_ context.Context, dataKey secrets.DataKey) error {
f.store[dataKey.Name] = &dataKey
func (f FakeSecretsStore) CreateDataKey(_ context.Context, dataKey *secrets.DataKey) error {
f.store[dataKey.Id] = dataKey
return nil
}
func (f FakeSecretsStore) CreateDataKeyWithDBSession(_ context.Context, dataKey secrets.DataKey, _ *xorm.Session) error {
f.store[dataKey.Name] = &dataKey
func (f FakeSecretsStore) CreateDataKeyWithDBSession(_ context.Context, dataKey *secrets.DataKey, _ *xorm.Session) error {
f.store[dataKey.Id] = dataKey
return nil
}
func (f FakeSecretsStore) DeleteDataKey(_ context.Context, name string) error {
delete(f.store, name)
func (f FakeSecretsStore) DisableDataKeys(_ context.Context) error {
for id := range f.store {
f.store[id].Active = false
}
return nil
}
func (f FakeSecretsStore) DeleteDataKey(_ context.Context, id string) error {
delete(f.store, id)
return nil
}

View File

@@ -13,6 +13,8 @@ var (
)
type dataKeyCacheEntry struct {
id string
name string
dataKey []byte
expiration time.Time
}
@@ -22,58 +24,86 @@ func (e dataKeyCacheEntry) expired() bool {
}
type dataKeyCache struct {
sync.RWMutex
entries map[string]dataKeyCacheEntry
mtx sync.RWMutex
byId map[string]*dataKeyCacheEntry
byName map[string]*dataKeyCacheEntry
cacheTTL time.Duration
}
func newDataKeyCache(ttl time.Duration) *dataKeyCache {
return &dataKeyCache{
entries: make(map[string]dataKeyCacheEntry),
byId: make(map[string]*dataKeyCacheEntry),
byName: make(map[string]*dataKeyCacheEntry),
cacheTTL: ttl,
}
}
func (c *dataKeyCache) get(id string) ([]byte, bool) {
c.RLock()
defer c.RUnlock()
func (c *dataKeyCache) getById(id string) (*dataKeyCacheEntry, bool) {
c.mtx.RLock()
defer c.mtx.RUnlock()
entry, exists := c.entries[id]
entry, exists := c.byId[id]
cacheReadsCounter.With(prometheus.Labels{
"hit": strconv.FormatBool(exists),
"hit": strconv.FormatBool(exists),
"method": "byId",
}).Inc()
if !exists || entry.expired() {
return nil, false
}
return entry.dataKey, true
return entry, true
}
func (c *dataKeyCache) add(id string, dataKey []byte) {
c.Lock()
defer c.Unlock()
func (c *dataKeyCache) getByName(name string) (*dataKeyCacheEntry, bool) {
c.mtx.RLock()
defer c.mtx.RUnlock()
c.entries[id] = dataKeyCacheEntry{
dataKey: dataKey,
expiration: now().Add(c.cacheTTL),
entry, exists := c.byName[name]
cacheReadsCounter.With(prometheus.Labels{
"hit": strconv.FormatBool(exists),
"method": "byName",
}).Inc()
if !exists || entry.expired() {
return nil, false
}
return entry, true
}
func (c *dataKeyCache) add(entry *dataKeyCacheEntry) {
c.mtx.Lock()
defer c.mtx.Unlock()
entry.expiration = now().Add(c.cacheTTL)
c.byId[entry.id] = entry
c.byName[entry.name] = entry
}
func (c *dataKeyCache) removeExpired() {
c.Lock()
defer c.Unlock()
c.mtx.Lock()
defer c.mtx.Unlock()
for id, entry := range c.entries {
for id, entry := range c.byId {
if entry.expired() {
delete(c.entries, id)
delete(c.byId, id)
}
}
for name, entry := range c.byName {
if entry.expired() {
delete(c.byName, name)
}
}
}
func (c *dataKeyCache) flush() {
c.Lock()
c.entries = make(map[string]dataKeyCacheEntry)
c.Unlock()
c.mtx.Lock()
c.byId = make(map[string]*dataKeyCacheEntry)
c.byName = make(map[string]*dataKeyCacheEntry)
c.mtx.Unlock()
}

View File

@@ -8,6 +8,7 @@ import (
"errors"
"fmt"
"strconv"
"sync"
"time"
"github.com/grafana/grafana/pkg/infra/log"
@@ -17,6 +18,7 @@ import (
"github.com/grafana/grafana/pkg/services/kmsproviders"
"github.com/grafana/grafana/pkg/services/secrets"
"github.com/grafana/grafana/pkg/setting"
"github.com/grafana/grafana/pkg/util"
"github.com/prometheus/client_golang/prometheus"
"golang.org/x/sync/errgroup"
"xorm.io/xorm"
@@ -29,10 +31,13 @@ type SecretsService struct {
features featuremgmt.FeatureToggles
usageStats usagestats.Service
currentProviderID secrets.ProviderID
mtx sync.Mutex
dataKeyCache *dataKeyCache
providers map[secrets.ProviderID]secrets.Provider
dataKeyCache *dataKeyCache
log log.Logger
currentProviderID secrets.ProviderID
log log.Logger
}
func ProvideSecretsService(
@@ -62,10 +67,9 @@ func ProvideSecretsService(
logger.Warn("Changing encryption provider requires enabling envelope encryption feature")
}
logger.Debug("Envelope encryption state", "enabled", enabled, "current provider", currentProviderID)
logger.Info("Envelope encryption state", "enabled", enabled, "current provider", currentProviderID)
ttl := settings.KeyValue("security.encryption", "data_keys_cache_ttl").MustDuration(15 * time.Minute)
cache := newDataKeyCache(ttl)
s := &SecretsService{
store: store,
@@ -73,8 +77,8 @@ func ProvideSecretsService(
settings: settings,
usageStats: usageStats,
providers: providers,
dataKeyCache: newDataKeyCache(ttl),
currentProviderID: currentProviderID,
dataKeyCache: cache,
features: features,
log: logger,
}
@@ -142,29 +146,23 @@ func (s *SecretsService) EncryptWithDBSession(ctx context.Context, payload []byt
// If encryption featuremgmt.FlagEnvelopeEncryption toggle is on, use envelope encryption
scope := opt()
keyName := s.keyName(scope)
name := secrets.KeyName(scope, s.currentProviderID)
var dataKey []byte
dataKey, err = s.dataKey(ctx, keyName)
id, dataKey, err := s.currentDataKey(ctx, name, scope, sess)
if err != nil {
if errors.Is(err, secrets.ErrDataKeyNotFound) {
dataKey, err = s.newDataKey(ctx, keyName, scope, sess)
if err != nil {
return nil, err
}
} else {
return nil, err
}
s.log.Error("Failed to get current data key", "error", err, "name", name)
return nil, err
}
var encrypted []byte
encrypted, err = s.enc.Encrypt(ctx, payload, string(dataKey))
if err != nil {
s.log.Error("Failed to encrypt secret", "error", err)
return nil, err
}
prefix := make([]byte, b64.EncodedLen(len(keyName))+2)
b64.Encode(prefix[1:], []byte(keyName))
prefix := make([]byte, b64.EncodedLen(len(id))+2)
b64.Encode(prefix[1:], []byte(id))
prefix[0] = '#'
prefix[len(prefix)-1] = '#'
@@ -175,8 +173,121 @@ func (s *SecretsService) EncryptWithDBSession(ctx context.Context, payload []byt
return blob, nil
}
func (s *SecretsService) keyName(scope string) string {
return fmt.Sprintf("%s/%s@%s", now().Format("2006-01-02"), scope, s.currentProviderID)
// currentDataKey looks up for current data key in cache or database by name, and decrypts it.
// If there's no current data key in cache nor in database it generates a new random data key,
// and stores it into both the in-memory cache and database (encrypted by the encryption provider).
func (s *SecretsService) currentDataKey(ctx context.Context, name string, scope string, sess *xorm.Session) (string, []byte, error) {
// We want only one request fetching current data key at time to
// avoid the creation of multiple ones in case there's no one existing.
s.mtx.Lock()
defer s.mtx.Unlock()
// We try to fetch the data key, either from cache or database
id, dataKey, err := s.dataKeyByName(ctx, name)
if err != nil {
return "", nil, err
}
// If no existing data key was found, create a new one
if dataKey == nil {
id, dataKey, err = s.newDataKey(ctx, name, scope, sess)
if err != nil {
return "", nil, err
}
}
return id, dataKey, nil
}
// dataKeyByName looks up for data key in cache.
// Otherwise, it fetches it from database, decrypts it and caches it decrypted.
func (s *SecretsService) dataKeyByName(ctx context.Context, name string) (string, []byte, error) {
// 0. Get data key from in-memory cache.
if entry, exists := s.dataKeyCache.getByName(name); exists {
return entry.id, entry.dataKey, nil
}
// 1. Get data key from database.
dataKey, err := s.store.GetCurrentDataKey(ctx, name)
if err != nil {
if errors.Is(err, secrets.ErrDataKeyNotFound) {
return "", nil, nil
}
return "", nil, err
}
// 2.1 Find the encryption provider.
provider, exists := s.providers[kmsproviders.NormalizeProviderID(dataKey.Provider)]
if !exists {
return "", nil, fmt.Errorf("could not find encryption provider '%s'", dataKey.Provider)
}
// 2.2 Decrypt the data key fetched from the database.
decrypted, err := provider.Decrypt(ctx, dataKey.EncryptedData)
if err != nil {
return "", nil, err
}
// 3. Store the decrypted data key into the in-memory cache.
s.dataKeyCache.add(&dataKeyCacheEntry{id: dataKey.Id, name: dataKey.Name, dataKey: decrypted})
return dataKey.Id, decrypted, nil
}
// newDataKey creates a new random data key, encrypts it and stores it into the database and cache.
func (s *SecretsService) newDataKey(ctx context.Context, name string, scope string, sess *xorm.Session) (string, []byte, error) {
// 1. Create new data key.
dataKey, err := newRandomDataKey()
if err != nil {
return "", nil, err
}
// 2.1 Find the encryption provider.
provider, exists := s.providers[s.currentProviderID]
if !exists {
return "", nil, fmt.Errorf("could not find encryption provider '%s'", s.currentProviderID)
}
// 2.2 Encrypt the data key.
encrypted, err := provider.Encrypt(ctx, dataKey)
if err != nil {
return "", nil, err
}
// 3. Store its encrypted value into the DB.
id := util.GenerateShortUID()
dbDataKey := secrets.DataKey{
Id: id,
Active: true,
Name: name,
Provider: s.currentProviderID,
EncryptedData: encrypted,
Scope: scope,
}
if sess == nil {
err = s.store.CreateDataKey(ctx, &dbDataKey)
} else {
err = s.store.CreateDataKeyWithDBSession(ctx, &dbDataKey, sess)
}
if err != nil {
return "", nil, err
}
// 4. Store the decrypted data key into the in-memory cache.
s.dataKeyCache.add(&dataKeyCacheEntry{id: id, name: name, dataKey: dataKey})
return id, dataKey, nil
}
func newRandomDataKey() ([]byte, error) {
rawDataKey := make([]byte, 16)
_, err := rand.Read(rawDataKey)
if err != nil {
return nil, err
}
return rawDataKey, nil
}
func (s *SecretsService) Decrypt(ctx context.Context, payload []byte) ([]byte, error) {
@@ -192,6 +303,10 @@ func (s *SecretsService) Decrypt(ctx context.Context, payload []byte) ([]byte, e
"success": strconv.FormatBool(err == nil),
"operation": OpDecrypt,
}).Inc()
if err != nil {
s.log.Error("Failed to decrypt secret", "error", err)
}
}()
if len(payload) == 0 {
@@ -208,20 +323,20 @@ func (s *SecretsService) Decrypt(ctx context.Context, payload []byte) ([]byte, e
payload = payload[1:]
endOfKey := bytes.Index(payload, []byte{'#'})
if endOfKey == -1 {
err = fmt.Errorf("could not find valid key in encrypted payload")
err = fmt.Errorf("could not find valid key id in encrypted payload")
return nil, err
}
b64Key := payload[:endOfKey]
payload = payload[endOfKey+1:]
key := make([]byte, b64.DecodedLen(len(b64Key)))
_, err = b64.Decode(key, b64Key)
keyId := make([]byte, b64.DecodedLen(len(b64Key)))
_, err = b64.Decode(keyId, b64Key)
if err != nil {
return nil, err
}
dataKey, err = s.dataKey(ctx, string(key))
dataKey, err = s.dataKeyById(ctx, string(keyId))
if err != nil {
s.log.Error("Failed to lookup data key", "name", string(key), "error", err)
s.log.Error("Failed to lookup data key by id", "id", string(keyId), "error", err)
return nil, err
}
}
@@ -275,83 +390,34 @@ func (s *SecretsService) GetDecryptedValue(ctx context.Context, sjd map[string][
return fallback
}
func newRandomDataKey() ([]byte, error) {
rawDataKey := make([]byte, 16)
_, err := rand.Read(rawDataKey)
if err != nil {
return nil, err
}
return rawDataKey, nil
}
// newDataKey creates a new random DEK, caches it and returns its value
func (s *SecretsService) newDataKey(ctx context.Context, name string, scope string, sess *xorm.Session) ([]byte, error) {
// 1. Create new DEK
dataKey, err := newRandomDataKey()
if err != nil {
return nil, err
}
provider, exists := s.providers[s.currentProviderID]
if !exists {
return nil, fmt.Errorf("could not find encryption provider '%s'", s.currentProviderID)
// dataKeyById looks up for data key in cache.
// Otherwise, it fetches it from database and returns it decrypted.
func (s *SecretsService) dataKeyById(ctx context.Context, id string) ([]byte, error) {
// 0. Get decrypted data key from in-memory cache.
if entry, exists := s.dataKeyCache.getById(id); exists {
return entry.dataKey, nil
}
// 2. Encrypt it
encrypted, err := provider.Encrypt(ctx, dataKey)
// 1. Get encrypted data key from database.
dataKey, err := s.store.GetDataKey(ctx, id)
if err != nil {
return nil, err
}
// 3. Store its encrypted value in db
dek := secrets.DataKey{
Active: true, // TODO: right now we never mark a key as deactivated
Name: name,
Provider: s.currentProviderID,
EncryptedData: encrypted,
Scope: scope,
}
if sess == nil {
err = s.store.CreateDataKey(ctx, dek)
} else {
err = s.store.CreateDataKeyWithDBSession(ctx, dek, sess)
}
if err != nil {
return nil, err
}
// 4. Cache its unencrypted value and return it
s.dataKeyCache.add(name, dataKey)
return dataKey, nil
}
// dataKey looks up DEK in cache or database, and decrypts it
func (s *SecretsService) dataKey(ctx context.Context, name string) ([]byte, error) {
if dataKey, exists := s.dataKeyCache.get(name); exists {
return dataKey, nil
}
// 1. get encrypted data key from database
dataKey, err := s.store.GetDataKey(ctx, name)
if err != nil {
return nil, err
}
// 2. decrypt data key
// 2.1. Find the encryption provider.
provider, exists := s.providers[kmsproviders.NormalizeProviderID(dataKey.Provider)]
if !exists {
return nil, fmt.Errorf("could not find encryption provider '%s'", dataKey.Provider)
}
// 2.2. Encrypt the data key.
decrypted, err := provider.Decrypt(ctx, dataKey.EncryptedData)
if err != nil {
return nil, err
}
// 3. cache data key
s.dataKeyCache.add(name, decrypted)
// 3. Store the decrypted data key into the in-memory cache.
s.dataKeyCache.add(&dataKeyCacheEntry{id: id, name: dataKey.Name, dataKey: decrypted})
return decrypted, nil
}
@@ -360,17 +426,38 @@ 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)
func (s *SecretsService) RotateDataKeys(ctx context.Context) error {
s.log.Info("Data keys rotation triggered, acquiring lock...")
s.mtx.Lock()
defer s.mtx.Unlock()
s.log.Info("Data keys rotation started")
err := s.store.DisableDataKeys(ctx)
if err != nil {
s.log.Error("Data keys rotation failed", "error", err)
return err
}
s.dataKeyCache.flush()
s.log.Info("Data keys rotation finished successfully")
return nil
}
func (s *SecretsService) ReEncryptDataKeys(ctx context.Context) error {
s.log.Info("Data keys re-encryption triggered")
err := s.store.ReEncryptDataKeys(ctx, s.providers, s.currentProviderID)
if err != nil {
s.log.Error("Data keys re-encryption failed", "error", err)
return err
}
s.dataKeyCache.flush()
s.log.Info("Data keys re-encryption finished successfully")
return nil
}
func (s *SecretsService) Run(ctx context.Context) error {
gc := time.NewTicker(
s.settings.KeyValue("security.encryption", "data_keys_cache_cleanup_interval").
@@ -390,11 +477,11 @@ func (s *SecretsService) Run(ctx context.Context) error {
for {
select {
case <-gc.C:
s.log.Debug("removing expired data encryption keys from cache...")
s.log.Debug("Removing expired data keys from cache...")
s.dataKeyCache.removeExpired()
s.log.Debug("done removing expired data encryption keys from cache")
s.log.Debug("Removing expired data keys from cache finished successfully")
case <-gCtx.Done():
s.log.Debug("grafana is shutting down; stopping...")
s.log.Debug("Grafana is shutting down; stopping...")
gc.Stop()
if err := grp.Wait(); err != nil && !errors.Is(err, context.Canceled) {

View File

@@ -13,6 +13,7 @@ import (
"github.com/grafana/grafana/pkg/services/secrets/database"
"github.com/grafana/grafana/pkg/services/sqlstore"
"github.com/grafana/grafana/pkg/setting"
"github.com/grafana/grafana/pkg/util"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"gopkg.in/ini.v1"
@@ -97,7 +98,8 @@ func TestSecretsService_DataKeys(t *testing.T) {
store := database.ProvideSecretsStore(sqlstore.InitTestDB(t))
ctx := context.Background()
dataKey := secrets.DataKey{
dataKey := &secrets.DataKey{
Id: util.GenerateShortUID(),
Active: true,
Name: "test1",
Provider: "test",
@@ -105,7 +107,7 @@ func TestSecretsService_DataKeys(t *testing.T) {
}
t.Run("querying for a DEK that does not exist", func(t *testing.T) {
res, err := store.GetDataKey(ctx, dataKey.Name)
res, err := store.GetDataKey(ctx, dataKey.Id)
assert.ErrorIs(t, secrets.ErrDataKeyNotFound, err)
assert.Nil(t, res)
})
@@ -114,16 +116,26 @@ func TestSecretsService_DataKeys(t *testing.T) {
err := store.CreateDataKey(ctx, dataKey)
require.NoError(t, err)
res, err := store.GetDataKey(ctx, dataKey.Name)
res, err := store.GetDataKey(ctx, dataKey.Id)
require.NoError(t, err)
assert.Equal(t, dataKey.EncryptedData, res.EncryptedData)
assert.Equal(t, dataKey.Provider, res.Provider)
assert.Equal(t, dataKey.Name, res.Name)
assert.Equal(t, dataKey.Id, res.Id)
assert.True(t, dataKey.Active)
current, err := store.GetCurrentDataKey(ctx, dataKey.Name)
require.NoError(t, err)
assert.Equal(t, dataKey.EncryptedData, current.EncryptedData)
assert.Equal(t, dataKey.Provider, current.Provider)
assert.Equal(t, dataKey.Name, current.Name)
assert.Equal(t, dataKey.Id, current.Id)
assert.True(t, current.Active)
})
t.Run("creating an inactive DEK", func(t *testing.T) {
k := secrets.DataKey{
k := &secrets.DataKey{
Id: util.GenerateShortUID(),
Active: false,
Name: "test2",
Provider: "test",
@@ -138,7 +150,7 @@ func TestSecretsService_DataKeys(t *testing.T) {
assert.Nil(t, res)
})
t.Run("deleting DEK when no name provided must fail", func(t *testing.T) {
t.Run("deleting DEK when no id provided must fail", func(t *testing.T) {
beforeDelete, err := store.GetAllDataKeys(ctx)
require.NoError(t, err)
err = store.DeleteDataKey(ctx, "")
@@ -150,10 +162,10 @@ func TestSecretsService_DataKeys(t *testing.T) {
})
t.Run("deleting a DEK", func(t *testing.T) {
err := store.DeleteDataKey(ctx, dataKey.Name)
err := store.DeleteDataKey(ctx, dataKey.Id)
require.NoError(t, err)
res, err := store.GetDataKey(ctx, dataKey.Name)
res, err := store.GetDataKey(ctx, dataKey.Id)
assert.Equal(t, secrets.ErrDataKeyNotFound, err)
assert.Nil(t, res)
})
@@ -280,7 +292,8 @@ func TestSecretsService_Run(t *testing.T) {
require.NoError(t, err)
// Data encryption key cache should contain one element
require.Len(t, svc.dataKeyCache.entries, 1)
require.Len(t, svc.dataKeyCache.byId, 1)
require.Len(t, svc.dataKeyCache.byName, 1)
t.Cleanup(func() { now = time.Now })
now = func() time.Time { return time.Now().Add(10 * time.Minute) }
@@ -294,7 +307,8 @@ func TestSecretsService_Run(t *testing.T) {
// Then, once the ticker has been triggered,
// the cleanup process should have happened,
// therefore the cache should be empty.
require.Len(t, svc.dataKeyCache.entries, 0)
require.Len(t, svc.dataKeyCache.byId, 0)
require.Len(t, svc.dataKeyCache.byName, 0)
})
}
@@ -328,11 +342,13 @@ func TestSecretsService_ReEncryptDataKeys(t *testing.T) {
// Decrypt to ensure data key is cached
_, err := svc.Decrypt(ctx, ciphertext)
require.NoError(t, err)
require.NotEmpty(t, svc.dataKeyCache.entries)
require.NotEmpty(t, svc.dataKeyCache.byId)
require.NotEmpty(t, svc.dataKeyCache.byName)
err = svc.ReEncryptDataKeys(ctx)
require.NoError(t, err)
assert.Empty(t, svc.dataKeyCache.entries)
assert.Empty(t, svc.dataKeyCache.byId)
assert.Empty(t, svc.dataKeyCache.byName)
})
}

View File

@@ -25,7 +25,7 @@ var (
Name: "encryption_cache_reads_total",
Help: "A counter for encryption cache reads",
},
[]string{"hit"},
[]string{"hit", "method"},
)
)

View File

@@ -4,6 +4,7 @@ import (
"context"
"fmt"
"strings"
"time"
"xorm.io/xorm"
)
@@ -25,16 +26,19 @@ type Service interface {
GetDecryptedValue(ctx context.Context, sjd map[string][]byte, key, fallback string) string
RotateDataKeys(ctx context.Context) error
ReEncryptDataKeys(ctx context.Context) error
}
// Store defines methods to interact with secrets storage
type Store interface {
GetDataKey(ctx context.Context, name string) (*DataKey, error)
GetDataKey(ctx context.Context, id string) (*DataKey, error)
GetCurrentDataKey(ctx context.Context, name string) (*DataKey, error)
GetAllDataKeys(ctx context.Context) ([]*DataKey, error)
CreateDataKey(ctx context.Context, dataKey DataKey) error
CreateDataKeyWithDBSession(ctx context.Context, dataKey DataKey, sess *xorm.Session) error
DeleteDataKey(ctx context.Context, name string) error
CreateDataKey(ctx context.Context, dataKey *DataKey) error
CreateDataKeyWithDBSession(ctx context.Context, dataKey *DataKey, sess *xorm.Session) error
DisableDataKeys(ctx context.Context) error
DeleteDataKey(ctx context.Context, id string) error
ReEncryptDataKeys(ctx context.Context, providers map[ProviderID]Provider, currProvider ProviderID) error
}
@@ -57,6 +61,10 @@ func (id ProviderID) Kind() (string, error) {
return parts[0], nil
}
func KeyName(scope string, providerID ProviderID) string {
return fmt.Sprintf("%s/%s@%s", time.Now().Format("2006-01-02"), scope, providerID)
}
// BackgroundProvider should be implemented for a provider that has a task that needs to be run in the background.
type BackgroundProvider interface {
Run(ctx context.Context) error

View File

@@ -9,6 +9,7 @@ var ErrDataKeyNotFound = errors.New("data key not found")
type DataKey struct {
Active bool
Id string
Name string
Scope string
Provider ProviderID

View File

@@ -1,6 +1,10 @@
package migrations
import "github.com/grafana/grafana/pkg/services/sqlstore/migrator"
import (
"fmt"
"github.com/grafana/grafana/pkg/services/sqlstore/migrator"
)
func addSecretsMigration(mg *migrator.Migrator) {
dataKeysV1 := migrator.Table{
@@ -38,4 +42,23 @@ func addSecretsMigration(mg *migrator.Migrator) {
}
mg.AddMigration("create secrets table", migrator.NewAddTableMigration(secretsV1))
mg.AddMigration("rename data_keys name column to id", migrator.NewRenameColumnMigration(
dataKeysV1, "name", "id",
))
mg.AddMigration("add name column into data_keys", migrator.NewAddColumnMigration(
dataKeysV1,
&migrator.Column{
Name: "name",
Type: migrator.DB_NVarchar,
Length: 100,
Default: "''",
Nullable: false,
},
))
mg.AddMigration("copy data_keys id column values into name", migrator.NewRawSQLMigration(
fmt.Sprintf("UPDATE %s SET %s = %s", dataKeysV1.Name, "name", "id"),
))
}

View File

@@ -37,6 +37,8 @@ type Dialect interface {
DropIndexSQL(tableName string, index *Index) string
RenameTable(oldName string, newName string) string
RenameColumn(table Table, oldName, newName string) string
UpdateTableSQL(tableName string, columns []*Column) string
IndexCheckSQL(tableName, indexName string) (string, []interface{})
@@ -209,6 +211,11 @@ func (b *BaseDialect) RenameTable(oldName string, newName string) string {
return fmt.Sprintf("ALTER TABLE %s RENAME TO %s", quote(oldName), quote(newName))
}
func (b *BaseDialect) RenameColumn(table Table, oldName, newName string) string {
quote := b.dialect.Quote
return fmt.Sprintf("ALTER TABLE %s RENAME COLUMN %s TO %s", quote(table.Name), quote(oldName), quote(newName))
}
func (b *BaseDialect) ColumnCheckSQL(tableName, columnName string) (string, []interface{}) {
return "", nil
}

View File

@@ -108,6 +108,32 @@ func (m *AddColumnMigration) SQL(dialect Dialect) string {
return dialect.AddColumnSQL(m.tableName, m.column)
}
type RenameColumnMigration struct {
MigrationBase
table Table
oldName string
newName string
}
func NewRenameColumnMigration(table Table, oldName, newName string) *RenameColumnMigration {
return &RenameColumnMigration{table: table, oldName: oldName, newName: newName}
}
func (m *RenameColumnMigration) Table(table Table) *RenameColumnMigration {
m.table = table
return m
}
func (m *RenameColumnMigration) Rename(oldName string, newName string) *RenameColumnMigration {
m.oldName = oldName
m.newName = newName
return m
}
func (m *RenameColumnMigration) SQL(d Dialect) string {
return d.RenameColumn(m.table, m.oldName, m.newName)
}
type AddIndexMigration struct {
MigrationBase
tableName string

View File

@@ -118,6 +118,20 @@ func (db *MySQLDialect) ColumnCheckSQL(tableName, columnName string) (string, []
return sql, args
}
func (db *MySQLDialect) RenameColumn(table Table, oldName, newName string) string {
var colType string
for _, col := range table.Columns {
if col.Name == oldName {
colType = db.SQLType(col)
break
}
}
quote := db.dialect.Quote
return fmt.Sprintf("ALTER TABLE %s CHANGE %s %s %s", quote(table.Name), quote(oldName), quote(newName), colType)
}
func (db *MySQLDialect) CleanDB() error {
tables, err := db.engine.DBMetas()
if err != nil {

View File

@@ -103,6 +103,7 @@ func (ss *SQLStore) GetSystemStats(ctx context.Context, query *models.GetSystemS
sb.Write(`(SELECT COUNT(id) FROM `+dialect.Quote("library_element")+` WHERE kind = ?) AS library_panels,`, models.PanelElement)
sb.Write(`(SELECT COUNT(id) FROM `+dialect.Quote("library_element")+` WHERE kind = ?) AS library_variables,`, models.VariableElement)
sb.Write(`(SELECT COUNT(*) FROM ` + dialect.Quote("data_keys") + `) AS data_keys,`)
sb.Write(`(SELECT COUNT(*) FROM ` + dialect.Quote("data_keys") + `WHERE active = true) AS active_data_keys,`)
sb.Write(ss.roleCounterSQL(ctx))