Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

HTTP: Add TLS version configurability for grafana server #67482

Merged
merged 18 commits into from May 8, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
70cf1c5
Add TLS version and cipher configurability for grafana server
chalapat Apr 28, 2023
e962bbb
Add TLS version and cipher configurability for grafana server
chalapat Apr 28, 2023
24c7c06
Merge branch 'add_tlsversion_configurability' of https://github.com/v…
venkatbvc Apr 28, 2023
69060f0
Merge branch 'grafana:main' into add_tlsversion_configurability
venkatbvc Apr 28, 2023
36eb808
Merge branch 'grafana:main' into add_tlsversion_configurability
venkatbvc Apr 28, 2023
921321a
Merge branch 'add_tlsversion_configurability' of https://github.com/v…
venkatbvc Apr 28, 2023
257938b
Merge branch 'add_tlsversion_configurability' of https://github.com/v…
venkatbvc Apr 28, 2023
8bac7fd
Merge branch 'add_tlsversion_configurability' of https://github.com/v…
venkatbvc Apr 28, 2023
4fbae49
Merge branch 'grafana:main' into add_tlsversion_configurability
venkatbvc Apr 29, 2023
0b728b3
Merge branch 'grafana:main' into add_tlsversion_configurability
venkatbvc Apr 29, 2023
3482e63
Merge branch 'add_tlsversion_configurability' of https://github.com/v…
venkatbvc May 2, 2023
9a17915
Addressed comments given on _index.md
venkatbvc May 2, 2023
e70daab
Merge branch 'add_tlsversion_configurability' of https://github.com/v…
venkatbvc May 2, 2023
422bb39
Addressed the comments given to the documentation
chalapat May 3, 2023
90720b6
Update docs/sources/setup-grafana/configure-grafana/_index.md
venkatbvc May 3, 2023
5a51eca
Update docs/sources/setup-grafana/configure-grafana/_index.md
venkatbvc May 3, 2023
3413606
As per the comments supporting configurability only for TLS version
chalapat May 3, 2023
dcf7ba7
Merge branch 'add_tlsversion_configurability' of https://github.com/v…
venkatbvc May 4, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
3 changes: 3 additions & 0 deletions conf/defaults.ini
Expand Up @@ -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 =

Expand Down
3 changes: 3 additions & 0 deletions conf/sample.ini
Expand Up @@ -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 =

Expand Down
5 changes: 5 additions & 0 deletions docs/sources/setup-grafana/configure-grafana/_index.md
Expand Up @@ -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.
Expand Down
91 changes: 64 additions & 27 deletions pkg/api/http_server.go
Expand Up @@ -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"
)

Expand Down Expand Up @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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
}
57 changes: 3 additions & 54 deletions pkg/services/ldap/settings.go
@@ -1,17 +1,16 @@
package ldap

import (
"crypto/tls"
"fmt"
"os"
"strings"
"sync"

"github.com/BurntSushi/toml"

"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
Expand Down Expand Up @@ -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)
}
Expand Down Expand Up @@ -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
}
6 changes: 6 additions & 0 deletions pkg/setting/setting.go
Expand Up @@ -166,6 +166,7 @@ type Cfg struct {
ReadTimeout time.Duration
EnableGzip bool
EnforceDomain bool
MinTLSVersion string

// Security settings
SecretKey string
Expand Down Expand Up @@ -1800,6 +1801,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")
Expand Down
15 changes: 15 additions & 0 deletions pkg/setting/setting_test.go
Expand Up @@ -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) {
Expand Down Expand Up @@ -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()
Expand Down
68 changes: 68 additions & 0 deletions 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, ",")
}
26 changes: 26 additions & 0 deletions 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)
}
}