From b70e419aeb8a13164b658d3fb316cc9a6eff0d26 Mon Sep 17 00:00:00 2001 From: Kyle Havlovitz Date: Thu, 27 Apr 2017 01:29:39 -0700 Subject: [PATCH] Add TLS cipher suite options and CA path support (#2963) This patch adds options to configure the available TLS cipher suites and adds support for a path for multiple CA certificates. Fixes #2959 --- command/agent/agent.go | 3 + command/agent/config.go | 30 ++++++ command/agent/config_test.go | 17 +++- command/agent/http.go | 19 ++-- consul/config.go | 33 +++++-- tlsutil/config.go | 97 +++++++++++++++++-- tlsutil/config_test.go | 59 +++++++++++ .../source/docs/agent/options.html.markdown | 13 +++ 8 files changed, 242 insertions(+), 29 deletions(-) diff --git a/command/agent/agent.go b/command/agent/agent.go index d7a6293f79..8988c7cfcf 100644 --- a/command/agent/agent.go +++ b/command/agent/agent.go @@ -453,11 +453,14 @@ func (a *Agent) consulConfig() *consul.Config { base.VerifyOutgoing = a.config.VerifyOutgoing base.VerifyServerHostname = a.config.VerifyServerHostname base.CAFile = a.config.CAFile + base.CAPath = a.config.CAPath base.CertFile = a.config.CertFile base.KeyFile = a.config.KeyFile base.ServerName = a.config.ServerName base.Domain = a.config.Domain base.TLSMinVersion = a.config.TLSMinVersion + base.TLSCipherSuites = a.config.TLSCipherSuites + base.TLSPreferServerCipherSuites = a.config.TLSPreferServerCipherSuites // Setup the ServerUp callback base.ServerUp = a.state.ConsulServerUp diff --git a/command/agent/config.go b/command/agent/config.go index 91e2299b34..9803858c3f 100644 --- a/command/agent/config.go +++ b/command/agent/config.go @@ -14,6 +14,7 @@ import ( "github.com/hashicorp/consul/consul" "github.com/hashicorp/consul/lib" + "github.com/hashicorp/consul/tlsutil" "github.com/hashicorp/consul/types" "github.com/hashicorp/consul/watch" "github.com/mitchellh/mapstructure" @@ -469,6 +470,10 @@ type Config struct { // or VerifyOutgoing to verify the TLS connection. CAFile string `mapstructure:"ca_file"` + // CAPath is a path to a directory of certificate authority files. This is used with + // VerifyIncoming or VerifyOutgoing to verify the TLS connection. + CAPath string `mapstructure:"ca_path"` + // CertFile is used to provide a TLS certificate that is used for serving TLS connections. // Must be provided to serve TLS connections. CertFile string `mapstructure:"cert_file"` @@ -484,6 +489,14 @@ type Config struct { // TLSMinVersion is used to set the minimum TLS version used for TLS connections. TLSMinVersion string `mapstructure:"tls_min_version"` + // TLSCipherSuites is used to specify the list of supported ciphersuites. + TLSCipherSuites []uint16 `mapstructure:"-" json:"-"` + TLSCipherSuitesRaw string `mapstructure:"tls_cipher_suites"` + + // TLSPreferServerCipherSuites specifies whether to prefer the server's ciphersuite + // over the client ciphersuites. + TLSPreferServerCipherSuites bool `mapstructure:"tls_prefer_server_cipher_suites"` + // StartJoin is a list of addresses to attempt to join when the // agent starts. If Serf is unable to communicate with any of these // addresses, then the agent will error and exit. @@ -1178,6 +1191,14 @@ func DecodeConfig(r io.Reader) (*Config, error) { return nil, fmt.Errorf("Performance.RaftMultiplier must be <= %d", consul.MaxRaftMultiplier) } + if raw := result.TLSCipherSuitesRaw; raw != "" { + ciphers, err := tlsutil.ParseCiphers(raw) + if err != nil { + return nil, fmt.Errorf("TLSCipherSuites invalid: %v", err) + } + result.TLSCipherSuites = ciphers + } + return &result, nil } @@ -1517,6 +1538,9 @@ func MergeConfig(a, b *Config) *Config { if b.CAFile != "" { result.CAFile = b.CAFile } + if b.CAPath != "" { + result.CAPath = b.CAPath + } if b.CertFile != "" { result.CertFile = b.CertFile } @@ -1529,6 +1553,12 @@ func MergeConfig(a, b *Config) *Config { if b.TLSMinVersion != "" { result.TLSMinVersion = b.TLSMinVersion } + if len(b.TLSCipherSuites) != 0 { + result.TLSCipherSuites = append(result.TLSCipherSuites, b.TLSCipherSuites...) + } + if b.TLSPreferServerCipherSuites { + result.TLSPreferServerCipherSuites = true + } if b.Checks != nil { result.Checks = append(result.Checks, b.Checks...) } diff --git a/command/agent/config_test.go b/command/agent/config_test.go index e55dbd3e0e..22590f77be 100644 --- a/command/agent/config_test.go +++ b/command/agent/config_test.go @@ -2,6 +2,7 @@ package agent import ( "bytes" + "crypto/tls" "encoding/base64" "io/ioutil" "net" @@ -354,7 +355,8 @@ func TestDecodeConfig(t *testing.T) { } // TLS - input = `{"verify_incoming": true, "verify_outgoing": true, "verify_server_hostname": true, "tls_min_version": "tls12"}` + input = `{"verify_incoming": true, "verify_outgoing": true, "verify_server_hostname": true, "tls_min_version": "tls12", + "tls_cipher_suites": "TLS_RSA_WITH_AES_256_CBC_SHA", "tls_prefer_server_cipher_suites": true}` config, err = DecodeConfig(bytes.NewReader([]byte(input))) if err != nil { t.Fatalf("err: %s", err) @@ -376,8 +378,16 @@ func TestDecodeConfig(t *testing.T) { t.Fatalf("bad: %#v", config) } + if len(config.TLSCipherSuites) != 1 || config.TLSCipherSuites[0] != tls.TLS_RSA_WITH_AES_256_CBC_SHA { + t.Fatalf("bad: %#v", config) + } + + if !config.TLSPreferServerCipherSuites { + t.Fatalf("bad: %#v", config) + } + // TLS keys - input = `{"ca_file": "my/ca/file", "cert_file": "my.cert", "key_file": "key.pem", "server_name": "example.com"}` + input = `{"ca_file": "my/ca/file", "ca_path":"my/ca/path", "cert_file": "my.cert", "key_file": "key.pem", "server_name": "example.com"}` config, err = DecodeConfig(bytes.NewReader([]byte(input))) if err != nil { t.Fatalf("err: %s", err) @@ -386,6 +396,9 @@ func TestDecodeConfig(t *testing.T) { if config.CAFile != "my/ca/file" { t.Fatalf("bad: %#v", config) } + if config.CAPath != "my/ca/path" { + t.Fatalf("bad: %#v", config) + } if config.CertFile != "my.cert" { t.Fatalf("bad: %#v", config) } diff --git a/command/agent/http.go b/command/agent/http.go index 2f26b9d98e..ac38dbcf6a 100644 --- a/command/agent/http.go +++ b/command/agent/http.go @@ -56,14 +56,17 @@ func NewHTTPServers(agent *Agent, config *Config, logOutput io.Writer) ([]*HTTPS } tlsConf := &tlsutil.Config{ - VerifyIncoming: config.VerifyIncoming, - VerifyOutgoing: config.VerifyOutgoing, - CAFile: config.CAFile, - CertFile: config.CertFile, - KeyFile: config.KeyFile, - NodeName: config.NodeName, - ServerName: config.ServerName, - TLSMinVersion: config.TLSMinVersion, + VerifyIncoming: config.VerifyIncoming, + VerifyOutgoing: config.VerifyOutgoing, + CAFile: config.CAFile, + CAPath: config.CAPath, + CertFile: config.CertFile, + KeyFile: config.KeyFile, + NodeName: config.NodeName, + ServerName: config.ServerName, + TLSMinVersion: config.TLSMinVersion, + CipherSuites: config.TLSCipherSuites, + PreferServerCipherSuites: config.TLSPreferServerCipherSuites, } tlsConfig, err := tlsConf.IncomingTLSConfig() diff --git a/consul/config.go b/consul/config.go index 1d3eb37a3b..576d5c3aa7 100644 --- a/consul/config.go +++ b/consul/config.go @@ -142,6 +142,10 @@ type Config struct { // or VerifyOutgoing to verify the TLS connection. CAFile string + // CAPath is a path to a directory of certificate authority files. This is used with + // VerifyIncoming or VerifyOutgoing to verify the TLS connection. + CAPath string + // CertFile is used to provide a TLS certificate that is used for serving TLS connections. // Must be provided to serve TLS connections. CertFile string @@ -157,6 +161,13 @@ type Config struct { // TLSMinVersion is used to set the minimum TLS version used for TLS connections. TLSMinVersion string + // TLSCipherSuites is used to specify the list of supported ciphersuites. + TLSCipherSuites []uint16 + + // TLSPreferServerCipherSuites specifies whether to prefer the server's ciphersuite + // over the client ciphersuites. + TLSPreferServerCipherSuites bool + // RejoinAfterLeave controls our interaction with Serf. // When set to false (default), a leave causes a Consul to not rejoin // the cluster until an explicit join is received. If this is set to @@ -421,16 +432,18 @@ func (c *Config) ScaleRaft(raftMultRaw uint) { // tlsConfig maps this config into a tlsutil config. func (c *Config) tlsConfig() *tlsutil.Config { tlsConf := &tlsutil.Config{ - VerifyIncoming: c.VerifyIncoming, - VerifyOutgoing: c.VerifyOutgoing, - VerifyServerHostname: c.VerifyServerHostname, - CAFile: c.CAFile, - CertFile: c.CertFile, - KeyFile: c.KeyFile, - NodeName: c.NodeName, - ServerName: c.ServerName, - Domain: c.Domain, - TLSMinVersion: c.TLSMinVersion, + VerifyIncoming: c.VerifyIncoming, + VerifyOutgoing: c.VerifyOutgoing, + VerifyServerHostname: c.VerifyServerHostname, + CAFile: c.CAFile, + CAPath: c.CAPath, + CertFile: c.CertFile, + KeyFile: c.KeyFile, + NodeName: c.NodeName, + ServerName: c.ServerName, + Domain: c.Domain, + TLSMinVersion: c.TLSMinVersion, + PreferServerCipherSuites: c.TLSPreferServerCipherSuites, } return tlsConf } diff --git a/tlsutil/config.go b/tlsutil/config.go index 0cb785bc30..b2224b843d 100644 --- a/tlsutil/config.go +++ b/tlsutil/config.go @@ -8,6 +8,8 @@ import ( "net" "strings" "time" + + "github.com/hashicorp/go-rootcerts" ) // DCWrapper is a function that is used to wrap a non-TLS connection @@ -51,6 +53,10 @@ type Config struct { // or VerifyOutgoing to verify the TLS connection. CAFile string + // CAPath is a path to a directory containing certificate authority files. This is used + // with VerifyIncoming or VerifyOutgoing to verify the TLS connection. + CAPath string + // CertFile is used to provide a TLS certificate that is used for serving TLS connections. // Must be provided to serve TLS connections. CertFile string @@ -71,6 +77,13 @@ type Config struct { // TLSMinVersion is the minimum accepted TLS version that can be used. TLSMinVersion string + + // CipherSuites is the list of TLS cipher suites to use. + CipherSuites []uint16 + + // PreferServerCipherSuites specifies whether to prefer the server's ciphersuite + // over the client ciphersuites. + PreferServerCipherSuites bool } // AppendCA opens and parses the CA file and adds the certificates to @@ -130,15 +143,24 @@ func (c *Config) OutgoingTLSConfig() (*tls.Config, error) { tlsConfig.ServerName = "VerifyServerHostname" tlsConfig.InsecureSkipVerify = false } + if len(c.CipherSuites) != 0 { + tlsConfig.CipherSuites = c.CipherSuites + } + if c.PreferServerCipherSuites { + tlsConfig.PreferServerCipherSuites = true + } // Ensure we have a CA if VerifyOutgoing is set - if c.VerifyOutgoing && c.CAFile == "" { + if c.VerifyOutgoing && c.CAFile == "" && c.CAPath == "" { return nil, fmt.Errorf("VerifyOutgoing set, and no CA certificate provided!") } - // Parse the CA cert if any - err := c.AppendCA(tlsConfig.RootCAs) - if err != nil { + // Parse the CA certs if any + rootConfig := &rootcerts.Config{ + CAFile: c.CAFile, + CAPath: c.CAPath, + } + if err := rootcerts.ConfigureTLS(tlsConfig, rootConfig); err != nil { return nil, err } @@ -305,10 +327,27 @@ func (c *Config) IncomingTLSConfig() (*tls.Config, error) { tlsConfig.ServerName = c.NodeName } - // Parse the CA cert if any - err := c.AppendCA(tlsConfig.ClientCAs) - if err != nil { - return nil, err + // Set the cipher suites + if len(c.CipherSuites) != 0 { + tlsConfig.CipherSuites = c.CipherSuites + } + if c.PreferServerCipherSuites { + tlsConfig.PreferServerCipherSuites = true + } + + // Parse the CA certs if any + if c.CAFile != "" { + pool, err := rootcerts.LoadCAFile(c.CAFile) + if err != nil { + return nil, err + } + tlsConfig.ClientCAs = pool + } else if c.CAPath != "" { + pool, err := rootcerts.LoadCAPath(c.CAPath) + if err != nil { + return nil, err + } + tlsConfig.ClientCAs = pool } // Add cert/key @@ -322,7 +361,7 @@ func (c *Config) IncomingTLSConfig() (*tls.Config, error) { // Check if we require verification if c.VerifyIncoming { tlsConfig.ClientAuth = tls.RequireAndVerifyClientCert - if c.CAFile == "" { + if c.CAFile == "" && c.CAPath == "" { return nil, fmt.Errorf("VerifyIncoming set, and no CA certificate provided!") } if cert == nil { @@ -340,3 +379,43 @@ func (c *Config) IncomingTLSConfig() (*tls.Config, error) { } return tlsConfig, nil } + +// ParseCiphers parse ciphersuites from the comma-separated string into recognized slice +func ParseCiphers(cipherStr string) ([]uint16, error) { + suites := []uint16{} + + cipherStr = strings.TrimSpace(cipherStr) + if cipherStr == "" { + return []uint16{}, nil + } + ciphers := strings.Split(cipherStr, ",") + + cipherMap := map[string]uint16{ + "TLS_RSA_WITH_RC4_128_SHA": tls.TLS_RSA_WITH_RC4_128_SHA, + "TLS_RSA_WITH_3DES_EDE_CBC_SHA": tls.TLS_RSA_WITH_3DES_EDE_CBC_SHA, + "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_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_ECDHE_ECDSA_WITH_RC4_128_SHA": tls.TLS_ECDHE_ECDSA_WITH_RC4_128_SHA, + "TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA": tls.TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA, + "TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA": tls.TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA, + "TLS_ECDHE_RSA_WITH_RC4_128_SHA": tls.TLS_ECDHE_RSA_WITH_RC4_128_SHA, + "TLS_ECDHE_RSA_WITH_3DES_EDE_CBC_SHA": tls.TLS_ECDHE_RSA_WITH_3DES_EDE_CBC_SHA, + "TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA": tls.TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA, + "TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA": tls.TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA, + "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, + } + 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/tlsutil/config_test.go b/tlsutil/config_test.go index 5d9af0ffca..efb73a2727 100644 --- a/tlsutil/config_test.go +++ b/tlsutil/config_test.go @@ -6,6 +6,8 @@ import ( "io" "io/ioutil" "net" + "reflect" + "strings" "testing" "github.com/hashicorp/yamux" @@ -37,6 +39,20 @@ func TestConfig_CACertificate_Valid(t *testing.T) { } } +func TestConfig_CAPath_Valid(t *testing.T) { + conf := &Config{ + CAPath: "../test/ca_path", + } + + tlsConf, err := conf.IncomingTLSConfig() + if err != nil { + t.Fatalf("err: %v", err) + } + if len(tlsConf.ClientCAs.Subjects()) != 2 { + t.Fatalf("expected certs") + } +} + func TestConfig_KeyPair_None(t *testing.T) { conf := &Config{} cert, err := conf.KeyPair() @@ -494,3 +510,46 @@ func TestConfig_IncomingTLS_TLSMinVersion(t *testing.T) { } } } + +func TestConfig_ParseCiphers(t *testing.T) { + testOk := strings.Join([]string{ + "TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA", + "TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256", + "TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA", + "TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384", + "TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA", + "TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256", + "TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA", + "TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384", + "TLS_RSA_WITH_AES_128_CBC_SHA", + "TLS_RSA_WITH_AES_128_GCM_SHA256", + "TLS_RSA_WITH_AES_256_CBC_SHA", + "TLS_RSA_WITH_AES_256_GCM_SHA384", + }, ",") + ciphers := []uint16{ + tls.TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA, + tls.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256, + tls.TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA, + tls.TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384, + tls.TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA, + tls.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256, + tls.TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA, + tls.TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384, + tls.TLS_RSA_WITH_AES_128_CBC_SHA, + tls.TLS_RSA_WITH_AES_128_GCM_SHA256, + tls.TLS_RSA_WITH_AES_256_CBC_SHA, + tls.TLS_RSA_WITH_AES_256_GCM_SHA384, + } + v, err := ParseCiphers(testOk) + if err != nil { + t.Fatal(err) + } + if got, want := v, ciphers; !reflect.DeepEqual(got, want) { + t.Fatalf("got ciphers %#v want %#v", got, want) + } + + testBad := "TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA,cipherX" + if _, err := ParseCiphers(testBad); err == nil { + t.Fatal("should fail on unsupported cipherX") + } +} diff --git a/website/source/docs/agent/options.html.markdown b/website/source/docs/agent/options.html.markdown index 0a317b259b..84d867f8f0 100644 --- a/website/source/docs/agent/options.html.markdown +++ b/website/source/docs/agent/options.html.markdown @@ -617,6 +617,11 @@ Consul will not enable TLS for the HTTP API unless the `https` port has been ass server connections with the appropriate [`verify_incoming`](#verify_incoming) or [`verify_outgoing`](#verify_outgoing) flags. +* `ca_path` This provides a path to a directory of PEM-encoded + certificate authority files. These certificate authorities are used to check the authenticity of client and + server connections with the appropriate [`verify_incoming`](#verify_incoming) or + [`verify_outgoing`](#verify_outgoing) flags. + * `cert_file` This provides a file path to a PEM-encoded certificate. The certificate is provided to clients or servers to verify the agent's authenticity. It must be provided along with [`key_file`](#key_file). @@ -1028,6 +1033,14 @@ Consul will not enable TLS for the HTTP API unless the `https` port has been ass or "tls12". This defaults to "tls10". WARNING: TLS 1.1 and lower are generally considered less secure; avoid using these if possible. This will be changed to default to "tls12" in Consul 0.8.0. +* `tls_cipher_suites` Added in Consul + 0.8.2, this specifies the list of supported ciphersuites as a comma-separated-list. The list of all + available ciphersuites is available in the [Golang TLS documentation](https://golang.org/src/crypto/tls/cipher_suites.go). + +* + `tls_prefer_server_cipher_suites` Added in Consul 0.8.2, this will cause Consul to prefer the + server's ciphersuite over the client ciphersuites. + * `translate_wan_addrs` If set to true, Consul will prefer a node's configured WAN address when servicing DNS and HTTP requests for a node in a remote datacenter. This allows the node to