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>
This commit is contained in:
co-authored by
Rao B V Chalapathi
Christopher Moyer
parent
5155455c55
commit
b9e53f628f
+64
-27
@@ -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)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user