Skip to content

Commit eb587d6

Browse files
committed
allow configurable cipher suites
disallow 3DES and RC4 ciphers add documentation for tls_cipher_suites
1 parent f92d364 commit eb587d6

File tree

8 files changed

+180
-10
lines changed

8 files changed

+180
-10
lines changed

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

+6
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,7 @@ func parseTLSConfig(result **config.TLSConfig, list *ast.ObjectList) error {
760761
"cert_file",
761762
"key_file",
762763
"verify_https_client",
764+
"tls_cipher_suites",
763765
}
764766

765767
if err := helper.CheckHCLKeys(listVal, valid); err != nil {
@@ -776,6 +778,10 @@ func parseTLSConfig(result **config.TLSConfig, list *ast.ObjectList) error {
776778
return err
777779
}
778780

781+
if _, err := tlsutil.ParseCiphers(tlsConfig.TLSCipherSuites); err != nil {
782+
return err
783+
}
784+
779785
*result = &tlsConfig
780786
return nil
781787
}

helper/tlsutil/config.go

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

1112
"github.com/hashicorp/nomad/nomad/structs/config"
@@ -65,9 +66,18 @@ type Config struct {
6566

6667
// KeyLoader dynamically reloads TLS configuration.
6768
KeyLoader *config.KeyLoader
69+
70+
// CipherSuites have a default safe configuration, or operators can override
71+
// these values for acceptable safe alternatives.
72+
CipherSuites []uint16
6873
}
6974

70-
func NewTLSConfiguration(newConf *config.TLSConfig) *Config {
75+
func NewTLSConfiguration(newConf *config.TLSConfig) (*Config, error) {
76+
ciphers, err := ParseCiphers(newConf.TLSCipherSuites)
77+
if err != nil {
78+
return nil, err
79+
}
80+
7181
return &Config{
7282
VerifyIncoming: true,
7383
VerifyOutgoing: true,
@@ -76,7 +86,8 @@ func NewTLSConfiguration(newConf *config.TLSConfig) *Config {
7686
CertFile: newConf.CertFile,
7787
KeyFile: newConf.KeyFile,
7888
KeyLoader: newConf.GetKeyLoader(),
79-
}
89+
CipherSuites: ciphers,
90+
}, nil
8091
}
8192

8293
// AppendCA opens and parses the CA file and adds the certificates to
@@ -132,6 +143,7 @@ func (c *Config) OutgoingTLSConfig() (*tls.Config, error) {
132143
tlsConfig := &tls.Config{
133144
RootCAs: x509.NewCertPool(),
134145
InsecureSkipVerify: true,
146+
CipherSuites: c.CipherSuites,
135147
}
136148
if c.VerifyServerHostname {
137149
tlsConfig.InsecureSkipVerify = false
@@ -248,8 +260,9 @@ func WrapTLSClient(conn net.Conn, tlsConfig *tls.Config) (net.Conn, error) {
248260
func (c *Config) IncomingTLSConfig() (*tls.Config, error) {
249261
// Create the tlsConfig
250262
tlsConfig := &tls.Config{
251-
ClientCAs: x509.NewCertPool(),
252-
ClientAuth: tls.NoClientCert,
263+
ClientCAs: x509.NewCertPool(),
264+
ClientAuth: tls.NoClientCert,
265+
CipherSuites: c.CipherSuites,
253266
}
254267

255268
// Parse the CA cert if any
@@ -279,3 +292,51 @@ func (c *Config) IncomingTLSConfig() (*tls.Config, error) {
279292

280293
return tlsConfig, nil
281294
}
295+
296+
// ParseCiphers parse ciphersuites from the comma-separated string into recognized slice
297+
func ParseCiphers(cipherStr string) ([]uint16, error) {
298+
suites := []uint16{}
299+
300+
cipherStr = strings.TrimSpace(cipherStr)
301+
302+
var ciphers []string
303+
if cipherStr == "" {
304+
// Set good default values
305+
ciphers = []string{"TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305",
306+
"TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256",
307+
"TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384",
308+
}
309+
310+
} else {
311+
ciphers = strings.Split(cipherStr, ",")
312+
}
313+
314+
cipherMap := map[string]uint16{
315+
"TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305": tls.TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305,
316+
"TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305": tls.TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305,
317+
"TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256": tls.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256,
318+
"TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256": tls.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256,
319+
"TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384": tls.TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384,
320+
"TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384": tls.TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384,
321+
"TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA256": tls.TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA256,
322+
"TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA": tls.TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA,
323+
"TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA256": tls.TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA256,
324+
"TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA": tls.TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA,
325+
"TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA": tls.TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA,
326+
"TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA": tls.TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA,
327+
"TLS_RSA_WITH_AES_128_GCM_SHA256": tls.TLS_RSA_WITH_AES_128_GCM_SHA256,
328+
"TLS_RSA_WITH_AES_256_GCM_SHA384": tls.TLS_RSA_WITH_AES_256_GCM_SHA384,
329+
"TLS_RSA_WITH_AES_128_CBC_SHA256": tls.TLS_RSA_WITH_AES_128_CBC_SHA256,
330+
"TLS_RSA_WITH_AES_128_CBC_SHA": tls.TLS_RSA_WITH_AES_128_CBC_SHA,
331+
"TLS_RSA_WITH_AES_256_CBC_SHA": tls.TLS_RSA_WITH_AES_256_CBC_SHA,
332+
}
333+
for _, cipher := range ciphers {
334+
if v, ok := cipherMap[cipher]; ok {
335+
suites = append(suites, v)
336+
} else {
337+
return suites, fmt.Errorf("unsupported cipher %q", cipher)
338+
}
339+
}
340+
341+
return suites, nil
342+
}

helper/tlsutil/config_test.go

+83
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,83 @@ 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 cipher %q", cipher), err.Error())
495+
require.Equal(0, len(parsedCiphers))
496+
}
497+
}

nomad/server.go

+5-1
Original file line numberDiff line numberDiff line change
@@ -450,7 +450,11 @@ 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+
return err
456+
}
457+
454458
incomingTLS, tlsWrap, err := getTLSConf(newTLSConfig.EnableRPC, tlsConf)
455459
if err != nil {
456460
s.logger.Printf("[ERR] nomad: unable to reset TLS context %s", err)

nomad/structs/config/tls.go

+4
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,10 @@ 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"`
5862
}
5963

6064
type KeyLoader struct {

nomad/structs/config/tls_test.go

+4-3
Original file line numberDiff line numberDiff line change
@@ -171,9 +171,10 @@ func TestTLS_Copy(t *testing.T) {
171171
fookey = "../../../helper/tlsutil/testdata/nomad-foo-key.pem"
172172
)
173173
a := &TLSConfig{
174-
CAFile: cafile,
175-
CertFile: foocert,
176-
KeyFile: fookey,
174+
CAFile: cafile,
175+
CertFile: foocert,
176+
KeyFile: fookey,
177+
TLSCipherSuites: "TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305",
177178
}
178179
a.SetChecksum()
179180

website/source/docs/agent/configuration/tls.html.md

+6
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,12 @@ the [Agent's Gossip and RPC Encryption](/docs/agent/encryption.html).
5858
cluster is being upgraded to TLS, and removed after the migration is
5959
complete. This allows the agent to accept both TLS and plaintext traffic.
6060

61+
- `tls_cipher_suites` - Specifies the TLS cipher suites that will be used by
62+
the agent. Known insecure ciphers are disabled (3DES and RC4). By default,
63+
an agent is configured to use TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305,
64+
TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256, and
65+
TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384.
66+
6167
- `verify_https_client` `(bool: false)` - Specifies agents should require
6268
client certificates for all incoming HTTPS requests. The client certificates
6369
must be signed by the same CA as Nomad.

0 commit comments

Comments
 (0)