HTTP: Add TLS version configurability for Grafana server (#67482)

Co-authored-by: Rao B V Chalapathi <b_v_chalapathi.rao@nokia.com>
Co-authored-by: Christopher Moyer <35463610+chri2547@users.noreply.github.com>
pull/68024/head
venkatbvc 2 years ago committed by GitHub
parent 5155455c55
commit b9e53f628f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 3
      conf/defaults.ini
  2. 3
      conf/sample.ini
  3. 5
      docs/sources/setup-grafana/configure-grafana/_index.md
  4. 91
      pkg/api/http_server.go
  5. 57
      pkg/services/ldap/settings.go
  6. 6
      pkg/setting/setting.go
  7. 15
      pkg/setting/setting_test.go
  8. 68
      pkg/util/tls.go
  9. 26
      pkg/util/tls_test.go

@ -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 =

@ -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 =

@ -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.

@ -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
}

@ -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
}

@ -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")

@ -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()

@ -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, ",")
}

@ -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)
}
}
Loading…
Cancel
Save