From b9e53f628fa65664e92353d46fe90d6de66e9ddf Mon Sep 17 00:00:00 2001 From: venkatbvc Date: Mon, 8 May 2023 20:41:36 +0530 Subject: [PATCH] HTTP: Add TLS version configurability for Grafana server (#67482) Co-authored-by: Rao B V Chalapathi Co-authored-by: Christopher Moyer <35463610+chri2547@users.noreply.github.com> --- conf/defaults.ini | 3 + conf/sample.ini | 3 + .../setup-grafana/configure-grafana/_index.md | 5 + pkg/api/http_server.go | 91 +++++++++++++------ pkg/services/ldap/settings.go | 57 +----------- pkg/setting/setting.go | 6 ++ pkg/setting/setting_test.go | 15 +++ pkg/util/tls.go | 68 ++++++++++++++ pkg/util/tls_test.go | 26 ++++++ 9 files changed, 193 insertions(+), 81 deletions(-) create mode 100644 pkg/util/tls.go create mode 100644 pkg/util/tls_test.go diff --git a/conf/defaults.ini b/conf/defaults.ini index 1066299459e..989d2324458 100644 --- a/conf/defaults.ini +++ b/conf/defaults.ini @@ -34,6 +34,9 @@ provisioning = conf/provisioning # Protocol (http, https, h2, socket) protocol = http +# Minimum TLS version allowed. By default, this value is empty. Accepted values are: TLS1.2, TLS1.3. If nothing is set TLS1.2 would be taken +min_tls_version = "" + # The ip address to bind to, empty will bind to all interfaces http_addr = diff --git a/conf/sample.ini b/conf/sample.ini index 91d4e908be5..41b56c46aa7 100644 --- a/conf/sample.ini +++ b/conf/sample.ini @@ -34,6 +34,9 @@ # Protocol (http, https, h2, socket) ;protocol = http +# This is the minimum TLS version allowed. By default, this value is empty. Accepted values are: TLS1.2, TLS1.3. If nothing is set TLS1.2 would be taken +;min_tls_version = "" + # The ip address to bind to, empty will bind to all interfaces ;http_addr = diff --git a/docs/sources/setup-grafana/configure-grafana/_index.md b/docs/sources/setup-grafana/configure-grafana/_index.md index 7eb24453425..15a877018f8 100644 --- a/docs/sources/setup-grafana/configure-grafana/_index.md +++ b/docs/sources/setup-grafana/configure-grafana/_index.md @@ -189,6 +189,11 @@ Folder that contains [provisioning]({{< relref "../../administration/provisionin `http`,`https`,`h2` or `socket` +### min_tls_version + +The TLS Handshake requires a minimum TLS version. The available options are TLS1.2 and TLS1.3. +If you do not specify a version, the system uses TLS1.2. + ### http_addr The host for the server to listen on. If your machine has more than one network interface, you can use this setting to expose the Grafana service on only one network interface and not have it available on others, such as the loopback interface. An empty value is equivalent to setting the value to `0.0.0.0`, which means the Grafana service binds to all interfaces. diff --git a/pkg/api/http_server.go b/pkg/api/http_server.go index b8dcc68310c..78a459389a8 100644 --- a/pkg/api/http_server.go +++ b/pkg/api/http_server.go @@ -99,6 +99,7 @@ import ( "github.com/grafana/grafana/pkg/services/user" "github.com/grafana/grafana/pkg/services/validations" "github.com/grafana/grafana/pkg/setting" + "github.com/grafana/grafana/pkg/util" "github.com/grafana/grafana/pkg/web" ) @@ -501,21 +502,22 @@ func (hs *HTTPServer) configureHttps() error { return fmt.Errorf(`cannot find SSL key_file at %q`, hs.Cfg.KeyFile) } + minTlsVersion, err := util.TlsNameToVersion(hs.Cfg.MinTLSVersion) + if err != nil { + return err + } + + tlsCiphers := hs.getDefaultCiphers(minTlsVersion, string(setting.HTTPSScheme)) + if err != nil { + return err + } + + hs.log.Info("HTTP Server TLS settings", "Min TLS Version", hs.Cfg.MinTLSVersion, + "configured ciphers", util.TlsCipherIdsToString(tlsCiphers)) + tlsCfg := &tls.Config{ - MinVersion: tls.VersionTLS12, - CipherSuites: []uint16{ - tls.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256, - tls.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256, - tls.TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384, - tls.TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384, - tls.TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA, - tls.TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA, - tls.TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA, - tls.TLS_RSA_WITH_AES_128_GCM_SHA256, - tls.TLS_RSA_WITH_AES_256_GCM_SHA384, - tls.TLS_RSA_WITH_AES_128_CBC_SHA, - tls.TLS_RSA_WITH_AES_256_CBC_SHA, - }, + MinVersion: minTlsVersion, + CipherSuites: tlsCiphers, } hs.httpSrv.TLSConfig = tlsCfg @@ -541,20 +543,20 @@ func (hs *HTTPServer) configureHttp2() error { return fmt.Errorf("cannot find SSL key_file at %q", hs.Cfg.KeyFile) } + minTlsVersion, err := util.TlsNameToVersion(hs.Cfg.MinTLSVersion) + if err != nil { + return err + } + + tlsCiphers := hs.getDefaultCiphers(minTlsVersion, string(setting.HTTP2Scheme)) + + hs.log.Info("HTTP Server TLS settings", "Min TLS Version", hs.Cfg.MinTLSVersion, + "configured ciphers", util.TlsCipherIdsToString(tlsCiphers)) + tlsCfg := &tls.Config{ - MinVersion: tls.VersionTLS12, - CipherSuites: []uint16{ - tls.TLS_CHACHA20_POLY1305_SHA256, - tls.TLS_AES_128_GCM_SHA256, - tls.TLS_AES_256_GCM_SHA384, - tls.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256, - tls.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256, - tls.TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384, - tls.TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384, - tls.TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305, - tls.TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305, - }, - NextProtos: []string{"h2", "http/1.1"}, + MinVersion: minTlsVersion, + CipherSuites: tlsCiphers, + NextProtos: []string{"h2", "http/1.1"}, } hs.httpSrv.TLSConfig = tlsCfg @@ -737,3 +739,38 @@ func (hs *HTTPServer) mapStatic(m *web.Mux, rootDir string, dir string, prefix s func (hs *HTTPServer) metricsEndpointBasicAuthEnabled() bool { return hs.Cfg.MetricsEndpointBasicAuthUsername != "" && hs.Cfg.MetricsEndpointBasicAuthPassword != "" } + +func (hs *HTTPServer) getDefaultCiphers(tlsVersion uint16, protocol string) []uint16 { + if tlsVersion != tls.VersionTLS12 { + return nil + } + if protocol == "https" { + return []uint16{ + tls.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256, + tls.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256, + tls.TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384, + tls.TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384, + tls.TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA, + tls.TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA, + tls.TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA, + tls.TLS_RSA_WITH_AES_128_GCM_SHA256, + tls.TLS_RSA_WITH_AES_256_GCM_SHA384, + tls.TLS_RSA_WITH_AES_128_CBC_SHA, + tls.TLS_RSA_WITH_AES_256_CBC_SHA, + } + } + if protocol == "h2" { + return []uint16{ + tls.TLS_CHACHA20_POLY1305_SHA256, + tls.TLS_AES_128_GCM_SHA256, + tls.TLS_AES_256_GCM_SHA384, + tls.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256, + tls.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256, + tls.TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384, + tls.TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384, + tls.TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305, + tls.TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305, + } + } + return nil +} diff --git a/pkg/services/ldap/settings.go b/pkg/services/ldap/settings.go index 142a17d854c..47abe75ece3 100644 --- a/pkg/services/ldap/settings.go +++ b/pkg/services/ldap/settings.go @@ -1,10 +1,8 @@ package ldap import ( - "crypto/tls" "fmt" "os" - "strings" "sync" "github.com/BurntSushi/toml" @@ -12,6 +10,7 @@ import ( "github.com/grafana/grafana/pkg/infra/log" "github.com/grafana/grafana/pkg/services/org" "github.com/grafana/grafana/pkg/setting" + "github.com/grafana/grafana/pkg/util" ) const defaultTimeout = 10 @@ -144,14 +143,14 @@ func readConfig(configFile string) (*Config, error) { } if server.MinTLSVersion != "" { - server.minTLSVersion, err = tlsNameToVersion(server.MinTLSVersion) + server.minTLSVersion, err = util.TlsNameToVersion(server.MinTLSVersion) if err != nil { logger.Error("Failed to set min TLS version. Ignoring", "err", err) } } if len(server.TLSCiphers) > 0 { - server.tlsCiphers, err = tlsCiphersToIDs(server.TLSCiphers) + server.tlsCiphers, err = util.TlsCiphersToIDs(server.TLSCiphers) if err != nil { logger.Error("Unrecognized TLS Cipher(s). Ignoring", "err", err) } @@ -191,53 +190,3 @@ func assertNotEmptyCfg(val interface{}, propName string) error { } return nil } - -// tlsNameToVersion converts a string to a tls version -func tlsNameToVersion(name string) (uint16, error) { - name = strings.ToUpper(name) - switch name { - case "TLS1.0": - return tls.VersionTLS10, nil - case "TLS1.1": - return tls.VersionTLS11, nil - case "TLS1.2": - return tls.VersionTLS12, nil - case "TLS1.3": - return tls.VersionTLS13, nil - } - - return 0, fmt.Errorf("unknown tls version: %q", name) -} - -// Cipher strings https://go.dev/src/crypto/tls/cipher_suites.go -// Ex: "TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256" or "TLS_RSA_WITH_AES_128_CBC_SHA" -func tlsCiphersToIDs(names []string) ([]uint16, error) { - if len(names) == 0 || names == nil { - // no ciphers specified, use defaults - return nil, nil - } - var ids []uint16 - var missing []string - - ciphers := tls.CipherSuites() - var cipherMap = make(map[string]uint16, len(ciphers)) - for _, cipher := range ciphers { - cipherMap[cipher.Name] = cipher.ID - } - - for _, name := range names { - name = strings.ToUpper(name) - id, ok := cipherMap[name] - if !ok { - missing = append(missing, name) - continue - } - ids = append(ids, id) - } - - if len(missing) > 0 { - return ids, fmt.Errorf("unknown ciphers: %v", missing) - } - - return ids, nil -} diff --git a/pkg/setting/setting.go b/pkg/setting/setting.go index 122d3ef2bf0..4865681deb5 100644 --- a/pkg/setting/setting.go +++ b/pkg/setting/setting.go @@ -166,6 +166,7 @@ type Cfg struct { ReadTimeout time.Duration EnableGzip bool EnforceDomain bool + MinTLSVersion string // Security settings SecretKey string @@ -1796,6 +1797,11 @@ func (cfg *Cfg) readServerSettings(iniFile *ini.File) error { cfg.SocketPath = server.Key("socket").String() } + cfg.MinTLSVersion = valueAsString(server, "min_tls_version", "TLS1.2") + if cfg.MinTLSVersion == "TLS1.0" || cfg.MinTLSVersion == "TLS1.1" { + return fmt.Errorf("TLS version not configured correctly:%v, allowed values are TLS1.2 and TLS1.3", cfg.MinTLSVersion) + } + cfg.Domain = valueAsString(server, "domain", "localhost") cfg.HTTPAddr = valueAsString(server, "http_addr", DefaultHTTPAddr) cfg.HTTPPort = valueAsString(server, "http_port", "3000") diff --git a/pkg/setting/setting_test.go b/pkg/setting/setting_test.go index 2bc539cbeb4..9ab2dd9cc98 100644 --- a/pkg/setting/setting_test.go +++ b/pkg/setting/setting_test.go @@ -31,6 +31,7 @@ func TestLoadingSettings(t *testing.T) { require.Equal(t, "admin", cfg.AdminUser) require.Equal(t, "http://localhost:3000/", cfg.RendererCallbackUrl) + require.Equal(t, "TLS1.2", cfg.MinTLSVersion) }) t.Run("default.ini should have no semi-colon commented entries", func(t *testing.T) { @@ -143,6 +144,20 @@ func TestLoadingSettings(t *testing.T) { require.Equal(t, "test2", cfg.Domain) }) + t.Run("Should be able to override TLS version via command line", func(t *testing.T) { + cfg := NewCfg() + err := cfg.Load(CommandLineArgs{ + HomePath: "../../", + Args: []string{ + "cfg:default.server.min_tls_version=TLS1.3", + }, + Config: filepath.Join(HomePath, "pkg/setting/testdata/override.ini"), + }) + require.Nil(t, err) + + require.Equal(t, "TLS1.3", cfg.MinTLSVersion) + }) + t.Run("Defaults can be overridden in specified config file", func(t *testing.T) { if runtime.GOOS == windows { cfg := NewCfg() diff --git a/pkg/util/tls.go b/pkg/util/tls.go new file mode 100644 index 00000000000..049d6cf12da --- /dev/null +++ b/pkg/util/tls.go @@ -0,0 +1,68 @@ +package util + +import ( + "crypto/tls" + "fmt" + "strings" +) + +// tlsNameToVersion converts a string to a tls version +func TlsNameToVersion(name string) (uint16, error) { + name = strings.ToUpper(name) + switch name { + case "TLS1.0": + return tls.VersionTLS10, nil + case "TLS1.1": + return tls.VersionTLS11, nil + case "TLS1.2": + return tls.VersionTLS12, nil + case "TLS1.3": + return tls.VersionTLS13, nil + } + + return 0, fmt.Errorf("unknown tls version: %q", name) +} + +// Cipher strings https://go.dev/src/crypto/tls/cipher_suites.go +// Ex: "TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256" or "TLS_RSA_WITH_AES_128_CBC_SHA" +func TlsCiphersToIDs(names []string) ([]uint16, error) { + if len(names) == 0 || names == nil { + // no ciphers specified, use defaults + return nil, nil + } + var ids []uint16 + var missing []string + + ciphers := tls.CipherSuites() + var cipherMap = make(map[string]uint16, len(ciphers)) + for _, cipher := range ciphers { + cipherMap[cipher.Name] = cipher.ID + } + + for _, name := range names { + name = strings.ToUpper(name) + id, ok := cipherMap[name] + if !ok { + missing = append(missing, name) + continue + } + ids = append(ids, id) + } + + if len(missing) > 0 { + return ids, fmt.Errorf("unknown ciphers: %v", missing) + } + + return ids, nil +} + +// tlsNameToVersion converts a tls version to a string +func TlsCipherIdsToString(ids []uint16) string { + var tlsCiphers []string + if len(ids) > 0 { + for _, cipher := range ids { + tlsCiphers = append(tlsCiphers, tls.CipherSuiteName(cipher)) + } + } + return strings.Join(tlsCiphers, ",") +} diff --git a/pkg/util/tls_test.go b/pkg/util/tls_test.go new file mode 100644 index 00000000000..8d9f402e64e --- /dev/null +++ b/pkg/util/tls_test.go @@ -0,0 +1,26 @@ +package util + +import ( + "crypto/tls" + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestTlsNameToVersion(t *testing.T) { + tests := []struct { + tlsVer string + expected uint16 + }{ + {"TLS1.0", tls.VersionTLS10}, + {"TLS1.1", tls.VersionTLS11}, + {"TLS1.2", tls.VersionTLS12}, + {"TLS1.3", tls.VersionTLS13}, + {"SSSL", 0}, + } + + for _, testcase := range tests { + verStr, _ := TlsNameToVersion(testcase.tlsVer) + assert.EqualValues(t, testcase.expected, verStr) + } +}