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
This commit is contained in:
Kyle Havlovitz 2017-04-27 01:29:39 -07:00 committed by Frank Schröder
parent 4af5b22669
commit b70e419aeb
8 changed files with 242 additions and 29 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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.
* <a name="ca_path"></a><a href="#ca_path">`ca_path`</a> 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.
* <a name="cert_file"></a><a href="#cert_file">`cert_file`</a> 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.
* <a name="tls_cipher_suites"></a><a href="#tls_cipher_suites">`tls_cipher_suites`</a> 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).
* <a name="tls_prefer_server_cipher_suites"></a><a href="#tls_prefer_server_cipher_suites">
`tls_prefer_server_cipher_suites`</a> Added in Consul 0.8.2, this will cause Consul to prefer the
server's ciphersuite over the client ciphersuites.
* <a name="translate_wan_addrs"</a><a href="#translate_wan_addrs">`translate_wan_addrs`</a> If
set to true, Consul will prefer a node's configured <a href="#_advertise-wan">WAN address</a>
when servicing DNS and HTTP requests for a node in a remote datacenter. This allows the node to