Merge pull request #4269 from hashicorp/f-tls-remove-weak-standards

Configurable TLS cipher suites and versions; disallow weak ciphers
This commit is contained in:
Chelsea Komlo 2018-05-11 08:11:46 -04:00 committed by GitHub
commit 687c26093c
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
9 changed files with 270 additions and 10 deletions

View file

@ -9,6 +9,8 @@ IMPROVEMENTS:
image pulls [[GH-4192](https://github.com/hashicorp/nomad/issues/4192)] image pulls [[GH-4192](https://github.com/hashicorp/nomad/issues/4192)]
* env: Default interpolation of optional meta fields of parameterized jobs to * 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)] 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 * 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 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)] a deployment should see healthy allocations [[GH-4259](https://github.com/hashicorp/nomad/issues/4259)]

View file

@ -399,11 +399,16 @@ func (c *Client) init() error {
func (c *Client) reloadTLSConnections(newConfig *nconfig.TLSConfig) error { func (c *Client) reloadTLSConnections(newConfig *nconfig.TLSConfig) error {
var tlsWrap tlsutil.RegionWrapper var tlsWrap tlsutil.RegionWrapper
if newConfig != nil && newConfig.EnableRPC { if newConfig != nil && newConfig.EnableRPC {
tw, err := tlsutil.NewTLSConfiguration(newConfig).OutgoingTLSWrapper() tw, err := tlsutil.NewTLSConfiguration(newConfig)
if err != nil { if err != nil {
return err return err
} }
tlsWrap = tw
twWrap, err := tw.OutgoingTLSWrapper()
if err != nil {
return err
}
tlsWrap = twWrap
} }
// Store the new tls wrapper. // Store the new tls wrapper.

View file

@ -13,6 +13,7 @@ import (
"github.com/hashicorp/hcl" "github.com/hashicorp/hcl"
"github.com/hashicorp/hcl/hcl/ast" "github.com/hashicorp/hcl/hcl/ast"
"github.com/hashicorp/nomad/helper" "github.com/hashicorp/nomad/helper"
"github.com/hashicorp/nomad/helper/tlsutil"
"github.com/hashicorp/nomad/nomad/structs/config" "github.com/hashicorp/nomad/nomad/structs/config"
"github.com/mitchellh/mapstructure" "github.com/mitchellh/mapstructure"
) )
@ -760,6 +761,8 @@ func parseTLSConfig(result **config.TLSConfig, list *ast.ObjectList) error {
"cert_file", "cert_file",
"key_file", "key_file",
"verify_https_client", "verify_https_client",
"tls_cipher_suites",
"tls_min_version",
} }
if err := helper.CheckHCLKeys(listVal, valid); err != nil { if err := helper.CheckHCLKeys(listVal, valid); err != nil {
@ -776,6 +779,14 @@ func parseTLSConfig(result **config.TLSConfig, list *ast.ObjectList) error {
return err return err
} }
if _, err := tlsutil.ParseCiphers(tlsConfig.TLSCipherSuites); err != nil {
return err
}
if _, err := tlsutil.ParseMinVersion(tlsConfig.TLSMinVersion); err != nil {
return err
}
*result = &tlsConfig *result = &tlsConfig
return nil return nil
} }

View file

@ -6,11 +6,46 @@ import (
"fmt" "fmt"
"io/ioutil" "io/ioutil"
"net" "net"
"strings"
"time" "time"
"github.com/hashicorp/nomad/nomad/structs/config" "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 // RegionSpecificWrapper is used to invoke a static Region and turns a
// RegionWrapper into a Wrapper type. // RegionWrapper into a Wrapper type.
func RegionSpecificWrapper(region string, tlsWrap RegionWrapper) Wrapper { func RegionSpecificWrapper(region string, tlsWrap RegionWrapper) Wrapper {
@ -65,9 +100,26 @@ type Config struct {
// KeyLoader dynamically reloads TLS configuration. // KeyLoader dynamically reloads TLS configuration.
KeyLoader *config.KeyLoader KeyLoader *config.KeyLoader
// 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) {
ciphers, err := ParseCiphers(newConf.TLSCipherSuites)
if err != nil {
return nil, err
}
minVersion, err := ParseMinVersion(newConf.TLSMinVersion)
if err != nil {
return nil, err
} }
func NewTLSConfiguration(newConf *config.TLSConfig) *Config {
return &Config{ return &Config{
VerifyIncoming: true, VerifyIncoming: true,
VerifyOutgoing: true, VerifyOutgoing: true,
@ -76,7 +128,9 @@ func NewTLSConfiguration(newConf *config.TLSConfig) *Config {
CertFile: newConf.CertFile, CertFile: newConf.CertFile,
KeyFile: newConf.KeyFile, KeyFile: newConf.KeyFile,
KeyLoader: newConf.GetKeyLoader(), KeyLoader: newConf.GetKeyLoader(),
} CipherSuites: ciphers,
MinVersion: minVersion,
}, nil
} }
// AppendCA opens and parses the CA file and adds the certificates to // AppendCA opens and parses the CA file and adds the certificates to
@ -132,6 +186,8 @@ func (c *Config) OutgoingTLSConfig() (*tls.Config, error) {
tlsConfig := &tls.Config{ tlsConfig := &tls.Config{
RootCAs: x509.NewCertPool(), RootCAs: x509.NewCertPool(),
InsecureSkipVerify: true, InsecureSkipVerify: true,
CipherSuites: c.CipherSuites,
MinVersion: c.MinVersion,
} }
if c.VerifyServerHostname { if c.VerifyServerHostname {
tlsConfig.InsecureSkipVerify = false tlsConfig.InsecureSkipVerify = false
@ -250,6 +306,8 @@ func (c *Config) IncomingTLSConfig() (*tls.Config, error) {
tlsConfig := &tls.Config{ tlsConfig := &tls.Config{
ClientCAs: x509.NewCertPool(), ClientCAs: x509.NewCertPool(),
ClientAuth: tls.NoClientCert, ClientAuth: tls.NoClientCert,
CipherSuites: c.CipherSuites,
MinVersion: c.MinVersion,
} }
// Parse the CA cert if any // Parse the CA cert if any
@ -279,3 +337,42 @@ func (c *Config) IncomingTLSConfig() (*tls.Config, error) {
return tlsConfig, nil 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 == "" {
ciphers = defaultTLSCiphers
} else {
ciphers = strings.Split(cipherStr, ",")
}
for _, cipher := range ciphers {
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
}

View file

@ -3,14 +3,17 @@ package tlsutil
import ( import (
"crypto/tls" "crypto/tls"
"crypto/x509" "crypto/x509"
"fmt"
"io" "io"
"io/ioutil" "io/ioutil"
"net" "net"
"strings"
"testing" "testing"
"github.com/hashicorp/nomad/nomad/structs/config" "github.com/hashicorp/nomad/nomad/structs/config"
"github.com/hashicorp/yamux" "github.com/hashicorp/yamux"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
) )
const ( const (
@ -412,3 +415,119 @@ func TestConfig_IncomingTLS_NoVerify(t *testing.T) {
t.Fatalf("unexpected client cert") 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 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)
}
}

View file

@ -450,7 +450,12 @@ func (s *Server) reloadTLSConnections(newTLSConfig *config.TLSConfig) error {
return fmt.Errorf("can't reload uninitialized RPC listener") return fmt.Errorf("can't reload uninitialized RPC listener")
} }
tlsConf := tlsutil.NewTLSConfiguration(newTLSConfig) tlsConf, err := tlsutil.NewTLSConfiguration(newTLSConfig)
if err != nil {
s.logger.Printf("[ERR] nomad: unable to create TLS configuration %s", err)
return err
}
incomingTLS, tlsWrap, err := getTLSConf(newTLSConfig.EnableRPC, tlsConf) incomingTLS, tlsWrap, err := getTLSConf(newTLSConfig.EnableRPC, tlsConf)
if err != nil { if err != nil {
s.logger.Printf("[ERR] nomad: unable to reset TLS context %s", err) s.logger.Printf("[ERR] nomad: unable to reset TLS context %s", err)

View file

@ -55,6 +55,14 @@ type TLSConfig struct {
// Checksum is a MD5 hash of the certificate CA File, Certificate file, and // Checksum is a MD5 hash of the certificate CA File, Certificate file, and
// key file. // key file.
Checksum string Checksum string
// 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 { type KeyLoader struct {
@ -147,6 +155,9 @@ func (t *TLSConfig) Copy() *TLSConfig {
new.RPCUpgradeMode = t.RPCUpgradeMode new.RPCUpgradeMode = t.RPCUpgradeMode
new.VerifyHTTPSClient = t.VerifyHTTPSClient new.VerifyHTTPSClient = t.VerifyHTTPSClient
new.TLSCipherSuites = t.TLSCipherSuites
new.TLSMinVersion = t.TLSMinVersion
new.SetChecksum() new.SetChecksum()
return new return new

View file

@ -174,6 +174,7 @@ func TestTLS_Copy(t *testing.T) {
CAFile: cafile, CAFile: cafile,
CertFile: foocert, CertFile: foocert,
KeyFile: fookey, KeyFile: fookey,
TLSCipherSuites: "TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305",
} }
a.SetChecksum() a.SetChecksum()

View file

@ -58,6 +58,15 @@ the [Agent's Gossip and RPC Encryption](/docs/agent/encryption.html).
cluster is being upgraded to TLS, and removed after the migration is cluster is being upgraded to TLS, and removed after the migration is
complete. This allows the agent to accept both TLS and plaintext traffic. 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.
- `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 - `verify_https_client` `(bool: false)` - Specifies agents should require
client certificates for all incoming HTTPS requests. The client certificates client certificates for all incoming HTTPS requests. The client certificates
must be signed by the same CA as Nomad. must be signed by the same CA as Nomad.