From 0f46208cc115809a68e40be355999f595a2b859b Mon Sep 17 00:00:00 2001 From: Chelsea Holland Komlo Date: Tue, 8 May 2018 16:32:07 -0400 Subject: [PATCH 1/4] allow configurable cipher suites disallow 3DES and RC4 ciphers add documentation for tls_cipher_suites --- client/client.go | 9 +- command/agent/config_parse.go | 6 ++ helper/tlsutil/config.go | 70 +++++++++++++++- helper/tlsutil/config_test.go | 83 +++++++++++++++++++ nomad/server.go | 6 +- nomad/structs/config/tls.go | 4 + nomad/structs/config/tls_test.go | 7 +- .../docs/agent/configuration/tls.html.md | 6 ++ 8 files changed, 181 insertions(+), 10 deletions(-) diff --git a/client/client.go b/client/client.go index a3e8dad6b6dd..2db927e6d73e 100644 --- a/client/client.go +++ b/client/client.go @@ -399,11 +399,16 @@ func (c *Client) init() error { func (c *Client) reloadTLSConnections(newConfig *nconfig.TLSConfig) error { var tlsWrap tlsutil.RegionWrapper if newConfig != nil && newConfig.EnableRPC { - tw, err := tlsutil.NewTLSConfiguration(newConfig).OutgoingTLSWrapper() + tw, err := tlsutil.NewTLSConfiguration(newConfig) if err != nil { return err } - tlsWrap = tw + + twWrap, err := tw.OutgoingTLSWrapper() + if err != nil { + return err + } + tlsWrap = twWrap } // Store the new tls wrapper. diff --git a/command/agent/config_parse.go b/command/agent/config_parse.go index 5239e8cd511e..37ea60dc609b 100644 --- a/command/agent/config_parse.go +++ b/command/agent/config_parse.go @@ -13,6 +13,7 @@ import ( "github.com/hashicorp/hcl" "github.com/hashicorp/hcl/hcl/ast" "github.com/hashicorp/nomad/helper" + "github.com/hashicorp/nomad/helper/tlsutil" "github.com/hashicorp/nomad/nomad/structs/config" "github.com/mitchellh/mapstructure" ) @@ -760,6 +761,7 @@ func parseTLSConfig(result **config.TLSConfig, list *ast.ObjectList) error { "cert_file", "key_file", "verify_https_client", + "tls_cipher_suites", } if err := helper.CheckHCLKeys(listVal, valid); err != nil { @@ -776,6 +778,10 @@ func parseTLSConfig(result **config.TLSConfig, list *ast.ObjectList) error { return err } + if _, err := tlsutil.ParseCiphers(tlsConfig.TLSCipherSuites); err != nil { + return err + } + *result = &tlsConfig return nil } diff --git a/helper/tlsutil/config.go b/helper/tlsutil/config.go index 8f6b1f01df3e..1bd130e1bf59 100644 --- a/helper/tlsutil/config.go +++ b/helper/tlsutil/config.go @@ -6,6 +6,7 @@ import ( "fmt" "io/ioutil" "net" + "strings" "time" "github.com/hashicorp/nomad/nomad/structs/config" @@ -65,9 +66,18 @@ type Config struct { // KeyLoader dynamically reloads TLS configuration. KeyLoader *config.KeyLoader + + // CipherSuites have a default safe configuration, or operators can override + // these values for acceptable safe alternatives. + CipherSuites []uint16 } -func NewTLSConfiguration(newConf *config.TLSConfig) *Config { +func NewTLSConfiguration(newConf *config.TLSConfig) (*Config, error) { + ciphers, err := ParseCiphers(newConf.TLSCipherSuites) + if err != nil { + return nil, err + } + return &Config{ VerifyIncoming: true, VerifyOutgoing: true, @@ -76,7 +86,8 @@ func NewTLSConfiguration(newConf *config.TLSConfig) *Config { CertFile: newConf.CertFile, KeyFile: newConf.KeyFile, KeyLoader: newConf.GetKeyLoader(), - } + CipherSuites: ciphers, + }, nil } // AppendCA opens and parses the CA file and adds the certificates to @@ -132,6 +143,7 @@ func (c *Config) OutgoingTLSConfig() (*tls.Config, error) { tlsConfig := &tls.Config{ RootCAs: x509.NewCertPool(), InsecureSkipVerify: true, + CipherSuites: c.CipherSuites, } if c.VerifyServerHostname { tlsConfig.InsecureSkipVerify = false @@ -248,8 +260,9 @@ func WrapTLSClient(conn net.Conn, tlsConfig *tls.Config) (net.Conn, error) { func (c *Config) IncomingTLSConfig() (*tls.Config, error) { // Create the tlsConfig tlsConfig := &tls.Config{ - ClientCAs: x509.NewCertPool(), - ClientAuth: tls.NoClientCert, + ClientCAs: x509.NewCertPool(), + ClientAuth: tls.NoClientCert, + CipherSuites: c.CipherSuites, } // Parse the CA cert if any @@ -279,3 +292,52 @@ func (c *Config) IncomingTLSConfig() (*tls.Config, error) { return tlsConfig, nil } + +// ParseCiphers parses ciphersuites from the comma-separated string into +// recognized slice +func ParseCiphers(cipherStr string) ([]uint16, error) { + suites := []uint16{} + + cipherStr = strings.TrimSpace(cipherStr) + + 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", + } + + } 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) + } + } + + return suites, nil +} diff --git a/helper/tlsutil/config_test.go b/helper/tlsutil/config_test.go index ffc3f54ec6c1..6b3ec95a6dc2 100644 --- a/helper/tlsutil/config_test.go +++ b/helper/tlsutil/config_test.go @@ -3,14 +3,17 @@ package tlsutil import ( "crypto/tls" "crypto/x509" + "fmt" "io" "io/ioutil" "net" + "strings" "testing" "github.com/hashicorp/nomad/nomad/structs/config" "github.com/hashicorp/yamux" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) const ( @@ -412,3 +415,83 @@ func TestConfig_IncomingTLS_NoVerify(t *testing.T) { t.Fatalf("unexpected client cert") } } + +func TestConfig_ParseCiphers_Valid(t *testing.T) { + require := require.New(t) + + validCiphers := strings.Join([]string{ + "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", + }, ",") + + expectedCiphers := []uint16{ + tls.TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305, + tls.TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305, + tls.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256, + tls.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256, + tls.TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384, + tls.TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384, + tls.TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA256, + tls.TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA, + tls.TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA256, + tls.TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA, + tls.TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA, + tls.TLS_ECDHE_ECDSA_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_SHA256, + tls.TLS_RSA_WITH_AES_128_CBC_SHA, + tls.TLS_RSA_WITH_AES_256_CBC_SHA, + } + + parsedCiphers, err := ParseCiphers(validCiphers) + require.Nil(err) + require.Equal(parsedCiphers, expectedCiphers) +} + +func TestConfig_ParseCiphers_Default(t *testing.T) { + require := require.New(t) + + expectedCiphers := []uint16{ + tls.TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305, + tls.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256, + tls.TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384, + } + + parsedCiphers, err := ParseCiphers("") + require.Nil(err) + require.Equal(parsedCiphers, expectedCiphers) +} + +func TestConfig_ParseCiphers_Invalid(t *testing.T) { + require := require.New(t) + + invalidCiphers := []string{"TLS_RSA_WITH_3DES_EDE_CBC_SHA", + "TLS_ECDHE_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", + } + + for _, cipher := range invalidCiphers { + parsedCiphers, err := ParseCiphers(cipher) + require.NotNil(err) + require.Equal(fmt.Sprintf("unsupported cipher %q", cipher), err.Error()) + require.Equal(0, len(parsedCiphers)) + } +} diff --git a/nomad/server.go b/nomad/server.go index f6fbea91de60..f51ec4a41299 100644 --- a/nomad/server.go +++ b/nomad/server.go @@ -450,7 +450,11 @@ func (s *Server) reloadTLSConnections(newTLSConfig *config.TLSConfig) error { return fmt.Errorf("can't reload uninitialized RPC listener") } - tlsConf := tlsutil.NewTLSConfiguration(newTLSConfig) + tlsConf, err := tlsutil.NewTLSConfiguration(newTLSConfig) + if err != nil { + return err + } + incomingTLS, tlsWrap, err := getTLSConf(newTLSConfig.EnableRPC, tlsConf) if err != nil { s.logger.Printf("[ERR] nomad: unable to reset TLS context %s", err) diff --git a/nomad/structs/config/tls.go b/nomad/structs/config/tls.go index f40866c015b3..eb5300123ec1 100644 --- a/nomad/structs/config/tls.go +++ b/nomad/structs/config/tls.go @@ -55,6 +55,10 @@ type TLSConfig struct { // Checksum is a MD5 hash of the certificate CA File, Certificate file, and // key file. Checksum string + + // TLSCipherSuites are operator-defined ciphers to be used in Nomad TLS + // connections + TLSCipherSuites string `mapstructure:"tls_cipher_suites"` } type KeyLoader struct { diff --git a/nomad/structs/config/tls_test.go b/nomad/structs/config/tls_test.go index 698855e08083..75d85b782b9f 100644 --- a/nomad/structs/config/tls_test.go +++ b/nomad/structs/config/tls_test.go @@ -171,9 +171,10 @@ func TestTLS_Copy(t *testing.T) { fookey = "../../../helper/tlsutil/testdata/nomad-foo-key.pem" ) a := &TLSConfig{ - CAFile: cafile, - CertFile: foocert, - KeyFile: fookey, + CAFile: cafile, + CertFile: foocert, + KeyFile: fookey, + TLSCipherSuites: "TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305", } a.SetChecksum() diff --git a/website/source/docs/agent/configuration/tls.html.md b/website/source/docs/agent/configuration/tls.html.md index b816f7f21ce0..bb62c2bbb112 100644 --- a/website/source/docs/agent/configuration/tls.html.md +++ b/website/source/docs/agent/configuration/tls.html.md @@ -58,6 +58,12 @@ the [Agent's Gossip and RPC Encryption](/docs/agent/encryption.html). cluster is being upgraded to TLS, and removed after the migration is complete. This allows the agent to accept both TLS and plaintext traffic. +- `tls_cipher_suites` - Specifies the TLS cipher suites that will be used by + the agent. Known insecure ciphers are disabled (3DES and RC4). By default, + an agent is configured to use TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305, + TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256, and + TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384. + - `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. From 509180ee00f2bfd57637462bd126b20853b311d1 Mon Sep 17 00:00:00 2001 From: Chelsea Holland Komlo Date: Wed, 9 May 2018 16:30:02 -0400 Subject: [PATCH 2/4] add support for configurable TLS minimum version --- command/agent/config_parse.go | 5 + helper/tlsutil/config.go | 93 +++++++++++++------ helper/tlsutil/config_test.go | 38 +++++++- nomad/structs/config/tls.go | 7 ++ .../docs/agent/configuration/tls.html.md | 3 + 5 files changed, 116 insertions(+), 30 deletions(-) diff --git a/command/agent/config_parse.go b/command/agent/config_parse.go index 37ea60dc609b..b9577327a6b3 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 1bd130e1bf59..ab7b17962462 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 6b3ec95a6dc2..6098acc6dc28 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 eb5300123ec1..5d893e9494b1 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 bb62c2bbb112..8979610a731f 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. From 793e7caaf9dbc4dd6226428f807234b33b76d15f Mon Sep 17 00:00:00 2001 From: Chelsea Holland Komlo Date: Thu, 10 May 2018 11:51:54 -0400 Subject: [PATCH 3/4] log error if unable to create TLS configuration --- nomad/server.go | 1 + 1 file changed, 1 insertion(+) diff --git a/nomad/server.go b/nomad/server.go index f51ec4a41299..1185b1853705 100644 --- a/nomad/server.go +++ b/nomad/server.go @@ -452,6 +452,7 @@ func (s *Server) reloadTLSConnections(newTLSConfig *config.TLSConfig) error { tlsConf, err := tlsutil.NewTLSConfiguration(newTLSConfig) if err != nil { + s.logger.Printf("[ERR] nomad: unable to create TLS configuration %s", err) return err } From 8daaa662760188fc02d29d40b3461d87fd02b3ae Mon Sep 17 00:00:00 2001 From: Chelsea Holland Komlo Date: Fri, 11 May 2018 07:48:31 -0400 Subject: [PATCH 4/4] add changelog --- CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index fbe3bc1b69cd..fb19aa01aeae 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,8 @@ IMPROVEMENTS: image pulls [[GH-4192](https://github.com/hashicorp/nomad/issues/4192)] * env: Default interpolation of optional meta fields of parameterized jobs to an empty string rather than the field key. [[GH-3720](https://github.com/hashicorp/nomad/issues/3720)] + * core: Add the option for operators to configure TLS versions and allowed + cipher suites. Default is a subset of safe ciphers and TLS 1.2 [[GH-4269](https://github.com/hashicorp/nomad/pull/4269)] * core: Add a new [progress_deadline](https://www.nomadproject.io/docs/job-specification/update.html#progress_deadline) parameter to support rescheduling failed allocations during a deployment. This allows operators to specify a configurable deadline before which a deployment should see healthy allocations [[GH-4259](https://github.com/hashicorp/nomad/issues/4259)]