mirror of
https://github.com/grafana/grafana.git
synced 2025-08-02 21:52:43 +08:00
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:
@ -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()
|
||||
|
68
pkg/util/tls.go
Normal file
68
pkg/util/tls.go
Normal file
@ -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, ",")
|
||||
}
|
26
pkg/util/tls_test.go
Normal file
26
pkg/util/tls_test.go
Normal file
@ -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