SecretsManager: Unify KMS across OSS and Enterprise (#108085)

* everything is compiling

* tests passing

* remove used object

* write a test for secret key upgrades

* misc cleanup

* clean up some wording

* lint issues

* fix a typo

* import hashicorp dependency explicitly

* simplify oss kmsprovider package structure

* consolidate current provider and available providers

* add a new manager configuration test

* fix hashivault import

* fix import issue

* fix unit tests

* Update go.mod

Co-authored-by: Matheus Macabu <macabu@users.noreply.github.com>

---------

Co-authored-by: Matheus Macabu <macabu@users.noreply.github.com>
pull/108152/head
Michael Mandrus 4 days ago committed by GitHub
parent 469f94028f
commit ab51794bdb
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
  1. 10
      conf/defaults.ini
  2. 7
      go.mod
  3. 11
      go.sum
  4. 3
      go.work.sum
  5. 1
      pkg/extensions/enterprise_imports.go
  6. 28
      pkg/registry/apis/secret/encryption/cipher/service/service.go
  7. 13
      pkg/registry/apis/secret/encryption/cipher/service/service_test.go
  8. 12
      pkg/registry/apis/secret/encryption/kmsproviders/grafana_provider.go
  9. 33
      pkg/registry/apis/secret/encryption/kmsproviders/kmsproviders.go
  10. 96
      pkg/registry/apis/secret/encryption/manager/manager.go
  11. 258
      pkg/registry/apis/secret/encryption/manager/manager_test.go
  12. 17
      pkg/registry/apis/secret/encryption/manager/test_helpers.go
  13. 18
      pkg/registry/apis/secret/encryption/secrets.go
  14. 14
      pkg/registry/apis/secret/secretkeeper/secretkeeper_test.go
  15. 17
      pkg/registry/apis/secret/testutils/testutils.go
  16. 4
      pkg/server/wire.go
  17. 123
      pkg/server/wire_gen.go
  18. 2
      pkg/server/wireexts_oss.go
  19. 33
      pkg/setting/setting_secrets_manager.go
  20. 173
      pkg/setting/setting_secrets_manager_test.go

@ -2147,12 +2147,12 @@ alert_rules_state = "paused"
###################################### Secrets Manager ######################################
[secrets_manager]
# Used for signing
# Current key provider used for envelope encryption
encryption_provider = secret_key.v1
[secrets_manager.encryption.secret_key.v1]
# Used to encrypt data keys
secret_key = SW2YcwTIb9zpOOhoPsMm
# Current key provider used for envelope encryption, default to static value specified by secret_key
encryption_provider = secretKey.v1
# List of configured key providers, space separated (Enterprise only): e.g., awskms.v1 azurekv.v1
available_encryption_providers =
################################## Frontend development configuration ###################################
# Warning! Any settings placed in this section will be available on `process.env.frontend_dev_{foo}` within frontend code

@ -194,6 +194,7 @@ require (
go.uber.org/goleak v1.3.0 // @grafana/grafana-search-and-storage
go.uber.org/zap v1.27.0 // @grafana/identity-access-team
gocloud.dev v0.42.0 // @grafana/grafana-app-platform-squad
gocloud.dev/secrets/hashivault v0.42.0 // @grafana/grafana-operator-experience-squad
golang.org/x/crypto v0.39.0 // @grafana/grafana-backend-group
golang.org/x/exp v0.0.0-20250506013437-ce4c2cf36ca6 // @grafana/alerting-backend
golang.org/x/mod v0.25.0 // indirect; @grafana/grafana-backend-group
@ -407,12 +408,17 @@ require (
github.com/hashicorp/go-immutable-radix v1.3.1 // indirect
github.com/hashicorp/go-metrics v0.5.4 // indirect
github.com/hashicorp/go-msgpack/v2 v2.1.2 // indirect
github.com/hashicorp/go-retryablehttp v0.7.7 // indirect
github.com/hashicorp/go-rootcerts v1.0.2 // indirect
github.com/hashicorp/go-secure-stdlib/parseutil v0.2.0 // indirect
github.com/hashicorp/go-secure-stdlib/strutil v0.1.2 // indirect
github.com/hashicorp/go-sockaddr v1.0.7 // indirect
github.com/hashicorp/go-uuid v1.0.3 // indirect
github.com/hashicorp/golang-lru v1.0.2 // indirect
github.com/hashicorp/hcl v1.0.0 // indirect
github.com/hashicorp/memberlist v0.5.2 // indirect
github.com/hashicorp/serf v0.10.2 // indirect
github.com/hashicorp/vault/api v1.16.0 // indirect
github.com/hashicorp/yamux v0.1.1 // indirect
github.com/inconshreveable/mousetrap v1.1.0 // indirect
github.com/invopop/jsonschema v0.13.0 // indirect
@ -501,6 +507,7 @@ require (
github.com/rivo/uniseg v0.4.7 // indirect
github.com/rogpeppe/go-internal v1.14.1 // indirect
github.com/russross/blackfriday/v2 v2.1.0 // indirect
github.com/ryanuber/go-glob v1.0.0 // indirect
github.com/sagikazarmark/locafero v0.7.0 // indirect
github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529 // indirect
github.com/segmentio/asm v1.2.0 // indirect

@ -1719,6 +1719,10 @@ github.com/hashicorp/go-rootcerts v1.0.0/go.mod h1:K6zTfqpRlCUIjkwsN4Z+hiSfzSTQa
github.com/hashicorp/go-rootcerts v1.0.1/go.mod h1:pqUvnprVnM5bf7AOirdbb01K4ccR319Vf4pU3K5EGc8=
github.com/hashicorp/go-rootcerts v1.0.2 h1:jzhAVGtqPKbwpyCPELlgNWhE1znq+qwJtW5Oi2viEzc=
github.com/hashicorp/go-rootcerts v1.0.2/go.mod h1:pqUvnprVnM5bf7AOirdbb01K4ccR319Vf4pU3K5EGc8=
github.com/hashicorp/go-secure-stdlib/parseutil v0.2.0 h1:U+kC2dOhMFQctRfhK0gRctKAPTloZdMU5ZJxaesJ/VM=
github.com/hashicorp/go-secure-stdlib/parseutil v0.2.0/go.mod h1:Ll013mhdmsVDuoIXVfBtvgGJsXDYkTw1kooNcoCXuE0=
github.com/hashicorp/go-secure-stdlib/strutil v0.1.2 h1:kes8mmyCpxJsI7FTwtzRqEy9CdjCtrXrXGuOpxEA7Ts=
github.com/hashicorp/go-secure-stdlib/strutil v0.1.2/go.mod h1:Gou2R9+il93BqX25LAKCLuM+y9U2T4hlwvT1yprcna4=
github.com/hashicorp/go-sockaddr v1.0.0/go.mod h1:7Xibr9yA9JjQq1JpNB2Vw7kxv8xerXegt+ozgdvDeDU=
github.com/hashicorp/go-sockaddr v1.0.6/go.mod h1:uoUUmtwU7n9Dv3O4SNLeFvg0SxQ3lyjsj6+CCykpaxI=
github.com/hashicorp/go-sockaddr v1.0.7 h1:G+pTkSO01HpR5qCxg7lxfsFEZaG+C0VssTy/9dbT+Fw=
@ -1740,6 +1744,7 @@ github.com/hashicorp/golang-lru v1.0.2 h1:dV3g9Z/unq5DpblPpw+Oqcv4dU/1omnb4Ok8iP
github.com/hashicorp/golang-lru v1.0.2/go.mod h1:iADmTwqILo4mZ8BN3D2Q6+9jd8WM5uGBxy+E8yxSoD4=
github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k=
github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM=
github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4=
github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=
github.com/hashicorp/hcl/v2 v2.17.0 h1:z1XvSUyXd1HP10U4lrLg5e0JMVz6CPaJvAgxM0KNZVY=
github.com/hashicorp/hcl/v2 v2.17.0/go.mod h1:gJyW2PTShkJqQBKpAmPO3yxMxIuoXkOF2TpqXzrQyx4=
@ -1760,6 +1765,8 @@ github.com/hashicorp/serf v0.9.6/go.mod h1:TXZNMjZQijwlDvp+r0b63xZ45H7JmCmgg4gpT
github.com/hashicorp/serf v0.9.7/go.mod h1:TXZNMjZQijwlDvp+r0b63xZ45H7JmCmgg4gpTwn9UV4=
github.com/hashicorp/serf v0.10.2 h1:m5IORhuNSjaxeljg5DeQVDlQyVkhRIjJDimbkCa8aAc=
github.com/hashicorp/serf v0.10.2/go.mod h1:T1CmSGfSeGfnfNy/w0odXQUR1rfECGd2Qdsp84DjOiY=
github.com/hashicorp/vault/api v1.16.0 h1:nbEYGJiAPGzT9U4oWgaaB0g+Rj8E59QuHKyA5LhwQN4=
github.com/hashicorp/vault/api v1.16.0/go.mod h1:KhuUhzOD8lDSk29AtzNjgAu2kxRA9jL9NAbkFlqvkBA=
github.com/hashicorp/yamux v0.1.1 h1:yrQxtgseBDrq9Y652vSRDvsKCJKOUD+GzTS4Y0Y8pvE=
github.com/hashicorp/yamux v0.1.1/go.mod h1:CtWFDAQgb7dxtzFs4tWbplKIe2jSi3+5vKbgIO0SLnQ=
github.com/hetznercloud/hcloud-go/v2 v2.19.1 h1:UU/7h3uc/rdgspM8xkQF7wokmwZXePWDXcLqrQRRzzY=
@ -2295,6 +2302,8 @@ github.com/ruudk/golang-pdf417 v0.0.0-20181029194003-1af4ab5afa58/go.mod h1:6lfF
github.com/ruudk/golang-pdf417 v0.0.0-20201230142125-a7e3863a1245/go.mod h1:pQAZKsJ8yyVxGRWYNEm9oFB8ieLgKFnamEyDmSA0BRk=
github.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts=
github.com/ryanuber/columnize v2.1.2+incompatible/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts=
github.com/ryanuber/go-glob v1.0.0 h1:iQh3xXAumdQ+4Ufa5b25cRpC5TYKlno6hsv6Cb3pkBk=
github.com/ryanuber/go-glob v1.0.0/go.mod h1:807d1WSdnB0XRJzKNil9Om6lcp/3a0v4qIHxIXzX/Yc=
github.com/sagikazarmark/crypt v0.6.0/go.mod h1:U8+INwJo3nBv1m6A/8OBXAq7Jnpspk5AxSgDyEQcea8=
github.com/sagikazarmark/locafero v0.7.0 h1:5MqpDsTGNDhY8sGp0Aowyf0qKsPrhewaLSsFaodPcyo=
github.com/sagikazarmark/locafero v0.7.0/go.mod h1:2za3Cg5rMaTMoG/2Ulr9AwtFaIppKXTRYnozin4aB5k=
@ -2634,6 +2643,8 @@ go4.org/netipx v0.0.0-20230125063823-8449b0a6169f h1:ketMxHg+vWm3yccyYiq+uK8D3fR
go4.org/netipx v0.0.0-20230125063823-8449b0a6169f/go.mod h1:tgPU4N2u9RByaTN3NC2p9xOzyFpte4jYwsIIRF7XlSc=
gocloud.dev v0.42.0 h1:qzG+9ItUL3RPB62/Amugws28n+4vGZXEoJEAMfjutzw=
gocloud.dev v0.42.0/go.mod h1:zkaYAapZfQisXOA4bzhsbA4ckiStGQ3Psvs9/OQ5dPM=
gocloud.dev/secrets/hashivault v0.42.0 h1:kPWIIu1AP6ApHf7HwUrzGrwyRJEfTd46JNUNgnbpsFA=
gocloud.dev/secrets/hashivault v0.42.0/go.mod h1:LXprr1XLEAT7BVZ+Y66dJEHQMzDsowIExj5Ktr9HLvM=
golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
golang.org/x/crypto v0.0.0-20181029021203-45a5f77698d3/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
golang.org/x/crypto v0.0.0-20181203042331-505ab145d0a9/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=

@ -982,7 +982,6 @@ github.com/hashicorp/go-msgpack v1.1.5 h1:9byZdVjKTe5mce63pRVNP1L7UAmdHOTEMGehn6
github.com/hashicorp/go-msgpack v1.1.5/go.mod h1:gWVc3sv/wbDmR3rQsj1CAktEZzoz1YNK9NfGLXJ69/4=
github.com/hashicorp/go-syslog v1.0.0 h1:KaodqZuhUoZereWVIYmpUgZysurB1kBLX2j0MwMrUAE=
github.com/hashicorp/go.net v0.0.1 h1:sNCoNyDEvN1xa+X0baata4RdcpKwcMS6DH+xwfqPgjw=
github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4=
github.com/hashicorp/logutils v1.0.0 h1:dLEQVugN8vlakKOUE3ihGLTZJRB4j+M2cdTm/ORI65Y=
github.com/hashicorp/mdns v1.0.5 h1:1M5hW1cunYeoXOqHwEb/GBDDHAFo0Yqb/uz/beC6LbE=
github.com/hashicorp/mdns v1.0.5/go.mod h1:mtBihi+LeNXGtG8L9dX59gAEa12BDtBQSp4v/YAJqrc=
@ -1123,6 +1122,8 @@ github.com/montanaflynn/stats v0.7.1/go.mod h1:etXPPgVO6n31NxCd9KQUMvCM+ve0ruNzt
github.com/mostynb/go-grpc-compression v1.2.3 h1:42/BKWMy0KEJGSdWvzqIyOZ95YcR9mLPqKctH7Uo//I=
github.com/mostynb/go-grpc-compression v1.2.3/go.mod h1:AghIxF3P57umzqM9yz795+y1Vjs47Km/Y2FE6ouQ7Lg=
github.com/nakagami/firebirdsql v0.0.0-20190310045651-3c02a58cfed8 h1:P48LjvUQpTReR3TQRbxSeSBsMXzfK0uol7eRcr7VBYQ=
github.com/natefinch/atomic v1.0.1 h1:ZPYKxkqQOx3KZ+RsbnP/YsgvxWQPGxjC0oBt2AhwV0A=
github.com/natefinch/atomic v1.0.1/go.mod h1:N/D/ELrljoqDyT3rZrsUmtsuzvHkeB/wWjHV22AZRbM=
github.com/natessilva/dag v0.0.0-20180124060714-7194b8dcc5c4 h1:dnMxwus89s86tI8rcGVp2HwZzlz7c5o92VOy7dSckBQ=
github.com/ncw/swift v1.0.53 h1:luHjjTNtekIEvHg5KdAFIBaH7bWfNkefwFnpDffSIks=
github.com/ncw/swift v1.0.53/go.mod h1:23YIA4yWVnGwv2dQlN4bB7egfYX6YLn0Yo/S6zZO/ZM=

@ -28,6 +28,7 @@ import (
_ "github.com/russellhaering/goxmldsig"
_ "github.com/spf13/cobra" // used by the standalone apiserver cli
_ "github.com/stretchr/testify/require"
_ "gocloud.dev/secrets/hashivault"
_ "golang.org/x/time/rate"
_ "k8s.io/api"
_ "k8s.io/apimachinery/pkg/util/httpstream/spdy"

@ -14,20 +14,16 @@ import (
"github.com/grafana/grafana/pkg/registry/apis/secret/encryption"
"github.com/grafana/grafana/pkg/registry/apis/secret/encryption/cipher"
"github.com/grafana/grafana/pkg/registry/apis/secret/encryption/cipher/provider"
"github.com/grafana/grafana/pkg/setting"
)
const (
encryptionAlgorithmDelimiter = '*'
)
// Service must not be used for cipher.
// Use secrets.Service implementing envelope encryption instead.
type Service struct {
type cipherService struct {
tracer trace.Tracer
log log.Logger
cfg *setting.Cfg
usageMetrics usagestats.Service
cipher cipher.Encrypter
@ -35,16 +31,13 @@ type Service struct {
algorithm string
}
func NewEncryptionService(
// ProvideAESGCMCipherService provides an AES-GCM cipher for encryption and decryption.
// It should not be used to encrypt payloads directly, as it is intended to encrypt data keys for envelope encryption.
func ProvideAESGCMCipherService(
tracer trace.Tracer,
usageMetrics usagestats.Service,
cfg *setting.Cfg,
) (*Service, error) {
if cfg.SecretsManagement.SecretKey == "" {
return nil, fmt.Errorf("`[secrets_manager]secret_key` is not set")
}
s := &Service{
) (cipher.Cipher, error) {
s := &cipherService{
tracer: tracer,
log: log.New("encryption"),
@ -55,7 +48,6 @@ func NewEncryptionService(
algorithm: provider.AesGcm,
usageMetrics: usageMetrics,
cfg: cfg,
}
s.registerUsageMetrics()
@ -63,7 +55,7 @@ func NewEncryptionService(
return s, nil
}
func (s *Service) registerUsageMetrics() {
func (s *cipherService) registerUsageMetrics() {
s.usageMetrics.RegisterMetricsFunc(func(context.Context) (map[string]any, error) {
return map[string]any{
fmt.Sprintf("stats.%s.encryption.cipher.%s.count", encryption.UsageInsightsPrefix, s.algorithm): 1,
@ -71,7 +63,7 @@ func (s *Service) registerUsageMetrics() {
})
}
func (s *Service) Decrypt(ctx context.Context, payload []byte, secret string) ([]byte, error) {
func (s *cipherService) Decrypt(ctx context.Context, payload []byte, secret string) ([]byte, error) {
ctx, span := s.tracer.Start(ctx, "CipherService.Decrypt")
defer span.End()
@ -99,7 +91,7 @@ func (s *Service) Decrypt(ctx context.Context, payload []byte, secret string) ([
return decrypted, err
}
func (s *Service) deriveEncryptionAlgorithm(payload []byte) (string, []byte, error) {
func (s *cipherService) deriveEncryptionAlgorithm(payload []byte) (string, []byte, error) {
if len(payload) == 0 {
return "", nil, fmt.Errorf("unable to derive encryption algorithm")
}
@ -120,7 +112,7 @@ func (s *Service) deriveEncryptionAlgorithm(payload []byte) (string, []byte, err
return string(algorithm), payload, nil
}
func (s *Service) Encrypt(ctx context.Context, payload []byte, secret string) ([]byte, error) {
func (s *cipherService) Encrypt(ctx context.Context, payload []byte, secret string) ([]byte, error) {
ctx, span := s.tracer.Start(ctx, "CipherService.Encrypt")
defer span.End()

@ -8,21 +8,14 @@ import (
"go.opentelemetry.io/otel/trace/noop"
"github.com/grafana/grafana/pkg/infra/usagestats"
"github.com/grafana/grafana/pkg/setting"
"github.com/grafana/grafana/pkg/registry/apis/secret/encryption/cipher"
)
func newGcmService(t *testing.T) *Service {
func newGcmService(t *testing.T) cipher.Cipher {
t.Helper()
usageStats := &usagestats.UsageStatsMock{}
settings := &setting.Cfg{
SecretsManagement: setting.SecretsManagerSettings{
SecretKey: "SdlklWklckeLS",
EncryptionProvider: "secretKey.v1",
},
}
svc, err := NewEncryptionService(noop.NewTracerProvider().Tracer("test"), usageStats, settings)
svc, err := ProvideAESGCMCipherService(noop.NewTracerProvider().Tracer("test"), usageStats)
require.NoError(t, err, "failed to set up encryption service")
return svc
}

@ -1,4 +1,4 @@
package defaultprovider
package kmsproviders
import (
"context"
@ -7,22 +7,22 @@ import (
"github.com/grafana/grafana/pkg/registry/apis/secret/encryption/cipher"
)
type grafanaProvider struct {
type secretKeyProvider struct {
sk string
encryption cipher.Cipher
}
func New(sk string, encryption cipher.Cipher) encryption.Provider {
return grafanaProvider{
func newSecretKeyProvider(sk string, encryption cipher.Cipher) encryption.Provider {
return secretKeyProvider{
sk: sk,
encryption: encryption,
}
}
func (p grafanaProvider) Encrypt(ctx context.Context, blob []byte) ([]byte, error) {
func (p secretKeyProvider) Encrypt(ctx context.Context, blob []byte) ([]byte, error) {
return p.encryption.Encrypt(ctx, blob, p.sk)
}
func (p grafanaProvider) Decrypt(ctx context.Context, blob []byte) ([]byte, error) {
func (p secretKeyProvider) Decrypt(ctx context.Context, blob []byte) ([]byte, error) {
return p.encryption.Decrypt(ctx, blob, p.sk)
}

@ -1,19 +1,40 @@
package kmsproviders
import (
"fmt"
"strings"
"github.com/grafana/grafana/pkg/registry/apis/secret/encryption"
"github.com/grafana/grafana/pkg/registry/apis/secret/encryption/cipher"
"github.com/grafana/grafana/pkg/registry/apis/secret/encryption/kmsproviders/defaultprovider"
"github.com/grafana/grafana/pkg/setting"
)
const (
// Default is the identifier of the default kms provider which fallbacks to the configured secret_key
Default = "secretKey.v1"
// OSSProviderType is the identifier of the default kms provider which fallbacks to the configured secret_key
OSSProviderType = "secret_key"
// SecretKeyKey is the key in the section that contains the secret key
SecretKeyKey = "secret_key"
)
func GetOSSKMSProviders(cfg *setting.Cfg, enc cipher.Cipher) encryption.ProviderMap {
return encryption.ProviderMap{
Default: defaultprovider.New(cfg.SecretsManagement.SecretKey, enc),
// ProvideOSSKMSProviders provides the ProviderConfig expected by the encryption manager in the OSS wire configuration.
// It looks for all configured 'secret_key' sections and creates a separate provider for each, each with its own secret key, allowing users to upgrade their secret key without breaking existing secrets.
func ProvideOSSKMSProviders(cfg *setting.Cfg, cipher cipher.Cipher) (encryption.ProviderConfig, error) {
pCfg := encryption.ProviderConfig{
CurrentProvider: encryption.ProviderID(cfg.SecretsManagement.CurrentEncryptionProvider),
AvailableProviders: make(encryption.ProviderMap),
}
// Look through the available secret_key providers and add them to the map
for providerName, properties := range cfg.SecretsManagement.ConfiguredKMSProviders {
if strings.HasPrefix(providerName, OSSProviderType) {
secretKey := properties[SecretKeyKey]
if secretKey != "" {
pCfg.AvailableProviders[encryption.ProviderID(providerName)] = newSecretKeyProvider(secretKey, cipher)
} else {
return pCfg, fmt.Errorf("missing secret_key for provider %s", providerName)
}
}
}
return pCfg, nil
}

@ -20,9 +20,6 @@ import (
"github.com/grafana/grafana/pkg/registry/apis/secret/contracts"
"github.com/grafana/grafana/pkg/registry/apis/secret/encryption"
"github.com/grafana/grafana/pkg/registry/apis/secret/encryption/cipher"
"github.com/grafana/grafana/pkg/registry/apis/secret/encryption/cipher/service"
"github.com/grafana/grafana/pkg/registry/apis/secret/encryption/kmsproviders"
"github.com/grafana/grafana/pkg/setting"
"github.com/grafana/grafana/pkg/util"
)
@ -33,16 +30,14 @@ const (
type EncryptionManager struct {
tracer trace.Tracer
store contracts.DataKeyStorage
enc cipher.Cipher
cfg *setting.Cfg
usageStats usagestats.Service
mtx sync.Mutex
pOnce sync.Once
providers encryption.ProviderMap
currentProviderID encryption.ProviderID
// The cipher is used to encrypt and decrypt payloads with a data key.
cipher cipher.Cipher
// The providerConfig are used to encrypt and decrypt the data keys.
providerConfig encryption.ProviderConfig
log log.Logger
}
@ -51,33 +46,22 @@ type EncryptionManager struct {
func ProvideEncryptionManager(
tracer trace.Tracer,
store contracts.DataKeyStorage,
cfg *setting.Cfg,
usageStats usagestats.Service,
thirdPartyKMS encryption.ProviderMap,
enc cipher.Cipher,
providerConfig encryption.ProviderConfig,
) (contracts.EncryptionManager, error) {
currentProviderID := encryption.ProviderID(cfg.SecretsManagement.EncryptionProvider)
enc, err := service.NewEncryptionService(tracer, usageStats, cfg)
if err != nil {
return nil, fmt.Errorf("failed to create encryption service: %w", err)
currentProviderID := providerConfig.CurrentProvider
if _, ok := providerConfig.AvailableProviders[currentProviderID]; !ok {
return nil, fmt.Errorf("missing configuration for current encryption provider %s", currentProviderID)
}
s := &EncryptionManager{
tracer: tracer,
store: store,
cfg: cfg,
usageStats: usageStats,
enc: enc,
currentProviderID: currentProviderID,
log: log.New("encryption"),
}
if err := s.InitProviders(thirdPartyKMS); err != nil {
return nil, err
}
if _, ok := s.providers[currentProviderID]; !ok {
return nil, fmt.Errorf("missing configuration for current encryption provider %s", currentProviderID)
tracer: tracer,
store: store,
usageStats: usageStats,
cipher: enc,
log: log.New("encryption"),
providerConfig: providerConfig,
}
s.registerUsageMetrics()
@ -85,44 +69,20 @@ func ProvideEncryptionManager(
return s, nil
}
func (s *EncryptionManager) InitProviders(extraProviders encryption.ProviderMap) (err error) {
done := false
s.pOnce.Do(func() {
providers := kmsproviders.GetOSSKMSProviders(s.cfg, s.enc)
for id, p := range extraProviders {
if _, exists := s.providers[id]; exists {
err = fmt.Errorf("provider %s already registered", id)
return
}
providers[id] = p
}
s.providers = providers
done = true
})
if !done && err == nil {
err = fmt.Errorf("providers were already initialized, no action taken")
}
return
}
func (s *EncryptionManager) registerUsageMetrics() {
s.usageStats.RegisterMetricsFunc(func(ctx context.Context) (map[string]any, error) {
usageMetrics := make(map[string]any)
// Current provider
kind, err := s.currentProviderID.Kind()
kind, err := s.providerConfig.CurrentProvider.Kind()
if err != nil {
return nil, fmt.Errorf("encryptionManager.registerUsageMetrics: %w", err)
}
usageMetrics[fmt.Sprintf("stats.%s.encryption.current_provider.%s.count", encryption.UsageInsightsPrefix, kind)] = 1
// Count by kind
countByKind := make(map[string]int, len(s.providers))
for id := range s.providers {
countByKind := make(map[string]int, len(s.providerConfig.AvailableProviders))
for id := range s.providerConfig.AvailableProviders {
kind, err := id.Kind()
if err != nil {
return nil, fmt.Errorf("encryptionManager.registerUsageMetrics: %w", err)
@ -161,7 +121,7 @@ func (s *EncryptionManager) Encrypt(ctx context.Context, namespace string, paylo
}
}()
label := encryption.KeyLabel(s.currentProviderID)
label := encryption.KeyLabel(s.providerConfig.CurrentProvider)
var id string
var dataKey []byte
@ -172,7 +132,7 @@ func (s *EncryptionManager) Encrypt(ctx context.Context, namespace string, paylo
}
var encrypted []byte
encrypted, err = s.enc.Encrypt(ctx, payload, string(dataKey))
encrypted, err = s.cipher.Encrypt(ctx, payload, string(dataKey))
if err != nil {
s.log.Error("Failed to encrypt secret", "error", err)
return nil, err
@ -235,7 +195,7 @@ func (s *EncryptionManager) dataKeyByLabel(ctx context.Context, namespace, label
}
// 2.1 Find the encryption provider.
provider, exists := s.providers[dataKey.Provider]
provider, exists := s.providerConfig.AvailableProviders[dataKey.Provider]
if !exists {
return "", nil, fmt.Errorf("could not find encryption provider '%s'", dataKey.Provider)
}
@ -264,9 +224,9 @@ func (s *EncryptionManager) newDataKey(ctx context.Context, namespace string, la
}
// 2.1 Find the encryption provider.
provider, exists := s.providers[s.currentProviderID]
provider, exists := s.providerConfig.AvailableProviders[s.providerConfig.CurrentProvider]
if !exists {
return "", nil, fmt.Errorf("could not find encryption provider '%s'", s.currentProviderID)
return "", nil, fmt.Errorf("could not find encryption provider '%s'", s.providerConfig.CurrentProvider)
}
// 2.2 Encrypt the data key.
@ -282,7 +242,7 @@ func (s *EncryptionManager) newDataKey(ctx context.Context, namespace string, la
Active: true,
UID: id,
Namespace: namespace,
Provider: s.currentProviderID,
Provider: s.providerConfig.CurrentProvider,
EncryptedData: encrypted,
Label: label,
}
@ -351,7 +311,7 @@ func (s *EncryptionManager) Decrypt(ctx context.Context, namespace string, paylo
}
var decrypted []byte
decrypted, err = s.enc.Decrypt(ctx, payload, string(dataKey))
decrypted, err = s.cipher.Decrypt(ctx, payload, string(dataKey))
return decrypted, err
}
@ -384,7 +344,7 @@ func (s *EncryptionManager) dataKeyById(ctx context.Context, namespace, id strin
}
// 2.1. Find the encryption provider.
provider, exists := s.providers[dataKey.Provider]
provider, exists := s.providerConfig.AvailableProviders[dataKey.Provider]
if !exists {
return nil, fmt.Errorf("could not find encryption provider '%s'", dataKey.Provider)
}
@ -398,6 +358,6 @@ func (s *EncryptionManager) dataKeyById(ctx context.Context, namespace, id strin
return decrypted, nil
}
func (s *EncryptionManager) GetProviders() encryption.ProviderMap {
return s.providers
func (s *EncryptionManager) GetProviders() encryption.ProviderConfig {
return s.providerConfig
}

@ -5,6 +5,7 @@ import (
"errors"
"testing"
"github.com/google/uuid"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"go.opentelemetry.io/otel/trace/noop"
@ -14,6 +15,8 @@ import (
"github.com/grafana/grafana/pkg/infra/usagestats"
"github.com/grafana/grafana/pkg/registry/apis/secret/contracts"
"github.com/grafana/grafana/pkg/registry/apis/secret/encryption"
"github.com/grafana/grafana/pkg/registry/apis/secret/encryption/cipher/service"
osskmsproviders "github.com/grafana/grafana/pkg/registry/apis/secret/encryption/kmsproviders"
"github.com/grafana/grafana/pkg/services/featuremgmt"
"github.com/grafana/grafana/pkg/services/sqlstore"
"github.com/grafana/grafana/pkg/setting"
@ -67,8 +70,8 @@ func TestEncryptionService_EnvelopeEncryption(t *testing.T) {
reports, err := svc.usageStats.GetUsageReport(context.Background())
require.NoError(t, err)
assert.Equal(t, 1, reports.Metrics["stats.secrets_manager.encryption.current_provider.secretKey.count"])
assert.Equal(t, 1, reports.Metrics["stats.secrets_manager.encryption.providers.secretKey.count"])
assert.Equal(t, 1, reports.Metrics["stats.secrets_manager.encryption.current_provider.secret_key.count"])
assert.Equal(t, 1, reports.Metrics["stats.secrets_manager.encryption.providers.secret_key.count"])
})
}
@ -161,7 +164,7 @@ func TestEncryptionService_DataKeys(t *testing.T) {
func TestEncryptionService_UseCurrentProvider(t *testing.T) {
t.Run("When encryption_provider is not specified explicitly, should use 'secretKey' as a current provider", func(t *testing.T) {
svc := setupTestService(t)
assert.Equal(t, encryption.ProviderID("secretKey.v1"), svc.currentProviderID)
assert.Equal(t, encryption.ProviderID("secret_key.v1"), svc.providerConfig.CurrentProvider)
})
t.Run("Should use encrypt/decrypt methods of the current encryption provider", func(t *testing.T) {
@ -175,8 +178,8 @@ func TestEncryptionService_UseCurrentProvider(t *testing.T) {
cfg := &setting.Cfg{
Raw: raw,
SecretsManagement: setting.SecretsManagerSettings{
SecretKey: "sdDkslslld",
EncryptionProvider: "secretKey.v1",
CurrentEncryptionProvider: "secret_key.v1",
ConfiguredKMSProviders: map[string]map[string]string{"secret_key.v1": {"secret_key": "SW2YcwTIb9zpOOhoPsMm"}},
},
}
@ -186,12 +189,19 @@ func TestEncryptionService_UseCurrentProvider(t *testing.T) {
encryptionStore, err := encryptionstorage.ProvideDataKeyStorage(database.ProvideDatabase(testDB, tracer), tracer, features, nil)
require.NoError(t, err)
usageStats := &usagestats.UsageStatsMock{T: t}
enc, err := service.ProvideAESGCMCipherService(tracer, usageStats)
require.NoError(t, err)
ossProviders, err := osskmsproviders.ProvideOSSKMSProviders(cfg, enc)
require.NoError(t, err)
encMgr, err := ProvideEncryptionManager(
tracer,
encryptionStore,
cfg,
&usagestats.UsageStatsMock{T: t},
encryption.ProvideThirdPartyProviderMap(),
usageStats,
enc,
ossProviders,
)
require.NoError(t, err)
@ -199,8 +209,10 @@ func TestEncryptionService_UseCurrentProvider(t *testing.T) {
//override default provider with fake, and register the fake separately
fake := &fakeProvider{}
encryptionManager.providers[encryption.ProviderID("fakeProvider.v1")] = fake
encryptionManager.currentProviderID = "fakeProvider.v1"
encryptionManager.providerConfig.AvailableProviders = encryption.ProviderMap{
encryption.ProviderID("fakeProvider.v1"): fake,
}
encryptionManager.providerConfig.CurrentProvider = encryption.ProviderID("fakeProvider.v1")
namespace := "test-namespace"
encrypted, _ := encryptionManager.Encrypt(context.Background(), namespace, []byte{})
@ -212,21 +224,185 @@ func TestEncryptionService_UseCurrentProvider(t *testing.T) {
svcDecryptMgr, err := ProvideEncryptionManager(
tracer,
encryptionStore,
cfg,
&usagestats.UsageStatsMock{T: t},
encryption.ProvideThirdPartyProviderMap(),
usageStats,
enc,
ossProviders,
)
require.NoError(t, err)
svcDecrypt := svcDecryptMgr.(*EncryptionManager)
svcDecrypt.providers[encryption.ProviderID("fakeProvider.v1")] = fake
svcDecrypt.currentProviderID = "fakeProvider.v1"
svcDecrypt.providerConfig.AvailableProviders = encryption.ProviderMap{
encryption.ProviderID("fakeProvider.v1"): fake,
}
svcDecrypt.providerConfig.CurrentProvider = encryption.ProviderID("fakeProvider.v1")
_, _ = svcDecrypt.Decrypt(context.Background(), namespace, encrypted)
assert.True(t, fake.decryptCalled, "fake provider's decrypt should be called")
})
}
func TestEncryptionService_SecretKeyVersionUpgrade(t *testing.T) {
ctx := context.Background()
namespace := "test-namespace"
// Generate random keys for testing
oldKey := util.GenerateShortUID() + util.GenerateShortUID() // 32 chars
newKey := util.GenerateShortUID() + util.GenerateShortUID() // 32 chars
t.Run("should encrypt with v1, upgrade to v2, encrypt with v2, and decrypt both", func(t *testing.T) {
// Step 1: Set up v1 configuration
cfgV1 := &setting.Cfg{
SecretsManagement: setting.SecretsManagerSettings{
CurrentEncryptionProvider: "secret_key.v1",
ConfiguredKMSProviders: map[string]map[string]string{"secret_key.v1": {"secret_key": oldKey}},
},
}
testDB := sqlstore.NewTestStore(t, sqlstore.WithMigrator(migrator.New()))
features := featuremgmt.WithFeatures(featuremgmt.FlagGrafanaAPIServerWithExperimentalAPIs, featuremgmt.FlagSecretsManagementAppPlatform)
tracer := noop.NewTracerProvider().Tracer("test")
encryptionStore, err := encryptionstorage.ProvideDataKeyStorage(database.ProvideDatabase(testDB, tracer), tracer, features, nil)
require.NoError(t, err)
usageStats := &usagestats.UsageStatsMock{T: t}
enc, err := service.ProvideAESGCMCipherService(tracer, usageStats)
require.NoError(t, err)
ossProviders, err := osskmsproviders.ProvideOSSKMSProviders(cfgV1, enc)
require.NoError(t, err)
svcV1, err := ProvideEncryptionManager(
tracer,
encryptionStore,
usageStats,
enc,
ossProviders,
)
require.NoError(t, err)
// Step 2: Encrypt something with v1
plaintext := []byte("secret data from v1")
encryptedV1, err := svcV1.Encrypt(ctx, namespace, plaintext)
require.NoError(t, err)
// Verify v1 can decrypt its own data
decryptedV1, err := svcV1.Decrypt(ctx, namespace, encryptedV1)
require.NoError(t, err)
assert.Equal(t, plaintext, decryptedV1)
// Verify current provider is v1
encMgrV1 := svcV1.(*EncryptionManager)
assert.Equal(t, encryption.ProviderID("secret_key.v1"), encMgrV1.providerConfig.CurrentProvider)
// Step 3: Create new configuration with v2 as current provider
cfgV2 := &setting.Cfg{
SecretsManagement: setting.SecretsManagerSettings{
CurrentEncryptionProvider: "secret_key.v2",
ConfiguredKMSProviders: map[string]map[string]string{
"secret_key.v1": {"secret_key": oldKey},
"secret_key.v2": {"secret_key": newKey},
},
},
}
// Reinitialize service with v2 configuration (reuse same store)
ossProvidersV2, err := osskmsproviders.ProvideOSSKMSProviders(cfgV2, enc)
require.NoError(t, err)
svcV2, err := ProvideEncryptionManager(
tracer,
encryptionStore,
usageStats,
enc,
ossProvidersV2,
)
require.NoError(t, err)
// Step 4: Ensure we can encrypt and decrypt with the new key (v2)
newPlaintext := []byte("secret data from v2")
encryptedV2, err := svcV2.Encrypt(ctx, namespace, newPlaintext)
require.NoError(t, err)
decryptedV2, err := svcV2.Decrypt(ctx, namespace, encryptedV2)
require.NoError(t, err)
assert.Equal(t, newPlaintext, decryptedV2)
// Verify current provider is v2
encMgrV2 := svcV2.(*EncryptionManager)
assert.Equal(t, encryption.ProviderID("secret_key.v2"), encMgrV2.providerConfig.CurrentProvider)
// Step 5: Ensure we can decrypt the old value encrypted with v1
decryptedOldWithV2, err := svcV2.Decrypt(ctx, namespace, encryptedV1)
require.NoError(t, err)
assert.Equal(t, plaintext, decryptedOldWithV2)
// Verify both providers are available
assert.Contains(t, encMgrV2.providerConfig.AvailableProviders, encryption.ProviderID("secret_key.v1"))
assert.Contains(t, encMgrV2.providerConfig.AvailableProviders, encryption.ProviderID("secret_key.v2"))
assert.Equal(t, 2, len(encMgrV2.providerConfig.AvailableProviders))
})
t.Run("encrypting with v1 then removing the v1 config should cause decryption to fail", func(t *testing.T) {
tracer := noop.NewTracerProvider().Tracer("test")
testDB := sqlstore.NewTestStore(t, sqlstore.WithMigrator(migrator.New()))
features := featuremgmt.WithFeatures(featuremgmt.FlagGrafanaAPIServerWithExperimentalAPIs, featuremgmt.FlagSecretsManagementAppPlatform)
encryptionStore, err := encryptionstorage.ProvideDataKeyStorage(database.ProvideDatabase(testDB, tracer), tracer, features, nil)
require.NoError(t, err)
usageStats := &usagestats.UsageStatsMock{T: t}
enc, err := service.ProvideAESGCMCipherService(tracer, usageStats)
require.NoError(t, err)
cfgV1 := &setting.Cfg{
SecretsManagement: setting.SecretsManagerSettings{
CurrentEncryptionProvider: "secret_key.v1",
ConfiguredKMSProviders: map[string]map[string]string{
"secret_key.v1": {"secret_key": uuid.New().String()},
},
},
}
ossProviders, err := osskmsproviders.ProvideOSSKMSProviders(cfgV1, enc)
require.NoError(t, err)
svcV1, err := ProvideEncryptionManager(
tracer,
encryptionStore,
usageStats,
enc,
ossProviders,
)
require.NoError(t, err)
rsp, err := svcV1.Encrypt(ctx, namespace, []byte("test"))
require.NoError(t, err)
cfgV2 := &setting.Cfg{
SecretsManagement: setting.SecretsManagerSettings{
CurrentEncryptionProvider: "secret_key.v2",
ConfiguredKMSProviders: map[string]map[string]string{
"secret_key.v2": {"secret_key": uuid.New().String()},
},
},
}
ossProvidersV2, err := osskmsproviders.ProvideOSSKMSProviders(cfgV2, enc)
require.NoError(t, err)
svcV2, err := ProvideEncryptionManager(
tracer,
encryptionStore,
usageStats,
enc,
ossProvidersV2,
)
require.NoError(t, err)
_, err = svcV2.Decrypt(ctx, namespace, rsp)
require.Error(t, err)
})
}
type fakeProvider struct {
encryptCalled bool
decryptCalled bool
@ -364,14 +540,12 @@ func TestIntegration_SecretsService(t *testing.T) {
t.Run(name, func(t *testing.T) {
testDB := sqlstore.NewTestStore(t, sqlstore.WithMigrator(migrator.New()))
tracer := noop.NewTracerProvider().Tracer("test")
features := featuremgmt.WithFeatures(featuremgmt.FlagSecretsManagementAppPlatform)
defaultKey := "SdlklWklckeLS"
cfg := &setting.Cfg{
SecretsManagement: setting.SecretsManagerSettings{
SecretKey: defaultKey,
EncryptionProvider: "secretKey.v1",
CurrentEncryptionProvider: "secret_key.v1",
ConfiguredKMSProviders: map[string]map[string]string{"secret_key.v1": {"secret_key": "SW2YcwTIb9zpOOhoPsMm"}},
},
}
store, err := encryptionstorage.ProvideDataKeyStorage(database.ProvideDatabase(testDB, tracer), tracer, features, nil)
@ -379,12 +553,18 @@ func TestIntegration_SecretsService(t *testing.T) {
usageStats := &usagestats.UsageStatsMock{T: t}
enc, err := service.ProvideAESGCMCipherService(tracer, usageStats)
require.NoError(t, err)
ossProviders, err := osskmsproviders.ProvideOSSKMSProviders(cfg, enc)
require.NoError(t, err)
svc, err := ProvideEncryptionManager(
tracer,
store,
cfg,
usageStats,
encryption.ProvideThirdPartyProviderMap(),
enc,
ossProviders,
)
require.NoError(t, err)
@ -415,34 +595,28 @@ func TestIntegration_SecretsService(t *testing.T) {
}
}
func TestEncryptionService_ReInitReturnsError(t *testing.T) {
svc := setupTestService(t)
err := svc.InitProviders(encryption.ProviderMap{
"fakeProvider.v1": &fakeProvider{},
})
require.Error(t, err)
}
func TestEncryptionService_ThirdPartyProviders(t *testing.T) {
cfg := &setting.Cfg{
SecretsManagement: setting.SecretsManagerSettings{
SecretKey: "SdlklWklckeLS",
EncryptionProvider: "secretKey.v1",
},
}
tracer := noop.NewTracerProvider().Tracer("test")
usageStats := &usagestats.UsageStatsMock{T: t}
enc, err := service.ProvideAESGCMCipherService(tracer, usageStats)
require.NoError(t, err)
svc, err := ProvideEncryptionManager(
tracer,
nil,
nil,
cfg,
&usagestats.UsageStatsMock{},
encryption.ProviderMap{
"fakeProvider.v1": &fakeProvider{},
usageStats,
enc,
encryption.ProviderConfig{
CurrentProvider: encryption.ProviderID("fakeProvider.v1"),
AvailableProviders: encryption.ProviderMap{
encryption.ProviderID("fakeProvider.v1"): &fakeProvider{},
},
},
)
require.NoError(t, err)
encMgr := svc.(*EncryptionManager)
require.Len(t, encMgr.providers, 2)
require.Contains(t, encMgr.providers, encryption.ProviderID("fakeProvider.v1"))
require.Len(t, encMgr.providerConfig.AvailableProviders, 1)
require.Contains(t, encMgr.providerConfig.AvailableProviders, encryption.ProviderID("fakeProvider.v1"))
}

@ -7,7 +7,8 @@ import (
"go.opentelemetry.io/otel/trace/noop"
"github.com/grafana/grafana/pkg/infra/usagestats"
"github.com/grafana/grafana/pkg/registry/apis/secret/encryption"
"github.com/grafana/grafana/pkg/registry/apis/secret/encryption/cipher/service"
osskmsproviders "github.com/grafana/grafana/pkg/registry/apis/secret/encryption/kmsproviders"
"github.com/grafana/grafana/pkg/services/featuremgmt"
"github.com/grafana/grafana/pkg/services/sqlstore"
"github.com/grafana/grafana/pkg/setting"
@ -27,8 +28,8 @@ func setupTestService(tb testing.TB) *EncryptionManager {
defaultKey := "SdlklWklckeLS"
cfg := &setting.Cfg{
SecretsManagement: setting.SecretsManagerSettings{
SecretKey: defaultKey,
EncryptionProvider: "secretKey.v1",
CurrentEncryptionProvider: "secret_key.v1",
ConfiguredKMSProviders: map[string]map[string]string{"secret_key.v1": {"secret_key": defaultKey}},
},
}
store, err := encryptionstorage.ProvideDataKeyStorage(database, tracer, features, nil)
@ -36,12 +37,18 @@ func setupTestService(tb testing.TB) *EncryptionManager {
usageStats := &usagestats.UsageStatsMock{T: tb}
enc, err := service.ProvideAESGCMCipherService(tracer, usageStats)
require.NoError(tb, err)
ossProviders, err := osskmsproviders.ProvideOSSKMSProviders(cfg, enc)
require.NoError(tb, err)
encMgr, err := ProvideEncryptionManager(
tracer,
store,
cfg,
usageStats,
encryption.ProvideThirdPartyProviderMap(),
enc,
ossProviders,
)
require.NoError(tb, err)

@ -9,7 +9,14 @@ import (
const UsageInsightsPrefix = "secrets_manager"
// Provider is a key encryption key provider for envelope encryption
type ProviderConfig struct {
CurrentProvider ProviderID
AvailableProviders ProviderMap
}
type ProviderMap map[ProviderID]Provider
// Provider is a fully configured key encryption key provider used for to encrypt and decrypt data keys for envelope encryption
type Provider interface {
Encrypt(ctx context.Context, blob []byte) ([]byte, error)
Decrypt(ctx context.Context, blob []byte) ([]byte, error)
@ -17,6 +24,7 @@ type Provider interface {
type ProviderID string
// Kind returns the kind of the provider, e.g. "secret_key", "aws_kms", "azure_keyvault", "google_kms", "hashicorp_vault"
func (id ProviderID) Kind() (string, error) {
idStr := string(id)
@ -28,17 +36,11 @@ func (id ProviderID) Kind() (string, error) {
return parts[0], nil
}
// KeyLabel returns a label for the data key that is unique to the current provider and today's date.
func KeyLabel(providerID ProviderID) string {
return fmt.Sprintf("%s@%s", time.Now().Format("2006-01-02"), providerID)
}
type ProviderMap map[ProviderID]Provider
// ProvideThirdPartyProviderMap fulfills the wire dependency needed by the encryption manager in OSS
func ProvideThirdPartyProviderMap() ProviderMap {
return ProviderMap{}
}
// 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

@ -8,6 +8,8 @@ import (
"go.opentelemetry.io/otel/trace/noop"
"github.com/grafana/grafana/pkg/infra/usagestats"
"github.com/grafana/grafana/pkg/registry/apis/secret/encryption/cipher/service"
osskmsproviders "github.com/grafana/grafana/pkg/registry/apis/secret/encryption/kmsproviders"
"github.com/grafana/grafana/pkg/registry/apis/secret/encryption/manager"
"github.com/grafana/grafana/pkg/registry/apis/secret/secretkeeper/sqlkeeper"
"github.com/grafana/grafana/pkg/services/featuremgmt"
@ -26,8 +28,8 @@ func TestMain(m *testing.M) {
func Test_OSSKeeperService(t *testing.T) {
cfg := &setting.Cfg{
SecretsManagement: setting.SecretsManagerSettings{
SecretKey: "sdDkslslld",
EncryptionProvider: "secretKey.v1",
CurrentEncryptionProvider: "secret_key.v1",
ConfiguredKMSProviders: map[string]map[string]string{"secret_key.v1": {"secret_key": "SW2YcwTIb9zpOOhoPsMm"}},
},
}
keeperService, err := setupTestService(t, cfg)
@ -55,7 +57,13 @@ func setupTestService(t *testing.T, cfg *setting.Cfg) (*OSSKeeperService, error)
encValueStore, err := encryptionstorage.ProvideEncryptedValueStorage(database, tracer, features)
require.NoError(t, err)
encryptionManager, err := manager.ProvideEncryptionManager(tracer, dataKeyStore, cfg, &usagestats.UsageStatsMock{T: t}, nil)
usageStats := &usagestats.UsageStatsMock{T: t}
enc, err := service.ProvideAESGCMCipherService(tracer, usageStats)
require.NoError(t, err)
ossProviders, err := osskmsproviders.ProvideOSSKMSProviders(cfg, enc)
require.NoError(t, err)
encryptionManager, err := manager.ProvideEncryptionManager(tracer, dataKeyStore, usageStats, enc, ossProviders)
require.NoError(t, err)
// Initialize the keeper service

@ -16,7 +16,8 @@ import (
"github.com/grafana/grafana/pkg/infra/usagestats"
"github.com/grafana/grafana/pkg/registry/apis/secret/contracts"
"github.com/grafana/grafana/pkg/registry/apis/secret/decrypt"
"github.com/grafana/grafana/pkg/registry/apis/secret/encryption"
cipher "github.com/grafana/grafana/pkg/registry/apis/secret/encryption/cipher/service"
osskmsproviders "github.com/grafana/grafana/pkg/registry/apis/secret/encryption/kmsproviders"
"github.com/grafana/grafana/pkg/registry/apis/secret/encryption/manager"
"github.com/grafana/grafana/pkg/registry/apis/secret/secretkeeper/sqlkeeper"
"github.com/grafana/grafana/pkg/registry/apis/secret/service"
@ -82,8 +83,8 @@ func Setup(t *testing.T, opts ...func(*SetupConfig)) Sut {
defaultKey := "SdlklWklckeLS"
cfg := &setting.Cfg{
SecretsManagement: setting.SecretsManagerSettings{
SecretKey: defaultKey,
EncryptionProvider: "secretKey.v1",
CurrentEncryptionProvider: "secret_key.v1",
ConfiguredKMSProviders: map[string]map[string]string{"secret_key.v1": {"secret_key": defaultKey}},
},
}
store, err := encryptionstorage.ProvideDataKeyStorage(database, tracer, features, nil)
@ -91,12 +92,18 @@ func Setup(t *testing.T, opts ...func(*SetupConfig)) Sut {
usageStats := &usagestats.UsageStatsMock{T: t}
enc, err := cipher.ProvideAESGCMCipherService(tracer, usageStats)
require.NoError(t, err)
ossProviders, err := osskmsproviders.ProvideOSSKMSProviders(cfg, enc)
require.NoError(t, err)
encryptionManager, err := manager.ProvideEncryptionManager(
tracer,
store,
cfg,
usageStats,
encryption.ProvideThirdPartyProviderMap(),
enc,
ossProviders,
)
require.NoError(t, err)

@ -43,7 +43,7 @@ import (
"github.com/grafana/grafana/pkg/registry/apis/provisioning/repository/github"
secretcontracts "github.com/grafana/grafana/pkg/registry/apis/secret/contracts"
secretdecrypt "github.com/grafana/grafana/pkg/registry/apis/secret/decrypt"
gsmEncryption "github.com/grafana/grafana/pkg/registry/apis/secret/encryption"
cipher "github.com/grafana/grafana/pkg/registry/apis/secret/encryption/cipher/service"
encryptionManager "github.com/grafana/grafana/pkg/registry/apis/secret/encryption/manager"
secretsecurevalueservice "github.com/grafana/grafana/pkg/registry/apis/secret/service"
secretvalidator "github.com/grafana/grafana/pkg/registry/apis/secret/validator"
@ -437,7 +437,7 @@ var wireBasicSet = wire.NewSet(
secretdatabase.ProvideDatabase,
wire.Bind(new(secretcontracts.Database), new(*secretdatabase.Database)),
encryptionManager.ProvideEncryptionManager,
gsmEncryption.ProvideThirdPartyProviderMap,
cipher.ProvideAESGCMCipherService,
// Unified storage
resource.ProvideStorageMetrics,
resource.ProvideIndexMetrics,

File diff suppressed because one or more lines are too long

@ -14,6 +14,7 @@ import (
"github.com/grafana/grafana/pkg/registry"
apisregistry "github.com/grafana/grafana/pkg/registry/apis"
"github.com/grafana/grafana/pkg/registry/apis/secret/contracts"
gsmKMSProviders "github.com/grafana/grafana/pkg/registry/apis/secret/encryption/kmsproviders"
"github.com/grafana/grafana/pkg/registry/apis/secret/secretkeeper"
"github.com/grafana/grafana/pkg/registry/backgroundsvcs"
"github.com/grafana/grafana/pkg/registry/usagestatssvcs"
@ -130,6 +131,7 @@ var wireExtsBasicSet = wire.NewSet(
builder.ProvideDefaultBuildHandlerChainFuncFromBuilders,
aggregatorrunner.ProvideNoopAggregatorConfigurator,
apisregistry.WireSetExts,
gsmKMSProviders.ProvideOSSKMSProviders,
)
var wireExtsSet = wire.NewSet(

@ -1,22 +1,37 @@
package setting
import (
"regexp"
"strings"
)
"github.com/grafana/grafana/pkg/services/kmsproviders"
const (
ProviderPrefix = "secrets_manager.encryption."
MisconfiguredProvider = "misconfigured"
)
type SecretsManagerSettings struct {
SecretKey string
EncryptionProvider string
AvailableProviders []string
CurrentEncryptionProvider string
// ConfiguredKMSProviders is a map of KMS providers found in the config file. The keys are in the format of <provider>.<keyName>, and the values are a map of the properties in that section
// In OSS, the provider type can only be "secret_key". In Enterprise, it can additionally be one of: "aws_kms", "azure_keyvault", "google_kms", "hashicorp_vault"
ConfiguredKMSProviders map[string]map[string]string
}
func (cfg *Cfg) readSecretsManagerSettings() {
secretsMgmt := cfg.Raw.Section("secrets_manager")
cfg.SecretsManagement.EncryptionProvider = secretsMgmt.Key("encryption_provider").MustString(kmsproviders.Default)
cfg.SecretsManagement.CurrentEncryptionProvider = secretsMgmt.Key("encryption_provider").MustString(MisconfiguredProvider)
// TODO: These are not used yet by the secrets manager because we need to distentagle the dependencies with OSS.
cfg.SecretsManagement.SecretKey = secretsMgmt.Key("secret_key").MustString("")
cfg.SecretsManagement.AvailableProviders = regexp.MustCompile(`\s*,\s*`).Split(secretsMgmt.Key("available_encryption_providers").MustString(""), -1) // parse comma separated list
// Extract available KMS providers from configuration sections
providers := make(map[string]map[string]string)
for _, section := range cfg.Raw.Sections() {
sectionName := section.Name()
if strings.HasPrefix(sectionName, ProviderPrefix) {
// Extract the provider name (everything after the prefix)
providerName := strings.TrimPrefix(sectionName, ProviderPrefix)
if providerName != "" {
providers[providerName] = section.KeysHash()
}
}
}
cfg.SecretsManagement.ConfiguredKMSProviders = providers
}

@ -0,0 +1,173 @@
package setting
import (
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestReadSecretsManagerSettings(t *testing.T) {
t.Run("should parse basic encryption provider", func(t *testing.T) {
iniContent := `
[secrets_manager]
encryption_provider = aws_kms
`
cfg, err := NewCfgFromBytes([]byte(iniContent))
require.NoError(t, err)
assert.Equal(t, "aws_kms", cfg.SecretsManagement.CurrentEncryptionProvider)
assert.Empty(t, cfg.SecretsManagement.ConfiguredKMSProviders)
})
t.Run("should parse single KMS provider configuration", func(t *testing.T) {
iniContent := `
[secrets_manager]
encryption_provider = aws_kms.v1
[secrets_manager.encryption.aws_kms.v1]
region = us-east-1
key_id = arn:aws:kms:us-east-1:123456789012:key/12345678-1234-1234-1234-123456789012
`
cfg, err := NewCfgFromBytes([]byte(iniContent))
require.NoError(t, err)
assert.Equal(t, "aws_kms.v1", cfg.SecretsManagement.CurrentEncryptionProvider)
assert.Len(t, cfg.SecretsManagement.ConfiguredKMSProviders, 1)
awsProvider := cfg.SecretsManagement.ConfiguredKMSProviders["aws_kms.v1"]
assert.Equal(t, "us-east-1", awsProvider["region"])
assert.Equal(t, "arn:aws:kms:us-east-1:123456789012:key/12345678-1234-1234-1234-123456789012", awsProvider["key_id"])
})
t.Run("should parse multiple KMS providers", func(t *testing.T) {
iniContent := `
[secrets_manager]
encryption_provider = aws_kms.v1
[secrets_manager.encryption.aws_kms.v1]
region = us-east-1
key_id = arn:aws:kms:us-east-1:123456789012:key/12345678-1234-1234-1234-123456789012
[secrets_manager.encryption.azure_kv.v1]
vault_url = https://myvault.vault.azure.net/
key_name = mykey
tenant_id = 12345678-1234-1234-1234-123456789012
[secrets_manager.encryption.secret_key.v1]
key = my-secret-key
`
cfg, err := NewCfgFromBytes([]byte(iniContent))
require.NoError(t, err)
assert.Equal(t, "aws_kms.v1", cfg.SecretsManagement.CurrentEncryptionProvider)
assert.Len(t, cfg.SecretsManagement.ConfiguredKMSProviders, 3)
// Check AWS KMS provider
awsProvider := cfg.SecretsManagement.ConfiguredKMSProviders["aws_kms.v1"]
assert.Equal(t, "us-east-1", awsProvider["region"])
assert.Equal(t, "arn:aws:kms:us-east-1:123456789012:key/12345678-1234-1234-1234-123456789012", awsProvider["key_id"])
// Check Azure Key Vault provider
azureProvider := cfg.SecretsManagement.ConfiguredKMSProviders["azure_kv.v1"]
assert.Equal(t, "https://myvault.vault.azure.net/", azureProvider["vault_url"])
assert.Equal(t, "mykey", azureProvider["key_name"])
assert.Equal(t, "12345678-1234-1234-1234-123456789012", azureProvider["tenant_id"])
// Check secret key provider
secretProvider := cfg.SecretsManagement.ConfiguredKMSProviders["secret_key.v1"]
assert.Equal(t, "my-secret-key", secretProvider["key"])
})
t.Run("should default to misconfigured provider when no encryption_provider is set", func(t *testing.T) {
iniContent := `
[secrets_manager]
# no encryption_provider setting
`
cfg, err := NewCfgFromBytes([]byte(iniContent))
require.NoError(t, err)
assert.Equal(t, MisconfiguredProvider, cfg.SecretsManagement.CurrentEncryptionProvider)
})
t.Run("should handle empty sections gracefully", func(t *testing.T) {
iniContent := `
[secrets_manager]
encryption_provider = empty_provider
[secrets_manager.encryption.empty_provider]
# empty section
`
cfg, err := NewCfgFromBytes([]byte(iniContent))
require.NoError(t, err)
assert.Equal(t, "empty_provider", cfg.SecretsManagement.CurrentEncryptionProvider)
assert.Len(t, cfg.SecretsManagement.ConfiguredKMSProviders, 1)
emptyProvider := cfg.SecretsManagement.ConfiguredKMSProviders["empty_provider"]
assert.NotNil(t, emptyProvider)
assert.Empty(t, emptyProvider)
})
t.Run("should ignore sections that don't match provider prefix", func(t *testing.T) {
iniContent := `
[secrets_manager]
encryption_provider = aws_kms.v1
[secrets_manager.encryption.valid_provider]
key = value
[secrets_manager.other_section]
setting = value
[completely_different_section]
some_setting = some_value
`
cfg, err := NewCfgFromBytes([]byte(iniContent))
require.NoError(t, err)
assert.Equal(t, "aws_kms.v1", cfg.SecretsManagement.CurrentEncryptionProvider)
assert.Len(t, cfg.SecretsManagement.ConfiguredKMSProviders, 1)
validProvider := cfg.SecretsManagement.ConfiguredKMSProviders["valid_provider"]
assert.Equal(t, "value", validProvider["key"])
})
t.Run("should handle provider names with special characters", func(t *testing.T) {
iniContent := `
[secrets_manager]
encryption_provider = aws_kms.v1
[secrets_manager.encryption.aws_kms.v1]
region = us-west-2
key_id = test-key
[secrets_manager.encryption.azure_kv.v1]
vault_url = https://test.vault.azure.net/
`
cfg, err := NewCfgFromBytes([]byte(iniContent))
require.NoError(t, err)
assert.Equal(t, "aws_kms.v1", cfg.SecretsManagement.CurrentEncryptionProvider)
assert.Len(t, cfg.SecretsManagement.ConfiguredKMSProviders, 2)
awsProvider := cfg.SecretsManagement.ConfiguredKMSProviders["aws_kms.v1"]
assert.Equal(t, "us-west-2", awsProvider["region"])
assert.Equal(t, "test-key", awsProvider["key_id"])
azureProvider := cfg.SecretsManagement.ConfiguredKMSProviders["azure_kv.v1"]
assert.Equal(t, "https://test.vault.azure.net/", azureProvider["vault_url"])
})
t.Run("should handle configuration with no secrets_manager section", func(t *testing.T) {
iniContent := `
[server]
domain = example.com
`
cfg, err := NewCfgFromBytes([]byte(iniContent))
require.NoError(t, err)
assert.Equal(t, MisconfiguredProvider, cfg.SecretsManagement.CurrentEncryptionProvider)
assert.Empty(t, cfg.SecretsManagement.ConfiguredKMSProviders)
})
}
Loading…
Cancel
Save