diff --git a/go.mod b/go.mod index 113f394dfe8..a2769e348f2 100644 --- a/go.mod +++ b/go.mod @@ -51,7 +51,7 @@ require ( github.com/grafana/cuetsy v0.1.11 // @grafana/grafana-as-code github.com/grafana/grafana-aws-sdk v0.25.0 // @grafana/aws-datasources github.com/grafana/grafana-azure-sdk-go/v2 v2.0.1 // @grafana/partner-datasources - github.com/grafana/grafana-plugin-sdk-go v0.216.0 // @grafana/plugins-platform-backend + github.com/grafana/grafana-plugin-sdk-go v0.217.0 // @grafana/plugins-platform-backend github.com/grpc-ecosystem/go-grpc-middleware v1.4.0 // @grafana/backend-platform github.com/hashicorp/go-hclog v1.6.2 // @grafana/plugins-platform-backend github.com/hashicorp/go-plugin v1.6.0 // @grafana/plugins-platform-backend @@ -109,7 +109,7 @@ require ( gopkg.in/mail.v2 v2.3.1 // @grafana/backend-platform gopkg.in/yaml.v2 v2.4.0 // indirect gopkg.in/yaml.v3 v3.0.1 // @grafana/alerting-squad-backend - xorm.io/builder v0.3.6 // indirect; @grafana/backend-platform + xorm.io/builder v0.3.6 // @grafana/backend-platform xorm.io/core v0.7.3 // @grafana/backend-platform xorm.io/xorm v0.8.2 // @grafana/alerting-squad-backend ) @@ -161,7 +161,7 @@ require ( github.com/grpc-ecosystem/go-grpc-prometheus v1.2.1-0.20191002090509-6af20e3a5340 // indirect github.com/hashicorp/errwrap v1.1.0 // indirect github.com/hashicorp/go-msgpack v0.5.5 // indirect - github.com/hashicorp/go-multierror v1.1.1 // indirect; @grafana/alerting-squad + github.com/hashicorp/go-multierror v1.1.1 // @grafana/alerting-squad github.com/hashicorp/go-sockaddr v1.0.6 // indirect github.com/hashicorp/golang-lru v0.6.0 // indirect github.com/hashicorp/yamux v0.1.1 // indirect @@ -317,7 +317,7 @@ require ( github.com/googleapis/enterprise-certificate-proxy v0.3.2 // indirect github.com/grafana/regexp v0.0.0-20221123153739-15dc172cd2db // indirect github.com/hashicorp/go-immutable-radix v1.3.1 // indirect - github.com/hashicorp/golang-lru/v2 v2.0.7 // indirect; @grafana/alerting-squad-backend + github.com/hashicorp/golang-lru/v2 v2.0.7 // @grafana/alerting-squad-backend github.com/hashicorp/memberlist v0.5.0 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect github.com/invopop/yaml v0.2.0 // indirect diff --git a/go.sum b/go.sum index 6f0b28792fe..2a406adbdb9 100644 --- a/go.sum +++ b/go.sum @@ -2184,8 +2184,8 @@ github.com/grafana/grafana-google-sdk-go v0.1.0/go.mod h1:Vo2TKWfDVmNTELBUM+3lkr github.com/grafana/grafana-openapi-client-go v0.0.0-20231213163343-bd475d63fb79 h1:r+mU5bGMzcXCRVAuOrTn54S80qbfVkvTdUJZfSfTNbs= github.com/grafana/grafana-openapi-client-go v0.0.0-20231213163343-bd475d63fb79/go.mod h1:wc6Hbh3K2TgCUSfBC/BOzabItujtHMESZeFk5ZhdxhQ= github.com/grafana/grafana-plugin-sdk-go v0.114.0/go.mod h1:D7x3ah+1d4phNXpbnOaxa/osSaZlwh9/ZUnGGzegRbk= -github.com/grafana/grafana-plugin-sdk-go v0.216.0 h1:DftszjdGZM6hikBazyB7CdrhyYOwUct9s5lA/BwSl2c= -github.com/grafana/grafana-plugin-sdk-go v0.216.0/go.mod h1:FdvSvOliqpVLnytM7e89zCFyYPDE6VOn9SIjVQRvVxM= +github.com/grafana/grafana-plugin-sdk-go v0.217.0 h1:oQjq5KRrVrhweXHxFtEMgjv1KW7hujGiRPIYrsPZ9PE= +github.com/grafana/grafana-plugin-sdk-go v0.217.0/go.mod h1:FdvSvOliqpVLnytM7e89zCFyYPDE6VOn9SIjVQRvVxM= github.com/grafana/grafana/pkg/apimachinery v0.0.0-20240226124929-648abdbd0ea4 h1:hpyusz8c3yRFoJPlA0o34rWnsLbaOOBZleqRhFBi5Lg= github.com/grafana/grafana/pkg/apimachinery v0.0.0-20240226124929-648abdbd0ea4/go.mod h1:vrRQJuNprTWqwm6JPxHf3BoTJhvO15QMEjQ7Q/YUOnI= github.com/grafana/grafana/pkg/apiserver v0.0.0-20240226124929-648abdbd0ea4 h1:tIbI5zgos92vwJ8lV3zwHwuxkV03GR3FGLkFW9V5LxY= diff --git a/pkg/services/datasources/service/datasource.go b/pkg/services/datasources/service/datasource.go index d110042d5a5..25b6d60a0d1 100644 --- a/pkg/services/datasources/service/datasource.go +++ b/pkg/services/datasources/service/datasource.go @@ -534,7 +534,7 @@ func (s *Service) httpClientOptions(ctx context.Context, ds *datasources.DataSou ClientCfg: &sdkproxy.ClientCfg{ ClientCert: s.cfg.SecureSocksDSProxy.ClientCert, ClientKey: s.cfg.SecureSocksDSProxy.ClientKey, - RootCA: s.cfg.SecureSocksDSProxy.RootCA, + RootCAs: s.cfg.SecureSocksDSProxy.RootCAs, ProxyAddress: s.cfg.SecureSocksDSProxy.ProxyAddress, ServerName: s.cfg.SecureSocksDSProxy.ServerName, AllowInsecure: s.cfg.SecureSocksDSProxy.AllowInsecure, diff --git a/pkg/services/pluginsintegration/pluginconfig/envvars.go b/pkg/services/pluginsintegration/pluginconfig/envvars.go index b7d57ec3af6..cca911ebfa5 100644 --- a/pkg/services/pluginsintegration/pluginconfig/envvars.go +++ b/pkg/services/pluginsintegration/pluginconfig/envvars.go @@ -122,13 +122,20 @@ func (p *EnvVarsProvider) awsEnvVars(pluginID string) []string { func (p *EnvVarsProvider) secureSocksProxyEnvVars() []string { if p.cfg.ProxySettings.Enabled { return []string{ - envVar(proxy.PluginSecureSocksProxyClientCert, p.cfg.ProxySettings.ClientCert), - envVar(proxy.PluginSecureSocksProxyClientKey, p.cfg.ProxySettings.ClientKey), - envVar(proxy.PluginSecureSocksProxyRootCACert, p.cfg.ProxySettings.RootCA), - envVar(proxy.PluginSecureSocksProxyProxyAddress, p.cfg.ProxySettings.ProxyAddress), - envVar(proxy.PluginSecureSocksProxyServerName, p.cfg.ProxySettings.ServerName), - envVar(proxy.PluginSecureSocksProxyEnabled, strconv.FormatBool(p.cfg.ProxySettings.Enabled)), - envVar(proxy.PluginSecureSocksProxyAllowInsecure, strconv.FormatBool(p.cfg.ProxySettings.AllowInsecure)), + // nolint:staticcheck + envVar(proxy.PluginSecureSocksProxyClientCertFilePathEnvVarName, p.cfg.ProxySettings.ClientCertFilePath), + // nolint:staticcheck + envVar(proxy.PluginSecureSocksProxyClientKeyFilePathEnvVarName, p.cfg.ProxySettings.ClientKeyFilePath), + // nolint:staticcheck + envVar(proxy.PluginSecureSocksProxyRootCACertFilePathsEnvVarName, strings.Join(p.cfg.ProxySettings.RootCAFilePaths, " ")), + // nolint:staticcheck + envVar(proxy.PluginSecureSocksProxyAddressEnvVarName, p.cfg.ProxySettings.ProxyAddress), + // nolint:staticcheck + envVar(proxy.PluginSecureSocksProxyServerNameEnvVarName, p.cfg.ProxySettings.ServerName), + // nolint:staticcheck + envVar(proxy.PluginSecureSocksProxyEnabledEnvVarName, strconv.FormatBool(p.cfg.ProxySettings.Enabled)), + // nolint:staticcheck + envVar(proxy.PluginSecureSocksProxyAllowInsecureEnvVarName, strconv.FormatBool(p.cfg.ProxySettings.AllowInsecure)), } } return nil diff --git a/pkg/services/pluginsintegration/pluginconfig/request.go b/pkg/services/pluginsintegration/pluginconfig/request.go index dfb1a5b2dd8..e052457f671 100644 --- a/pkg/services/pluginsintegration/pluginconfig/request.go +++ b/pkg/services/pluginsintegration/pluginconfig/request.go @@ -75,7 +75,7 @@ func (s *RequestConfigProvider) PluginRequestConfig(ctx context.Context, pluginI m[proxy.PluginSecureSocksProxyEnabled] = "true" m[proxy.PluginSecureSocksProxyClientCert] = s.cfg.ProxySettings.ClientCert m[proxy.PluginSecureSocksProxyClientKey] = s.cfg.ProxySettings.ClientKey - m[proxy.PluginSecureSocksProxyRootCACert] = s.cfg.ProxySettings.RootCA + m[proxy.PluginSecureSocksProxyRootCAs] = strings.Join(s.cfg.ProxySettings.RootCAs, ",") m[proxy.PluginSecureSocksProxyProxyAddress] = s.cfg.ProxySettings.ProxyAddress m[proxy.PluginSecureSocksProxyServerName] = s.cfg.ProxySettings.ServerName m[proxy.PluginSecureSocksProxyAllowInsecure] = strconv.FormatBool(s.cfg.ProxySettings.AllowInsecure) diff --git a/pkg/services/pluginsintegration/pluginconfig/request_test.go b/pkg/services/pluginsintegration/pluginconfig/request_test.go index 52fa6454b38..0dd75fe61a2 100644 --- a/pkg/services/pluginsintegration/pluginconfig/request_test.go +++ b/pkg/services/pluginsintegration/pluginconfig/request_test.go @@ -39,7 +39,7 @@ func TestRequestConfigProvider_PluginRequestConfig(t *testing.T) { ShowUI: true, ClientCert: "c3rt", ClientKey: "k3y", - RootCA: "ca", + RootCAs: []string{"ca"}, ProxyAddress: "https://proxy.grafana.com", ServerName: "secureProxy", AllowInsecure: true, @@ -65,7 +65,7 @@ func TestRequestConfigProvider_PluginRequestConfig(t *testing.T) { ShowUI: true, ClientCert: "c3rt", ClientKey: "k3y", - RootCA: "ca", + RootCAs: []string{"ca"}, ProxyAddress: "https://proxy.grafana.com", ServerName: "secureProxy", }, @@ -83,7 +83,7 @@ func TestRequestConfigProvider_PluginRequestConfig(t *testing.T) { ShowUI: true, ClientCert: "c3rt", ClientKey: "k3y", - RootCA: "ca", + RootCAs: []string{"ca"}, ProxyAddress: "https://proxy.grafana.com", ServerName: "secureProxy", }, diff --git a/pkg/setting/setting_secure_socks_proxy.go b/pkg/setting/setting_secure_socks_proxy.go index 815d7f60006..6e034cfa313 100644 --- a/pkg/setting/setting_secure_socks_proxy.go +++ b/pkg/setting/setting_secure_socks_proxy.go @@ -1,33 +1,41 @@ package setting import ( + "encoding/pem" "errors" + "os" "gopkg.in/ini.v1" ) type SecureSocksDSProxySettings struct { - Enabled bool - ShowUI bool - AllowInsecure bool - ClientCert string - ClientKey string - RootCA string - ProxyAddress string - ServerName string + Enabled bool + ShowUI bool + AllowInsecure bool + ClientCert string + ClientCertFilePath string + ClientKey string + ClientKeyFilePath string + RootCAs []string + RootCAFilePaths []string + ProxyAddress string + ServerName string } func readSecureSocksDSProxySettings(iniFile *ini.File) (SecureSocksDSProxySettings, error) { - s := SecureSocksDSProxySettings{} + s := SecureSocksDSProxySettings{ + RootCAs: []string{}, + RootCAFilePaths: []string{}, + } secureSocksProxySection := iniFile.Section("secure_socks_datasource_proxy") s.Enabled = secureSocksProxySection.Key("enabled").MustBool(false) - s.ClientCert = secureSocksProxySection.Key("client_cert").MustString("") - s.ClientKey = secureSocksProxySection.Key("client_key").MustString("") - s.RootCA = secureSocksProxySection.Key("root_ca_cert").MustString("") s.ProxyAddress = secureSocksProxySection.Key("proxy_address").MustString("") s.ServerName = secureSocksProxySection.Key("server_name").MustString("") s.ShowUI = secureSocksProxySection.Key("show_ui").MustBool(true) s.AllowInsecure = secureSocksProxySection.Key("allow_insecure").MustBool(false) + s.ClientCertFilePath = secureSocksProxySection.Key("client_cert").MustString("") + s.ClientKeyFilePath = secureSocksProxySection.Key("client_key").MustString("") + s.RootCAFilePaths = secureSocksProxySection.Key("root_ca_cert").Strings(" ") if !s.Enabled { return s, nil @@ -40,14 +48,50 @@ func readSecureSocksDSProxySettings(iniFile *ini.File) (SecureSocksDSProxySettin // If the proxy is going to use TLS. if !s.AllowInsecure { // all fields must be specified to use the proxy - if s.RootCA == "" { - return s, errors.New("rootCA required") - } else if s.ClientCert == "" || s.ClientKey == "" { + if len(s.RootCAFilePaths) == 0 { + return s, errors.New("one or more rootCA required") + } else if s.ClientCertFilePath == "" || s.ClientKeyFilePath == "" { return s, errors.New("client key pair required") } else if s.ServerName == "" { return s, errors.New("server name required") } + } else { + return s, nil } + if s.ClientCertFilePath != "" { + certPEMBlock, err := os.ReadFile(s.ClientCertFilePath) + if err != nil { + return s, err + } + s.ClientCert = string(certPEMBlock) + } + + if s.ClientKeyFilePath != "" { + keyPEMBlock, err := os.ReadFile(s.ClientKeyFilePath) + if err != nil { + return s, err + } + s.ClientKey = string(keyPEMBlock) + } + + var rootCAs []string + for _, rootCAFile := range s.RootCAFilePaths { + // nolint:gosec + // The gosec G304 warning can be ignored because `rootCAFile` comes from config ini, and we check below if + // it's the right file type. + pemBytes, err := os.ReadFile(rootCAFile) + if err != nil { + return s, err + } + + pemDecoded, _ := pem.Decode(pemBytes) + if pemDecoded == nil || pemDecoded.Type != "CERTIFICATE" { + return s, errors.New("root ca is invalid") + } + rootCAs = append(rootCAs, string(pemBytes)) + } + s.RootCAs = rootCAs + return s, nil } diff --git a/pkg/setting/setting_secure_socks_proxy_test.go b/pkg/setting/setting_secure_socks_proxy_test.go index 3d8ac9f301c..c198f70a556 100644 --- a/pkg/setting/setting_secure_socks_proxy_test.go +++ b/pkg/setting/setting_secure_socks_proxy_test.go @@ -1,25 +1,46 @@ package setting import ( + "bytes" + "crypto/rand" + "crypto/rsa" + "crypto/x509" + "crypto/x509/pkix" + "encoding/pem" "errors" "fmt" + "math/big" + "os" + "path/filepath" "testing" + "time" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" "gopkg.in/ini.v1" ) -func mustNewIniFile(fileContents string) *ini.File { - file, err := ini.Load([]byte(fileContents)) - if err != nil { - panic(fmt.Sprintf("creating ini file for test: %s", err)) - } - return file -} - func TestReadSecureSocksDSProxySettings(t *testing.T) { t.Parallel() + tempDir := t.TempDir() + testFilePath := filepath.Join(tempDir, "test") + testFileData := "foobar" + err := os.WriteFile(testFilePath, []byte(testFileData), 0600) + require.NoError(t, err) + + rootCACertFilePath := filepath.Join(tempDir, "ca.cert") + // nolint:gosec + caCertFile, err := os.Create(rootCACertFilePath) + require.NoError(t, err) + defer func() { + err = caCertFile.Close() + require.NoError(t, err) + }() + + rootCaFileData := createTestRootCAFile(t, rootCACertFilePath) + require.NoError(t, err) + cases := []struct { description string iniFile *ini.File @@ -32,24 +53,27 @@ func TestReadSecureSocksDSProxySettings(t *testing.T) { [secure_socks_datasource_proxy] `), expectedSettings: SecureSocksDSProxySettings{ - Enabled: false, - ClientCert: "", - ClientKey: "", - RootCA: "", - ProxyAddress: "", - ServerName: "", - ShowUI: true, - AllowInsecure: false, + Enabled: false, + ShowUI: true, + AllowInsecure: false, + ClientCertFilePath: "", + ClientCert: "", + ClientKey: "", + ClientKeyFilePath: "", + RootCAs: []string{}, + RootCAFilePaths: []string{}, + ProxyAddress: "", + ServerName: "", }, }, { - description: "root ca is required", + description: "one or more root ca is required", iniFile: mustNewIniFile(` [secure_socks_datasource_proxy] enabled = true proxy_address = address `), - expectedErr: errors.New("rootCA required"), + expectedErr: errors.New("one or more rootCA required"), }, { description: "client cert is required", @@ -106,35 +130,40 @@ server_name = name allow_insecure = true `), expectedSettings: SecureSocksDSProxySettings{ - Enabled: true, - ProxyAddress: "address", - ServerName: "name", - ShowUI: true, - AllowInsecure: true, + Enabled: true, + ProxyAddress: "address", + ServerName: "name", + ShowUI: true, + AllowInsecure: true, + RootCAFilePaths: []string{}, + RootCAs: []string{}, }, }, { description: "custom values", - iniFile: mustNewIniFile(` + iniFile: mustNewIniFile(fmt.Sprintf(` [secure_socks_datasource_proxy] enabled = true - client_cert = cert - client_key = key - root_ca_cert = root_ca + client_cert = %s + client_key = %s + root_ca_cert = %s proxy_address = proxy_address server_name = server_name show_ui = false - allow_insecure = true - `), + allow_insecure = false + `, testFilePath, testFilePath, rootCACertFilePath)), expectedSettings: SecureSocksDSProxySettings{ - Enabled: true, - ClientCert: "cert", - ClientKey: "key", - RootCA: "root_ca", - ProxyAddress: "proxy_address", - ServerName: "server_name", - ShowUI: false, - AllowInsecure: true, + Enabled: true, + ShowUI: false, + AllowInsecure: false, + ClientCert: testFileData, + ClientCertFilePath: testFilePath, + ClientKey: testFileData, + ClientKeyFilePath: testFilePath, + RootCAs: []string{rootCaFileData}, + RootCAFilePaths: []string{rootCACertFilePath}, + ProxyAddress: "proxy_address", + ServerName: "server_name", }, }, } @@ -146,9 +175,56 @@ allow_insecure = true if tt.expectedErr != nil { assert.Equal(t, tt.expectedErr, err) } else { - assert.Equal(t, tt.expectedSettings, settings) assert.NoError(t, err) + assert.Equal(t, tt.expectedSettings, settings) } }) } } + +func createTestRootCAFile(t *testing.T, path string) string { + t.Helper() + + ca := &x509.Certificate{ + SerialNumber: big.NewInt(2019), + Subject: pkix.Name{ + Organization: []string{"Grafana Labs"}, + CommonName: "Grafana", + }, + NotBefore: time.Now(), + NotAfter: time.Now().AddDate(10, 0, 0), + IsCA: true, + ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth, x509.ExtKeyUsageServerAuth}, + KeyUsage: x509.KeyUsageDigitalSignature | x509.KeyUsageCertSign, + BasicConstraintsValid: true, + } + caPrivKey, err := rsa.GenerateKey(rand.Reader, 4096) + require.NoError(t, err) + caBytes, err := x509.CreateCertificate(rand.Reader, ca, ca, &caPrivKey.PublicKey, caPrivKey) + require.NoError(t, err) + + // nolint:gosec + caCertFile, err := os.Create(path) + require.NoError(t, err) + + block := &pem.Block{ + Type: "CERTIFICATE", + Bytes: caBytes, + } + err = pem.Encode(caCertFile, block) + require.NoError(t, err) + + buf := new(bytes.Buffer) + err = pem.Encode(buf, block) + require.NoError(t, err) + + return buf.String() +} + +func mustNewIniFile(fileContents string) *ini.File { + file, err := ini.Load([]byte(fileContents)) + if err != nil { + panic(fmt.Sprintf("creating ini file for test: %s", err)) + } + return file +}