From 149d2211915c485ed0244c4913a265d000a84b28 Mon Sep 17 00:00:00 2001 From: Stanislav Putrya Date: Tue, 20 Aug 2019 01:01:01 +0200 Subject: [PATCH] Add capability to limit TLS versions and cipher suites (#6246) --- docs/TLS.md | 44 +++++++++ internal/tls/common.go | 34 +++++++ internal/tls/common_go112.go | 12 +++ internal/tls/config.go | 36 ++++++++ internal/tls/config_test.go | 91 +++++++++++++++++++ internal/tls/utils.go | 30 ++++++ plugins/outputs/prometheus_client/README.md | 8 ++ .../prometheus_client/prometheus_client.go | 2 + .../prometheus_client_tls_test.go | 15 ++- testutil/tls.go | 15 +++ 10 files changed, 286 insertions(+), 1 deletion(-) create mode 100644 docs/TLS.md create mode 100644 internal/tls/common.go create mode 100644 internal/tls/common_go112.go create mode 100644 internal/tls/utils.go diff --git a/docs/TLS.md b/docs/TLS.md new file mode 100644 index 000000000..0af0c384b --- /dev/null +++ b/docs/TLS.md @@ -0,0 +1,44 @@ +# TLS settings + +TLS for output plugin will be used if you provide options `tls_cert` and `tls_key`. +Settings that can be used to configure TLS: + +- `tls_cert` - path to certificate. Type: `string`. Ex. `tls_cert = "/etc/ssl/telegraf.crt"` +- `tls_key` - path to key. Type: `string`, Ex. `tls_key = "/etc/ssl/telegraf.key"` +- `tls_allowed_cacerts` - Set one or more allowed client CA certificate file names to enable mutually authenticated TLS connections. Type: `list`. Ex. `tls_allowed_cacerts = ["/etc/telegraf/clientca.pem"]` +- `tls_cipher_suites`- Define list of ciphers that will be supported. If wasn't defined default will be used. Type: `list`. Ex. `tls_cipher_suites = ["TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305", "TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305", "TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256", "TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256", "TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384", "TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384", "TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA256", "TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA", "TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA256", "TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA", "TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA", "TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA", "TLS_RSA_WITH_AES_128_GCM_SHA256", "TLS_RSA_WITH_AES_256_GCM_SHA384", "TLS_RSA_WITH_AES_128_CBC_SHA256", "TLS_RSA_WITH_AES_128_CBC_SHA", "TLS_RSA_WITH_AES_256_CBC_SHA"]` +- `tls_min_version` - Minimum TLS version that is acceptable. If wasn't defined default (TLS 1.0) will be used. Type: `string`. Ex. `tls_min_version = "TLS11"` +- `tls_max_version` - Maximum SSL/TLS version that is acceptable. If not set, then the maximum version supported is used, which is currently TLS 1.2 (for go < 1.12) or TLS 1.3 (for go == 1.12). Ex. `tls_max_version = "TLS12"` + +tls ciphers are supported: +- TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305 +- TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305 +- TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256 +- TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256 +- TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384 +- TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384 +- TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA256 +- TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA +- TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA256 +- TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA +- TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA +- TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA +- TLS_RSA_WITH_AES_128_GCM_SHA256 +- TLS_RSA_WITH_AES_256_GCM_SHA384 +- TLS_RSA_WITH_AES_128_CBC_SHA256 +- TLS_RSA_WITH_AES_128_CBC_SHA +- TLS_RSA_WITH_AES_256_CBC_SHA +- TLS_ECDHE_RSA_WITH_3DES_EDE_CBC_SHA +- TLS_RSA_WITH_3DES_EDE_CBC_SHA +- TLS_RSA_WITH_RC4_128_SHA +- TLS_ECDHE_RSA_WITH_RC4_128_SHA +- TLS_ECDHE_ECDSA_WITH_RC4_128_SHA +- TLS_AES_128_GCM_SHA256 (only if version go1.12 was used for make build) +- TLS_AES_256_GCM_SHA384 (only if version go1.12 was used for make build) +- TLS_CHACHA20_POLY1305_SHA256 (only if version go1.12 was used for make build) + +TLS versions are supported: +- TLS10 +- TLS11 +- TLS12 +- TLS13 (only if version go1.12 was used for make build) diff --git a/internal/tls/common.go b/internal/tls/common.go new file mode 100644 index 000000000..3100a73a1 --- /dev/null +++ b/internal/tls/common.go @@ -0,0 +1,34 @@ +package tls + +import "crypto/tls" + +var tlsVersionMap = map[string]uint16{ + "TLS10": tls.VersionTLS10, + "TLS11": tls.VersionTLS11, + "TLS12": tls.VersionTLS12, +} + +var tlsCipherMap = map[string]uint16{ + "TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305": tls.TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305, + "TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305": tls.TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305, + "TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256": tls.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256, + "TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256": tls.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256, + "TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384": tls.TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384, + "TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384": tls.TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384, + "TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA256": tls.TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA256, + "TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA": tls.TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA, + "TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA256": tls.TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA256, + "TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA": tls.TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA, + "TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA": tls.TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA, + "TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA": tls.TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA, + "TLS_RSA_WITH_AES_128_GCM_SHA256": tls.TLS_RSA_WITH_AES_128_GCM_SHA256, + "TLS_RSA_WITH_AES_256_GCM_SHA384": tls.TLS_RSA_WITH_AES_256_GCM_SHA384, + "TLS_RSA_WITH_AES_128_CBC_SHA256": tls.TLS_RSA_WITH_AES_128_CBC_SHA256, + "TLS_RSA_WITH_AES_128_CBC_SHA": tls.TLS_RSA_WITH_AES_128_CBC_SHA, + "TLS_RSA_WITH_AES_256_CBC_SHA": tls.TLS_RSA_WITH_AES_256_CBC_SHA, + "TLS_ECDHE_RSA_WITH_3DES_EDE_CBC_SHA": tls.TLS_ECDHE_RSA_WITH_3DES_EDE_CBC_SHA, + "TLS_RSA_WITH_3DES_EDE_CBC_SHA": tls.TLS_RSA_WITH_3DES_EDE_CBC_SHA, + "TLS_RSA_WITH_RC4_128_SHA": tls.TLS_RSA_WITH_RC4_128_SHA, + "TLS_ECDHE_RSA_WITH_RC4_128_SHA": tls.TLS_ECDHE_RSA_WITH_RC4_128_SHA, + "TLS_ECDHE_ECDSA_WITH_RC4_128_SHA": tls.TLS_ECDHE_ECDSA_WITH_RC4_128_SHA, +} diff --git a/internal/tls/common_go112.go b/internal/tls/common_go112.go new file mode 100644 index 000000000..988d6f936 --- /dev/null +++ b/internal/tls/common_go112.go @@ -0,0 +1,12 @@ +// +build go1.12 + +package tls + +import "crypto/tls" + +func init() { + tlsVersionMap["TLS13"] = tls.VersionTLS13 + tlsCipherMap["TLS_AES_128_GCM_SHA256"] = tls.TLS_AES_128_GCM_SHA256 + tlsCipherMap["TLS_AES_256_GCM_SHA384"] = tls.TLS_AES_256_GCM_SHA384 + tlsCipherMap["TLS_CHACHA20_POLY1305_SHA256"] = tls.TLS_CHACHA20_POLY1305_SHA256 +} diff --git a/internal/tls/config.go b/internal/tls/config.go index ce7958343..185c92cd0 100644 --- a/internal/tls/config.go +++ b/internal/tls/config.go @@ -5,6 +5,7 @@ import ( "crypto/x509" "fmt" "io/ioutil" + "strings" ) // ClientConfig represents the standard client TLS config. @@ -25,6 +26,9 @@ type ServerConfig struct { TLSCert string `toml:"tls_cert"` TLSKey string `toml:"tls_key"` TLSAllowedCACerts []string `toml:"tls_allowed_cacerts"` + TLSCipherSuites []string `toml:"tls_cipher_suites"` + TLSMinVersion string `toml:"tls_min_version"` + TLSMaxVersion string `toml:"tls_max_version"` } // TLSConfig returns a tls.Config, may be nil without error if TLS is not @@ -97,6 +101,38 @@ func (c *ServerConfig) TLSConfig() (*tls.Config, error) { } } + if len(c.TLSCipherSuites) != 0 { + cipherSuites, err := ParseCiphers(c.TLSCipherSuites) + if err != nil { + return nil, fmt.Errorf( + "could not parse server cipher suites %s: %v", strings.Join(c.TLSCipherSuites, ","), err) + } + tlsConfig.CipherSuites = cipherSuites + } + + if c.TLSMaxVersion != "" { + version, err := ParseTLSVersion(c.TLSMaxVersion) + if err != nil { + return nil, fmt.Errorf( + "could not parse tls max version %q: %v", c.TLSMaxVersion, err) + } + tlsConfig.MaxVersion = version + } + + if c.TLSMinVersion != "" { + version, err := ParseTLSVersion(c.TLSMinVersion) + if err != nil { + return nil, fmt.Errorf( + "could not parse tls min version %q: %v", c.TLSMinVersion, err) + } + tlsConfig.MinVersion = version + } + + if tlsConfig.MinVersion != 0 && tlsConfig.MaxVersion != 0 && tlsConfig.MinVersion > tlsConfig.MaxVersion { + return nil, fmt.Errorf( + "tls min version %q can't be greater then tls max version %q", tlsConfig.MinVersion, tlsConfig.MaxVersion) + } + return tlsConfig, nil } diff --git a/internal/tls/config_test.go b/internal/tls/config_test.go index 31a70d9a1..d7d75236e 100644 --- a/internal/tls/config_test.go +++ b/internal/tls/config_test.go @@ -123,6 +123,47 @@ func TestServerConfig(t *testing.T) { TLSCert: pki.ServerCertPath(), TLSKey: pki.ServerKeyPath(), TLSAllowedCACerts: []string{pki.CACertPath()}, + TLSCipherSuites: []string{pki.CipherSuite()}, + TLSMinVersion: pki.TLSMinVersion(), + TLSMaxVersion: pki.TLSMaxVersion(), + }, + }, + { + name: "missing tls cipher suites is okay", + server: tls.ServerConfig{ + TLSCert: pki.ServerCertPath(), + TLSKey: pki.ServerKeyPath(), + TLSAllowedCACerts: []string{pki.CACertPath()}, + TLSCipherSuites: []string{pki.CipherSuite()}, + }, + }, + { + name: "missing tls max version is okay", + server: tls.ServerConfig{ + TLSCert: pki.ServerCertPath(), + TLSKey: pki.ServerKeyPath(), + TLSAllowedCACerts: []string{pki.CACertPath()}, + TLSCipherSuites: []string{pki.CipherSuite()}, + TLSMaxVersion: pki.TLSMaxVersion(), + }, + }, + { + name: "missing tls min version is okay", + server: tls.ServerConfig{ + TLSCert: pki.ServerCertPath(), + TLSKey: pki.ServerKeyPath(), + TLSAllowedCACerts: []string{pki.CACertPath()}, + TLSCipherSuites: []string{pki.CipherSuite()}, + TLSMinVersion: pki.TLSMinVersion(), + }, + }, + { + name: "missing tls min/max versions is okay", + server: tls.ServerConfig{ + TLSCert: pki.ServerCertPath(), + TLSKey: pki.ServerKeyPath(), + TLSAllowedCACerts: []string{pki.CACertPath()}, + TLSCipherSuites: []string{pki.CipherSuite()}, }, }, { @@ -172,6 +213,56 @@ func TestServerConfig(t *testing.T) { expNil: true, expErr: true, }, + { + name: "invalid cipher suites", + server: tls.ServerConfig{ + TLSCert: pki.ServerCertPath(), + TLSKey: pki.ServerKeyPath(), + TLSAllowedCACerts: []string{pki.CACertPath()}, + TLSCipherSuites: []string{pki.CACertPath()}, + }, + expNil: true, + expErr: true, + }, + { + name: "TLS Max Version less then TLS Min version", + server: tls.ServerConfig{ + TLSCert: pki.ServerCertPath(), + TLSKey: pki.ServerKeyPath(), + TLSAllowedCACerts: []string{pki.CACertPath()}, + TLSCipherSuites: []string{pki.CACertPath()}, + TLSMinVersion: pki.TLSMaxVersion(), + TLSMaxVersion: pki.TLSMinVersion(), + }, + expNil: true, + expErr: true, + }, + { + name: "invalid tls min version", + server: tls.ServerConfig{ + TLSCert: pki.ServerCertPath(), + TLSKey: pki.ServerKeyPath(), + TLSAllowedCACerts: []string{pki.CACertPath()}, + TLSCipherSuites: []string{pki.CipherSuite()}, + TLSMinVersion: pki.ServerKeyPath(), + TLSMaxVersion: pki.TLSMaxVersion(), + }, + expNil: true, + expErr: true, + }, + { + name: "invalid tls max version", + server: tls.ServerConfig{ + TLSCert: pki.ServerCertPath(), + TLSKey: pki.ServerKeyPath(), + TLSAllowedCACerts: []string{pki.CACertPath()}, + TLSCipherSuites: []string{pki.CACertPath()}, + TLSMinVersion: pki.TLSMinVersion(), + TLSMaxVersion: pki.ServerCertPath(), + }, + expNil: true, + expErr: true, + }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { diff --git a/internal/tls/utils.go b/internal/tls/utils.go new file mode 100644 index 000000000..560d07ee2 --- /dev/null +++ b/internal/tls/utils.go @@ -0,0 +1,30 @@ +package tls + +import ( + "fmt" +) + +// ParseCiphers returns a `[]uint16` by received `[]string` key that represents ciphers from crypto/tls. +// If some of ciphers in received list doesn't exists ParseCiphers returns nil with error +func ParseCiphers(ciphers []string) ([]uint16, error) { + suites := []uint16{} + + for _, cipher := range ciphers { + if v, ok := tlsCipherMap[cipher]; ok { + suites = append(suites, v) + } else { + return nil, fmt.Errorf("unsupported cipher %q", cipher) + } + } + + return suites, nil +} + +// ParseTLSVersion returns a `uint16` by received version string key that represents tls version from crypto/tls. +// If version isn't supportes ParseTLSVersion returns 0 with error +func ParseTLSVersion(version string) (uint16, error) { + if v, ok := tlsVersionMap[version]; ok { + return v, nil + } + return 0, fmt.Errorf("unsupported version %q", version) +} diff --git a/plugins/outputs/prometheus_client/README.md b/plugins/outputs/prometheus_client/README.md index d1b4a1b0e..967c01ee6 100644 --- a/plugins/outputs/prometheus_client/README.md +++ b/plugins/outputs/prometheus_client/README.md @@ -40,6 +40,14 @@ This plugin starts a [Prometheus](https://prometheus.io/) Client, it exposes all ## enable mutually authenticated TLS connections # tls_allowed_cacerts = ["/etc/telegraf/clientca.pem"] + ## contains the minimum SSL/TLS version that is acceptable. + ## If not set, then TLS 1.0 is taken as the minimum. + # tls_min_version = "TLS11" + + ## contains the maximum SSL/TLS version that is acceptable. + ## If not set, then the maximum supported version is used. + # tls_max_version = "TLS12" + ## Export metric collection time. # export_timestamp = false ``` diff --git a/plugins/outputs/prometheus_client/prometheus_client.go b/plugins/outputs/prometheus_client/prometheus_client.go index 32dcdbb89..f7b2ea966 100644 --- a/plugins/outputs/prometheus_client/prometheus_client.go +++ b/plugins/outputs/prometheus_client/prometheus_client.go @@ -117,6 +117,8 @@ var sampleConfig = ` ## enable mutually authenticated TLS connections # tls_allowed_cacerts = ["/etc/telegraf/clientca.pem"] + # tls_cipher_suites = ["TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305", "TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305", "TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256", "TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256", "TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384", "TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384", "TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA256", "TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA", "TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA256", "TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA", "TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA", "TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA", "TLS_RSA_WITH_AES_128_GCM_SHA256", "TLS_RSA_WITH_AES_256_GCM_SHA384", "TLS_RSA_WITH_AES_128_CBC_SHA256", "TLS_RSA_WITH_AES_128_CBC_SHA", "TLS_RSA_WITH_AES_256_CBC_SHA"] + ## Export metric collection time. # export_timestamp = false ` diff --git a/plugins/outputs/prometheus_client/prometheus_client_tls_test.go b/plugins/outputs/prometheus_client/prometheus_client_tls_test.go index bcf6b4381..bcbb4e70e 100644 --- a/plugins/outputs/prometheus_client/prometheus_client_tls_test.go +++ b/plugins/outputs/prometheus_client/prometheus_client_tls_test.go @@ -6,6 +6,7 @@ import ( "net/http" "testing" + inttls "github.com/influxdata/telegraf/internal/tls" "github.com/influxdata/telegraf/plugins/outputs/prometheus_client" "github.com/influxdata/telegraf/testutil" "github.com/influxdata/toml" @@ -19,7 +20,9 @@ var configWithTLS = fmt.Sprintf(` tls_allowed_cacerts = ["%s"] tls_cert = "%s" tls_key = "%s" -`, pki.TLSServerConfig().TLSAllowedCACerts[0], pki.TLSServerConfig().TLSCert, pki.TLSServerConfig().TLSKey) + tls_cipher_suites = ["%s"] + tls_min_version = "%s" +`, pki.TLSServerConfig().TLSAllowedCACerts[0], pki.TLSServerConfig().TLSCert, pki.TLSServerConfig().TLSKey, pki.CipherSuite(), pki.TLSMaxVersion()) var configWithoutTLS = ` listen = "127.0.0.1:0" @@ -50,12 +53,22 @@ func TestWorksWithTLS(t *testing.T) { require.NoError(t, err) defer tc.Output.Close() + serverCiphers, err := inttls.ParseCiphers(tc.Output.ServerConfig.TLSCipherSuites) + require.NoError(t, err) + require.Equal(t, 1, len(serverCiphers)) + + tlsVersion, err := inttls.ParseTLSVersion(tc.Output.ServerConfig.TLSMinVersion) + require.NoError(t, err) + response, err := tc.Client.Get(tc.Output.URL()) require.NoError(t, err) require.NoError(t, err) require.Equal(t, response.StatusCode, http.StatusOK) + require.Equal(t, response.TLS.CipherSuite, serverCiphers[0]) + require.Equal(t, response.TLS.Version, tlsVersion) + tr := &http.Transport{ TLSClientConfig: &tls.Config{InsecureSkipVerify: true}, } diff --git a/testutil/tls.go b/testutil/tls.go index 4f7fc012a..333db3838 100644 --- a/testutil/tls.go +++ b/testutil/tls.go @@ -30,6 +30,9 @@ func (p *pki) TLSServerConfig() *tls.ServerConfig { TLSAllowedCACerts: []string{p.CACertPath()}, TLSCert: p.ServerCertPath(), TLSKey: p.ServerKeyPath(), + TLSCipherSuites: []string{p.CipherSuite()}, + TLSMinVersion: p.TLSMinVersion(), + TLSMaxVersion: p.TLSMaxVersion(), } } @@ -41,6 +44,18 @@ func (p *pki) CACertPath() string { return path.Join(p.path, "cacert.pem") } +func (p *pki) CipherSuite() string { + return "TLS_RSA_WITH_3DES_EDE_CBC_SHA" +} + +func (p *pki) TLSMinVersion() string { + return "TLS11" +} + +func (p *pki) TLSMaxVersion() string { + return "TLS12" +} + func (p *pki) ReadClientCert() string { return readCertificate(p.ClientCertPath()) }