Secrets: Provide grpc and local inline secure value client (#109179)

* TestUtils: Add helper to create certs

* InlineSecureValue: Move to its own package rather than service

* Settings: Rename decrypt server configs to grpc prefixed as it is more generic

* InlineSecureValue: Add gRPC client

* InlineSecureValue: Add service provider grpc/local depending on setting.Cfg

* Wire: Add ProvideInlineSecureValueService to basic wireset

* InlineSecureValue: Re-export in root secret pkg and generate mocks for interface

* Settings: Unify decrypt+inline setting into generic grpc client
This commit is contained in:
Matheus Macabu
2025-08-06 09:04:55 +02:00
committed by GitHub
parent f3b8a891af
commit fc117afebc
14 changed files with 802 additions and 153 deletions
+9 -22
View File
@@ -4,7 +4,6 @@ import (
"fmt"
authnlib "github.com/grafana/authlib/authn"
claims "github.com/grafana/authlib/types"
"go.opentelemetry.io/otel/trace"
"github.com/grafana/grafana/pkg/registry/apis/secret/contracts"
@@ -13,24 +12,15 @@ import (
)
func ProvideDecryptService(cfg *setting.Cfg, tracer trace.Tracer, decryptStorage contracts.DecryptStorage) (contracts.DecryptService, error) {
switch cfg.SecretsManagement.DecryptServerType {
case "grpc":
if cfg.SecretsManagement.GrpcClientEnable {
grpcClientConfig := grpcutils.ReadGrpcClientConfig(cfg)
if cfg.SecretsManagement.DecryptServerAddress == "" {
return nil, fmt.Errorf("decrypt_server_address is required when decrypt_server_type is grpc")
if cfg.SecretsManagement.GrpcServerAddress == "" {
return nil, fmt.Errorf("grpc_server_address is required when grpc client is enabled")
}
if grpcClientConfig.Token == "" || grpcClientConfig.TokenExchangeURL == "" {
return nil, fmt.Errorf("grpc_client_authentication.token and grpc_client_authentication.token_exchange_url are required when secrets_manager.decrypt_server_type is grpc")
}
nsInfo, err := claims.ParseNamespace(grpcClientConfig.TokenNamespace)
if err != nil {
return nil, fmt.Errorf("failed to parse token namespace %v: %w", grpcClientConfig.TokenNamespace, err)
}
if nsInfo.OrgID < 1 {
return nil, fmt.Errorf("invalid token namepsace %v", grpcClientConfig.TokenNamespace)
return nil, fmt.Errorf("grpc_client_authentication.token and grpc_client_authentication.token_exchange_url are required when grpc client is enabled")
}
tokenExchangeClient, err := authnlib.NewTokenExchangeClient(authnlib.TokenExchangeConfig{
@@ -43,22 +33,19 @@ func ProvideDecryptService(cfg *setting.Cfg, tracer trace.Tracer, decryptStorage
tlsConfig := readTLSFromConfig(cfg)
client, err := NewGRPCDecryptClientWithTLS(tokenExchangeClient, tracer, cfg.SecretsManagement.DecryptServerAddress, tlsConfig)
client, err := NewGRPCDecryptClientWithTLS(tokenExchangeClient, tracer, cfg.SecretsManagement.GrpcServerAddress, tlsConfig)
if err != nil {
return nil, fmt.Errorf("failed to create grpc decrypt client: %w", err)
}
return client, nil
case "local", "":
return NewLocalDecryptClient(decryptStorage)
}
return nil, fmt.Errorf("unsupported storage type: %s", cfg.SecretsManagement.DecryptServerType)
return NewLocalDecryptClient(decryptStorage)
}
func readTLSFromConfig(cfg *setting.Cfg) TLSConfig {
if !cfg.SecretsManagement.DecryptServerUseTLS {
if !cfg.SecretsManagement.GrpcServerUseTLS {
return TLSConfig{
UseTLS: false,
InsecureSkipVerify: true,
@@ -72,7 +59,7 @@ func readTLSFromConfig(cfg *setting.Cfg) TLSConfig {
CertFile: apiServer.Key("proxy_client_cert_file").MustString(""),
KeyFile: apiServer.Key("proxy_client_key_file").MustString(""),
CAFile: apiServer.Key("apiservice_ca_bundle_file").MustString(""),
ServerName: cfg.SecretsManagement.DecryptServerTLSServerName,
InsecureSkipVerify: cfg.SecretsManagement.DecryptServerTLSSkipVerify,
ServerName: cfg.SecretsManagement.GrpcServerTLSServerName,
InsecureSkipVerify: cfg.SecretsManagement.GrpcServerTLSSkipVerify,
}
}
@@ -1,4 +1,4 @@
package decrypt
package decrypt_test
import (
"context"
@@ -11,7 +11,6 @@ import (
decryptv1beta1 "github.com/grafana/grafana/apps/secret/decrypt/v1beta1"
secretv1beta1 "github.com/grafana/grafana/apps/secret/pkg/apis/secret/v1beta1"
"github.com/madflojo/testcerts"
"github.com/stretchr/testify/mock"
"github.com/stretchr/testify/require"
"go.opentelemetry.io/otel/trace/noop"
@@ -21,6 +20,8 @@ import (
"google.golang.org/grpc/metadata"
"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/testutils"
"github.com/grafana/grafana/pkg/registry/apis/secret/xkube"
"github.com/grafana/grafana/pkg/services/authn/clients"
"github.com/grafana/grafana/pkg/setting"
@@ -43,9 +44,8 @@ func TestDecryptService(t *testing.T) {
}
cfg := setting.NewCfg()
cfg.SecretsManagement.DecryptServerType = "local"
decryptService, err := ProvideDecryptService(cfg, tracer, mockStorage)
decryptService, err := decrypt.ProvideDecryptService(cfg, tracer, mockStorage)
require.NoError(t, err)
resp, err := decryptService.Decrypt(ctx, "svc-name", "default", []string{"secure-value-1"})
@@ -72,9 +72,8 @@ func TestDecryptService(t *testing.T) {
}
cfg := setting.NewCfg()
cfg.SecretsManagement.DecryptServerType = "local"
decryptService, err := ProvideDecryptService(cfg, tracer, mockStorage)
decryptService, err := decrypt.ProvideDecryptService(cfg, tracer, mockStorage)
require.NoError(t, err)
resp, err := decryptService.Decrypt(ctx, "svc-name", "default", []string{"secure-value-1", "secure-value-2"})
@@ -100,9 +99,9 @@ func TestDecryptService(t *testing.T) {
}
cfg := setting.NewCfg()
cfg.SecretsManagement.DecryptServerType = "local"
cfg.SecretsManagement.GrpcClientEnable = false
decryptService, err := ProvideDecryptService(cfg, tracer, mockStorage)
decryptService, err := decrypt.ProvideDecryptService(cfg, tracer, mockStorage)
require.NoError(t, err)
resp, err := decryptService.Decrypt(ctx, "svc-name", "default", []string{"secure-value-1", "secure-value-2"})
@@ -111,39 +110,27 @@ func TestDecryptService(t *testing.T) {
require.EqualValues(t, decryptedValuesResp, resp)
})
t.Run("when storage type is unsupported, it returns an error", func(t *testing.T) {
t.Run("when grpc server is enabled but token exchange config is missing, it returns an error", func(t *testing.T) {
t.Parallel()
cfg := setting.NewCfg()
cfg.SecretsManagement.DecryptServerType = "unsupported"
cfg.SecretsManagement.GrpcClientEnable = true
cfg.SecretsManagement.GrpcServerAddress = "127.0.0.1:10000"
decryptService, err := ProvideDecryptService(cfg, tracer, nil)
require.Error(t, err)
require.Contains(t, err.Error(), "unsupported storage type")
require.Nil(t, decryptService)
})
t.Run("when storage type is grpc but token exchange config is missing, it returns an error", func(t *testing.T) {
t.Parallel()
cfg := setting.NewCfg()
cfg.SecretsManagement.DecryptServerType = "grpc"
cfg.SecretsManagement.DecryptServerAddress = "127.0.0.1:10000"
_, err := ProvideDecryptService(cfg, tracer, nil)
_, err := decrypt.ProvideDecryptService(cfg, tracer, nil)
require.Error(t, err)
require.Contains(t, err.Error(), "grpc_client_authentication.token and grpc_client_authentication.token_exchange_url are required")
})
t.Run("when storage type is grpc but storage address is missing, it returns an error", func(t *testing.T) {
t.Run("when grpc server is enabled but storage address is missing, it returns an error", func(t *testing.T) {
t.Parallel()
cfg := setting.NewCfg()
cfg.SecretsManagement.DecryptServerType = "grpc"
cfg.SecretsManagement.GrpcClientEnable = true
_, err := ProvideDecryptService(cfg, tracer, nil)
_, err := decrypt.ProvideDecryptService(cfg, tracer, nil)
require.Error(t, err)
require.Contains(t, err.Error(), "decrypt_server_address is required")
require.Contains(t, err.Error(), "grpc_server_address is required")
})
t.Run("happy path with grpc+tls server with fake toke exchanger and server", func(t *testing.T) {
@@ -165,9 +152,9 @@ func TestDecryptService(t *testing.T) {
listener, err := nettest.NewLocalListener("tcp")
require.NoError(t, err)
certPaths := createX509TestDir(t)
certPaths := testutils.CreateX509TestDir(t)
serverCert, err := tls.LoadX509KeyPair(certPaths.serverCert, certPaths.serverKey)
serverCert, err := tls.LoadX509KeyPair(certPaths.ServerCert, certPaths.ServerKey)
require.NoError(t, err)
tlsConfig := &tls.Config{
@@ -204,12 +191,12 @@ func TestDecryptService(t *testing.T) {
namespace := "stacks-1234"
cfg := setting.NewCfg()
cfg.SecretsManagement.DecryptServerType = "grpc"
cfg.SecretsManagement.DecryptServerAddress = listener.Addr().String()
cfg.SecretsManagement.DecryptGrafanaServiceName = grafanaSvcIdentity
cfg.SecretsManagement.DecryptServerUseTLS = true
cfg.SecretsManagement.DecryptServerTLSServerName = "localhost"
cfg.SecretsManagement.DecryptServerTLSSkipVerify = false
cfg.SecretsManagement.GrpcClientEnable = true
cfg.SecretsManagement.GrpcServerAddress = listener.Addr().String()
cfg.SecretsManagement.GrpcGrafanaServiceName = grafanaSvcIdentity
cfg.SecretsManagement.GrpcServerUseTLS = true
cfg.SecretsManagement.GrpcServerTLSServerName = "localhost"
cfg.SecretsManagement.GrpcServerTLSSkipVerify = false
grpcClientAuth := cfg.Raw.Section("grpc_client_authentication")
_, err = grpcClientAuth.NewKey("token", "test-token")
@@ -220,15 +207,15 @@ func TestDecryptService(t *testing.T) {
require.NoError(t, err)
apiServer := cfg.Raw.Section("grafana-apiserver")
_, err = apiServer.NewKey("proxy_client_cert_file", certPaths.clientCert)
_, err = apiServer.NewKey("proxy_client_cert_file", certPaths.ClientCert)
require.NoError(t, err)
_, err = apiServer.NewKey("proxy_client_key_file", certPaths.clientKey)
_, err = apiServer.NewKey("proxy_client_key_file", certPaths.ClientKey)
require.NoError(t, err)
_, err = apiServer.NewKey("apiservice_ca_bundle_file", certPaths.ca)
_, err = apiServer.NewKey("apiservice_ca_bundle_file", certPaths.CA)
require.NoError(t, err)
// Create and test decryption, using the mock grpc server as we dont test the business logic here
decryptService, err := ProvideDecryptService(cfg, tracer, nil)
decryptService, err := decrypt.ProvideDecryptService(cfg, tracer, nil)
require.NoError(t, err)
require.NotNil(t, decryptService)
@@ -271,40 +258,3 @@ func (m *mockDecryptServer) DecryptSecureValues(ctx context.Context, req *decryp
args := m.Called(ctx, req)
return args.Get(0).(*decryptv1beta1.SecureValueDecryptResponseCollection), args.Error(1)
}
type certPaths struct {
clientCert string
clientKey string
serverCert string
serverKey string
ca string
}
func createX509TestDir(t *testing.T) certPaths {
t.Helper()
tmpDir := t.TempDir()
ca := testcerts.NewCA()
caCertFile, _, err := ca.ToTempFile(tmpDir)
require.NoError(t, err)
serverKp, err := ca.NewKeyPair("localhost")
require.NoError(t, err)
serverCertFile, serverKeyFile, err := serverKp.ToTempFile(tmpDir)
require.NoError(t, err)
clientKp, err := ca.NewKeyPair()
require.NoError(t, err)
clientCertFile, clientKeyFile, err := clientKp.ToTempFile(tmpDir)
require.NoError(t, err)
return certPaths{
clientCert: clientCertFile.Name(),
clientKey: clientKeyFile.Name(),
serverCert: serverCertFile.Name(),
serverKey: serverKeyFile.Name(),
ca: caCertFile.Name(),
}
}