Skip to content

Commit

Permalink
Add TLS cipher suite options and CA path support (#2963)
Browse files Browse the repository at this point in the history
This patch adds options to configure the available
TLS cipher suites and adds support for a path
for multiple CA certificates.

Fixes #2959
  • Loading branch information
kyhavlov authored and magiconair committed Apr 27, 2017
1 parent 4af5b22 commit b70e419
Show file tree
Hide file tree
Showing 8 changed files with 242 additions and 29 deletions.
3 changes: 3 additions & 0 deletions command/agent/agent.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
30 changes: 30 additions & 0 deletions command/agent/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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"`
Expand All @@ -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.
Expand Down Expand Up @@ -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
}

Expand Down Expand Up @@ -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
}
Expand All @@ -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...)
}
Expand Down
17 changes: 15 additions & 2 deletions command/agent/config_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package agent

import (
"bytes"
"crypto/tls"
"encoding/base64"
"io/ioutil"
"net"
Expand Down Expand Up @@ -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)
Expand All @@ -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)
Expand All @@ -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)
}
Expand Down
19 changes: 11 additions & 8 deletions command/agent/http.go
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down
33 changes: 23 additions & 10 deletions consul/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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
}
Expand Down
97 changes: 88 additions & 9 deletions tlsutil/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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
}

Expand Down Expand Up @@ -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
Expand All @@ -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 {
Expand All @@ -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
}
Loading

0 comments on commit b70e419

Please sign in to comment.