diff --git a/command/agent/config_parse.go b/command/agent/config_parse.go index 37ea60dc609..b9577327a6b 100644 --- a/command/agent/config_parse.go +++ b/command/agent/config_parse.go @@ -762,6 +762,7 @@ func parseTLSConfig(result **config.TLSConfig, list *ast.ObjectList) error { "key_file", "verify_https_client", "tls_cipher_suites", + "tls_min_version", } if err := helper.CheckHCLKeys(listVal, valid); err != nil { @@ -782,6 +783,10 @@ func parseTLSConfig(result **config.TLSConfig, list *ast.ObjectList) error { return err } + if _, err := tlsutil.ParseMinVersion(tlsConfig.TLSMinVersion); err != nil { + return err + } + *result = &tlsConfig return nil } diff --git a/helper/tlsutil/config.go b/helper/tlsutil/config.go index 1bd130e1bf5..ab7b1796246 100644 --- a/helper/tlsutil/config.go +++ b/helper/tlsutil/config.go @@ -12,6 +12,40 @@ import ( "github.com/hashicorp/nomad/nomad/structs/config" ) +// supportedTLSVersions are the current TLS versions that Nomad supports +var supportedTLSVersions = map[string]uint16{ + "tls10": tls.VersionTLS10, + "tls11": tls.VersionTLS11, + "tls12": tls.VersionTLS12, +} + +// supportedTLSCiphers are the complete list of TLS ciphers supported by Nomad +var supportedTLSCiphers = 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, +} + +// defaultTLSCiphers are the TLS Ciphers that are supported by default +var defaultTLSCiphers = []string{"TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305", + "TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256", + "TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384", +} + // RegionSpecificWrapper is used to invoke a static Region and turns a // RegionWrapper into a Wrapper type. func RegionSpecificWrapper(region string, tlsWrap RegionWrapper) Wrapper { @@ -70,6 +104,9 @@ type Config struct { // CipherSuites have a default safe configuration, or operators can override // these values for acceptable safe alternatives. CipherSuites []uint16 + + // MinVersion contains the minimum SSL/TLS version that is accepted. + MinVersion uint16 } func NewTLSConfiguration(newConf *config.TLSConfig) (*Config, error) { @@ -78,6 +115,11 @@ func NewTLSConfiguration(newConf *config.TLSConfig) (*Config, error) { return nil, err } + minVersion, err := ParseMinVersion(newConf.TLSMinVersion) + if err != nil { + return nil, err + } + return &Config{ VerifyIncoming: true, VerifyOutgoing: true, @@ -87,6 +129,7 @@ func NewTLSConfiguration(newConf *config.TLSConfig) (*Config, error) { KeyFile: newConf.KeyFile, KeyLoader: newConf.GetKeyLoader(), CipherSuites: ciphers, + MinVersion: minVersion, }, nil } @@ -144,6 +187,7 @@ func (c *Config) OutgoingTLSConfig() (*tls.Config, error) { RootCAs: x509.NewCertPool(), InsecureSkipVerify: true, CipherSuites: c.CipherSuites, + MinVersion: c.MinVersion, } if c.VerifyServerHostname { tlsConfig.InsecureSkipVerify = false @@ -263,6 +307,7 @@ func (c *Config) IncomingTLSConfig() (*tls.Config, error) { ClientCAs: x509.NewCertPool(), ClientAuth: tls.NoClientCert, CipherSuites: c.CipherSuites, + MinVersion: c.MinVersion, } // Parse the CA cert if any @@ -302,42 +347,32 @@ func ParseCiphers(cipherStr string) ([]uint16, error) { var ciphers []string if cipherStr == "" { - // Set strong default values - ciphers = []string{"TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305", - "TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256", - "TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384", - } + ciphers = defaultTLSCiphers } else { ciphers = strings.Split(cipherStr, ",") } - - cipherMap := 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, - } for _, cipher := range ciphers { - if v, ok := cipherMap[cipher]; ok { - suites = append(suites, v) - } else { - return suites, fmt.Errorf("unsupported cipher %q", cipher) + c, ok := supportedTLSCiphers[cipher] + if !ok { + return suites, fmt.Errorf("unsupported TLS cipher %q", cipher) } + suites = append(suites, c) } return suites, nil } + +// ParseMinVersion parses the specified minimum TLS version for the Nomad agent +func ParseMinVersion(version string) (uint16, error) { + if version == "" { + return supportedTLSVersions["tls12"], nil + } + + vers, ok := supportedTLSVersions[version] + if !ok { + return 0, fmt.Errorf("unsupported TLS version %q", version) + } + + return vers, nil +} diff --git a/helper/tlsutil/config_test.go b/helper/tlsutil/config_test.go index 6b3ec95a6dc..6098acc6dc2 100644 --- a/helper/tlsutil/config_test.go +++ b/helper/tlsutil/config_test.go @@ -491,7 +491,43 @@ func TestConfig_ParseCiphers_Invalid(t *testing.T) { for _, cipher := range invalidCiphers { parsedCiphers, err := ParseCiphers(cipher) require.NotNil(err) - require.Equal(fmt.Sprintf("unsupported cipher %q", cipher), err.Error()) + require.Equal(fmt.Sprintf("unsupported TLS cipher %q", cipher), err.Error()) require.Equal(0, len(parsedCiphers)) } } + +func TestConfig_ParseMinVersion_Valid(t *testing.T) { + require := require.New(t) + + validVersions := []string{"tls10", + "tls11", + "tls12", + } + + expected := map[string]uint16{ + "tls10": tls.VersionTLS10, + "tls11": tls.VersionTLS11, + "tls12": tls.VersionTLS12, + } + + for _, version := range validVersions { + parsedVersion, err := ParseMinVersion(version) + require.Nil(err) + require.Equal(expected[version], parsedVersion) + } +} + +func TestConfig_ParseMinVersion_Invalid(t *testing.T) { + require := require.New(t) + + invalidVersions := []string{"tls13", + "tls15", + } + + for _, version := range invalidVersions { + parsedVersion, err := ParseMinVersion(version) + require.NotNil(err) + require.Equal(fmt.Sprintf("unsupported TLS version %q", version), err.Error()) + require.Equal(uint16(0), parsedVersion) + } +} diff --git a/nomad/structs/config/tls.go b/nomad/structs/config/tls.go index eb5300123ec..5d893e9494b 100644 --- a/nomad/structs/config/tls.go +++ b/nomad/structs/config/tls.go @@ -59,6 +59,10 @@ type TLSConfig struct { // TLSCipherSuites are operator-defined ciphers to be used in Nomad TLS // connections TLSCipherSuites string `mapstructure:"tls_cipher_suites"` + + // TLSMinVersion is used to set the minimum TLS version used for TLS + // connections. Should be either "tls10", "tls11", or "tls12". + TLSMinVersion string `mapstructure:"tls_min_version"` } type KeyLoader struct { @@ -151,6 +155,9 @@ func (t *TLSConfig) Copy() *TLSConfig { new.RPCUpgradeMode = t.RPCUpgradeMode new.VerifyHTTPSClient = t.VerifyHTTPSClient + new.TLSCipherSuites = t.TLSCipherSuites + new.TLSMinVersion = t.TLSMinVersion + new.SetChecksum() return new diff --git a/website/source/docs/agent/configuration/tls.html.md b/website/source/docs/agent/configuration/tls.html.md index bb62c2bbb11..8979610a731 100644 --- a/website/source/docs/agent/configuration/tls.html.md +++ b/website/source/docs/agent/configuration/tls.html.md @@ -64,6 +64,9 @@ the [Agent's Gossip and RPC Encryption](/docs/agent/encryption.html). TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256, and TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384. +- `tls_min_version` - Specifies the minimum supported version of TLS. Accepted + values are "tls10", "tls11", "tls12". Defaults to TLS 1.2. + - `verify_https_client` `(bool: false)` - Specifies agents should require client certificates for all incoming HTTPS requests. The client certificates must be signed by the same CA as Nomad.