Skip to content

Commit 31c2198

Browse files
authored
Merge pull request #4269 from hashicorp/f-tls-remove-weak-standards
Configurable TLS cipher suites and versions; disallow weak ciphers
2 parents ddab0f1 + 8daaa66 commit 31c2198

File tree

9 files changed

+270
-10
lines changed

9 files changed

+270
-10
lines changed

CHANGELOG.md

+2
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@ IMPROVEMENTS:
99
image pulls [[GH-4192](https://github.com/hashicorp/nomad/issues/4192)]
1010
* env: Default interpolation of optional meta fields of parameterized jobs to
1111
an empty string rather than the field key. [[GH-3720](https://github.com/hashicorp/nomad/issues/3720)]
12+
* core: Add the option for operators to configure TLS versions and allowed
13+
cipher suites. Default is a subset of safe ciphers and TLS 1.2 [[GH-4269](https://github.com/hashicorp/nomad/pull/4269)]
1214
* core: Add a new [progress_deadline](https://www.nomadproject.io/docs/job-specification/update.html#progress_deadline) parameter to
1315
support rescheduling failed allocations during a deployment. This allows operators to specify a configurable deadline before which
1416
a deployment should see healthy allocations [[GH-4259](https://github.com/hashicorp/nomad/issues/4259)]

client/client.go

+7-2
Original file line numberDiff line numberDiff line change
@@ -399,11 +399,16 @@ func (c *Client) init() error {
399399
func (c *Client) reloadTLSConnections(newConfig *nconfig.TLSConfig) error {
400400
var tlsWrap tlsutil.RegionWrapper
401401
if newConfig != nil && newConfig.EnableRPC {
402-
tw, err := tlsutil.NewTLSConfiguration(newConfig).OutgoingTLSWrapper()
402+
tw, err := tlsutil.NewTLSConfiguration(newConfig)
403403
if err != nil {
404404
return err
405405
}
406-
tlsWrap = tw
406+
407+
twWrap, err := tw.OutgoingTLSWrapper()
408+
if err != nil {
409+
return err
410+
}
411+
tlsWrap = twWrap
407412
}
408413

409414
// Store the new tls wrapper.

command/agent/config_parse.go

+11
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import (
1313
"github.com/hashicorp/hcl"
1414
"github.com/hashicorp/hcl/hcl/ast"
1515
"github.com/hashicorp/nomad/helper"
16+
"github.com/hashicorp/nomad/helper/tlsutil"
1617
"github.com/hashicorp/nomad/nomad/structs/config"
1718
"github.com/mitchellh/mapstructure"
1819
)
@@ -760,6 +761,8 @@ func parseTLSConfig(result **config.TLSConfig, list *ast.ObjectList) error {
760761
"cert_file",
761762
"key_file",
762763
"verify_https_client",
764+
"tls_cipher_suites",
765+
"tls_min_version",
763766
}
764767

765768
if err := helper.CheckHCLKeys(listVal, valid); err != nil {
@@ -776,6 +779,14 @@ func parseTLSConfig(result **config.TLSConfig, list *ast.ObjectList) error {
776779
return err
777780
}
778781

782+
if _, err := tlsutil.ParseCiphers(tlsConfig.TLSCipherSuites); err != nil {
783+
return err
784+
}
785+
786+
if _, err := tlsutil.ParseMinVersion(tlsConfig.TLSMinVersion); err != nil {
787+
return err
788+
}
789+
779790
*result = &tlsConfig
780791
return nil
781792
}

helper/tlsutil/config.go

+101-4
Original file line numberDiff line numberDiff line change
@@ -6,11 +6,46 @@ import (
66
"fmt"
77
"io/ioutil"
88
"net"
9+
"strings"
910
"time"
1011

1112
"github.com/hashicorp/nomad/nomad/structs/config"
1213
)
1314

15+
// supportedTLSVersions are the current TLS versions that Nomad supports
16+
var supportedTLSVersions = map[string]uint16{
17+
"tls10": tls.VersionTLS10,
18+
"tls11": tls.VersionTLS11,
19+
"tls12": tls.VersionTLS12,
20+
}
21+
22+
// supportedTLSCiphers are the complete list of TLS ciphers supported by Nomad
23+
var supportedTLSCiphers = map[string]uint16{
24+
"TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305": tls.TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305,
25+
"TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305": tls.TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305,
26+
"TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256": tls.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256,
27+
"TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256": tls.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256,
28+
"TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384": tls.TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384,
29+
"TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384": tls.TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384,
30+
"TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA256": tls.TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA256,
31+
"TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA": tls.TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA,
32+
"TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA256": tls.TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA256,
33+
"TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA": tls.TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA,
34+
"TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA": tls.TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA,
35+
"TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA": tls.TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA,
36+
"TLS_RSA_WITH_AES_128_GCM_SHA256": tls.TLS_RSA_WITH_AES_128_GCM_SHA256,
37+
"TLS_RSA_WITH_AES_256_GCM_SHA384": tls.TLS_RSA_WITH_AES_256_GCM_SHA384,
38+
"TLS_RSA_WITH_AES_128_CBC_SHA256": tls.TLS_RSA_WITH_AES_128_CBC_SHA256,
39+
"TLS_RSA_WITH_AES_128_CBC_SHA": tls.TLS_RSA_WITH_AES_128_CBC_SHA,
40+
"TLS_RSA_WITH_AES_256_CBC_SHA": tls.TLS_RSA_WITH_AES_256_CBC_SHA,
41+
}
42+
43+
// defaultTLSCiphers are the TLS Ciphers that are supported by default
44+
var defaultTLSCiphers = []string{"TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305",
45+
"TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256",
46+
"TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384",
47+
}
48+
1449
// RegionSpecificWrapper is used to invoke a static Region and turns a
1550
// RegionWrapper into a Wrapper type.
1651
func RegionSpecificWrapper(region string, tlsWrap RegionWrapper) Wrapper {
@@ -65,9 +100,26 @@ type Config struct {
65100

66101
// KeyLoader dynamically reloads TLS configuration.
67102
KeyLoader *config.KeyLoader
103+
104+
// CipherSuites have a default safe configuration, or operators can override
105+
// these values for acceptable safe alternatives.
106+
CipherSuites []uint16
107+
108+
// MinVersion contains the minimum SSL/TLS version that is accepted.
109+
MinVersion uint16
68110
}
69111

70-
func NewTLSConfiguration(newConf *config.TLSConfig) *Config {
112+
func NewTLSConfiguration(newConf *config.TLSConfig) (*Config, error) {
113+
ciphers, err := ParseCiphers(newConf.TLSCipherSuites)
114+
if err != nil {
115+
return nil, err
116+
}
117+
118+
minVersion, err := ParseMinVersion(newConf.TLSMinVersion)
119+
if err != nil {
120+
return nil, err
121+
}
122+
71123
return &Config{
72124
VerifyIncoming: true,
73125
VerifyOutgoing: true,
@@ -76,7 +128,9 @@ func NewTLSConfiguration(newConf *config.TLSConfig) *Config {
76128
CertFile: newConf.CertFile,
77129
KeyFile: newConf.KeyFile,
78130
KeyLoader: newConf.GetKeyLoader(),
79-
}
131+
CipherSuites: ciphers,
132+
MinVersion: minVersion,
133+
}, nil
80134
}
81135

82136
// AppendCA opens and parses the CA file and adds the certificates to
@@ -132,6 +186,8 @@ func (c *Config) OutgoingTLSConfig() (*tls.Config, error) {
132186
tlsConfig := &tls.Config{
133187
RootCAs: x509.NewCertPool(),
134188
InsecureSkipVerify: true,
189+
CipherSuites: c.CipherSuites,
190+
MinVersion: c.MinVersion,
135191
}
136192
if c.VerifyServerHostname {
137193
tlsConfig.InsecureSkipVerify = false
@@ -248,8 +304,10 @@ func WrapTLSClient(conn net.Conn, tlsConfig *tls.Config) (net.Conn, error) {
248304
func (c *Config) IncomingTLSConfig() (*tls.Config, error) {
249305
// Create the tlsConfig
250306
tlsConfig := &tls.Config{
251-
ClientCAs: x509.NewCertPool(),
252-
ClientAuth: tls.NoClientCert,
307+
ClientCAs: x509.NewCertPool(),
308+
ClientAuth: tls.NoClientCert,
309+
CipherSuites: c.CipherSuites,
310+
MinVersion: c.MinVersion,
253311
}
254312

255313
// Parse the CA cert if any
@@ -279,3 +337,42 @@ func (c *Config) IncomingTLSConfig() (*tls.Config, error) {
279337

280338
return tlsConfig, nil
281339
}
340+
341+
// ParseCiphers parses ciphersuites from the comma-separated string into
342+
// recognized slice
343+
func ParseCiphers(cipherStr string) ([]uint16, error) {
344+
suites := []uint16{}
345+
346+
cipherStr = strings.TrimSpace(cipherStr)
347+
348+
var ciphers []string
349+
if cipherStr == "" {
350+
ciphers = defaultTLSCiphers
351+
352+
} else {
353+
ciphers = strings.Split(cipherStr, ",")
354+
}
355+
for _, cipher := range ciphers {
356+
c, ok := supportedTLSCiphers[cipher]
357+
if !ok {
358+
return suites, fmt.Errorf("unsupported TLS cipher %q", cipher)
359+
}
360+
suites = append(suites, c)
361+
}
362+
363+
return suites, nil
364+
}
365+
366+
// ParseMinVersion parses the specified minimum TLS version for the Nomad agent
367+
func ParseMinVersion(version string) (uint16, error) {
368+
if version == "" {
369+
return supportedTLSVersions["tls12"], nil
370+
}
371+
372+
vers, ok := supportedTLSVersions[version]
373+
if !ok {
374+
return 0, fmt.Errorf("unsupported TLS version %q", version)
375+
}
376+
377+
return vers, nil
378+
}

helper/tlsutil/config_test.go

+119
Original file line numberDiff line numberDiff line change
@@ -3,14 +3,17 @@ package tlsutil
33
import (
44
"crypto/tls"
55
"crypto/x509"
6+
"fmt"
67
"io"
78
"io/ioutil"
89
"net"
10+
"strings"
911
"testing"
1012

1113
"github.com/hashicorp/nomad/nomad/structs/config"
1214
"github.com/hashicorp/yamux"
1315
"github.com/stretchr/testify/assert"
16+
"github.com/stretchr/testify/require"
1417
)
1518

1619
const (
@@ -412,3 +415,119 @@ func TestConfig_IncomingTLS_NoVerify(t *testing.T) {
412415
t.Fatalf("unexpected client cert")
413416
}
414417
}
418+
419+
func TestConfig_ParseCiphers_Valid(t *testing.T) {
420+
require := require.New(t)
421+
422+
validCiphers := strings.Join([]string{
423+
"TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305",
424+
"TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305",
425+
"TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256",
426+
"TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256",
427+
"TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384",
428+
"TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384",
429+
"TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA256",
430+
"TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA",
431+
"TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA256",
432+
"TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA",
433+
"TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA",
434+
"TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA",
435+
"TLS_RSA_WITH_AES_128_GCM_SHA256",
436+
"TLS_RSA_WITH_AES_256_GCM_SHA384",
437+
"TLS_RSA_WITH_AES_128_CBC_SHA256",
438+
"TLS_RSA_WITH_AES_128_CBC_SHA",
439+
"TLS_RSA_WITH_AES_256_CBC_SHA",
440+
}, ",")
441+
442+
expectedCiphers := []uint16{
443+
tls.TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305,
444+
tls.TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305,
445+
tls.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256,
446+
tls.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256,
447+
tls.TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384,
448+
tls.TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384,
449+
tls.TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA256,
450+
tls.TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA,
451+
tls.TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA256,
452+
tls.TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA,
453+
tls.TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA,
454+
tls.TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA,
455+
tls.TLS_RSA_WITH_AES_128_GCM_SHA256,
456+
tls.TLS_RSA_WITH_AES_256_GCM_SHA384,
457+
tls.TLS_RSA_WITH_AES_128_CBC_SHA256,
458+
tls.TLS_RSA_WITH_AES_128_CBC_SHA,
459+
tls.TLS_RSA_WITH_AES_256_CBC_SHA,
460+
}
461+
462+
parsedCiphers, err := ParseCiphers(validCiphers)
463+
require.Nil(err)
464+
require.Equal(parsedCiphers, expectedCiphers)
465+
}
466+
467+
func TestConfig_ParseCiphers_Default(t *testing.T) {
468+
require := require.New(t)
469+
470+
expectedCiphers := []uint16{
471+
tls.TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305,
472+
tls.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256,
473+
tls.TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384,
474+
}
475+
476+
parsedCiphers, err := ParseCiphers("")
477+
require.Nil(err)
478+
require.Equal(parsedCiphers, expectedCiphers)
479+
}
480+
481+
func TestConfig_ParseCiphers_Invalid(t *testing.T) {
482+
require := require.New(t)
483+
484+
invalidCiphers := []string{"TLS_RSA_WITH_3DES_EDE_CBC_SHA",
485+
"TLS_ECDHE_RSA_WITH_3DES_EDE_CBC_SHA",
486+
"TLS_RSA_WITH_RC4_128_SHA",
487+
"TLS_ECDHE_RSA_WITH_RC4_128_SHA",
488+
"TLS_ECDHE_ECDSA_WITH_RC4_128_SHA",
489+
}
490+
491+
for _, cipher := range invalidCiphers {
492+
parsedCiphers, err := ParseCiphers(cipher)
493+
require.NotNil(err)
494+
require.Equal(fmt.Sprintf("unsupported TLS cipher %q", cipher), err.Error())
495+
require.Equal(0, len(parsedCiphers))
496+
}
497+
}
498+
499+
func TestConfig_ParseMinVersion_Valid(t *testing.T) {
500+
require := require.New(t)
501+
502+
validVersions := []string{"tls10",
503+
"tls11",
504+
"tls12",
505+
}
506+
507+
expected := map[string]uint16{
508+
"tls10": tls.VersionTLS10,
509+
"tls11": tls.VersionTLS11,
510+
"tls12": tls.VersionTLS12,
511+
}
512+
513+
for _, version := range validVersions {
514+
parsedVersion, err := ParseMinVersion(version)
515+
require.Nil(err)
516+
require.Equal(expected[version], parsedVersion)
517+
}
518+
}
519+
520+
func TestConfig_ParseMinVersion_Invalid(t *testing.T) {
521+
require := require.New(t)
522+
523+
invalidVersions := []string{"tls13",
524+
"tls15",
525+
}
526+
527+
for _, version := range invalidVersions {
528+
parsedVersion, err := ParseMinVersion(version)
529+
require.NotNil(err)
530+
require.Equal(fmt.Sprintf("unsupported TLS version %q", version), err.Error())
531+
require.Equal(uint16(0), parsedVersion)
532+
}
533+
}

nomad/server.go

+6-1
Original file line numberDiff line numberDiff line change
@@ -450,7 +450,12 @@ func (s *Server) reloadTLSConnections(newTLSConfig *config.TLSConfig) error {
450450
return fmt.Errorf("can't reload uninitialized RPC listener")
451451
}
452452

453-
tlsConf := tlsutil.NewTLSConfiguration(newTLSConfig)
453+
tlsConf, err := tlsutil.NewTLSConfiguration(newTLSConfig)
454+
if err != nil {
455+
s.logger.Printf("[ERR] nomad: unable to create TLS configuration %s", err)
456+
return err
457+
}
458+
454459
incomingTLS, tlsWrap, err := getTLSConf(newTLSConfig.EnableRPC, tlsConf)
455460
if err != nil {
456461
s.logger.Printf("[ERR] nomad: unable to reset TLS context %s", err)

nomad/structs/config/tls.go

+11
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,14 @@ type TLSConfig struct {
5555
// Checksum is a MD5 hash of the certificate CA File, Certificate file, and
5656
// key file.
5757
Checksum string
58+
59+
// TLSCipherSuites are operator-defined ciphers to be used in Nomad TLS
60+
// connections
61+
TLSCipherSuites string `mapstructure:"tls_cipher_suites"`
62+
63+
// TLSMinVersion is used to set the minimum TLS version used for TLS
64+
// connections. Should be either "tls10", "tls11", or "tls12".
65+
TLSMinVersion string `mapstructure:"tls_min_version"`
5866
}
5967

6068
type KeyLoader struct {
@@ -147,6 +155,9 @@ func (t *TLSConfig) Copy() *TLSConfig {
147155
new.RPCUpgradeMode = t.RPCUpgradeMode
148156
new.VerifyHTTPSClient = t.VerifyHTTPSClient
149157

158+
new.TLSCipherSuites = t.TLSCipherSuites
159+
new.TLSMinVersion = t.TLSMinVersion
160+
150161
new.SetChecksum()
151162

152163
return new

0 commit comments

Comments
 (0)