Add capability to limit TLS versions and cipher suites (#6246)

This commit is contained in:
Stanislav Putrya 2019-08-20 01:01:01 +02:00 committed by Daniel Nelson
parent fbfaf767f1
commit 149d221191
10 changed files with 286 additions and 1 deletions

44
docs/TLS.md Normal file
View File

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

34
internal/tls/common.go Normal file
View File

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

View File

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

View File

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

View File

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

30
internal/tls/utils.go Normal file
View File

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

View File

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

View File

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

View File

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

View File

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