Skip to content

Commit 1aace34

Browse files
szabolcs-horvathkeyki
authored andcommitted
CB-28864: Make TLS version and cipher suites configurable
1 parent bc932db commit 1aace34

File tree

7 files changed

+381
-37
lines changed

7 files changed

+381
-37
lines changed

Makefile

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
BINARY=salt-bootstrap
22

3-
VERSION=0.14.2
3+
VERSION=0.14.3
44
BUILD_TIME=$(shell date +%FT%T)
55
LDFLAGS=-ldflags "-X github.com/hortonworks/salt-bootstrap/saltboot.Version=${VERSION} -X github.com/hortonworks/salt-bootstrap/saltboot.BuildTime=${BUILD_TIME}"
66
GOFILES_NOVENDOR = $(shell find . -type f -name '*.go' -not -path "./vendor/*" -not -path "./.git/*")

saltboot/distributor.go

+4
Original file line numberDiff line numberDiff line change
@@ -30,11 +30,15 @@ func determineProtocol(httpsEnabled bool) string {
3030

3131
func getHttpClient(httpsEnabled bool) *http.Client {
3232
if httpsEnabled {
33+
httpsConfig := GetHttpsConfig()
3334
transport := http.DefaultTransport.(*http.Transport).Clone()
3435
if transport.TLSClientConfig == nil {
3536
transport.TLSClientConfig = &tls.Config{}
3637
}
3738
transport.TLSClientConfig.InsecureSkipVerify = true
39+
transport.TLSClientConfig.MinVersion = httpsConfig.MinTlsVersion
40+
transport.TLSClientConfig.MaxVersion = httpsConfig.MaxTlsVersion
41+
transport.TLSClientConfig.CipherSuites = httpsConfig.CipherSuites
3842

3943
return &http.Client{
4044
Transport: transport,

saltboot/netutil.go

+127-19
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package saltboot
22

33
import (
4+
"crypto/tls"
45
"fmt"
56
"io/ioutil"
67
"log"
@@ -12,17 +13,22 @@ import (
1213
)
1314

1415
const (
15-
httpsEnabledKey = "SALTBOOT_HTTPS_ENABLED"
16-
portKey = "SALTBOOT_PORT"
17-
defaultPort = 7070
18-
httpsPortKey = "SALTBOOT_HTTPS_PORT"
19-
defaultHttpsPort = 7071
20-
httpsCertFileKey = "SALTBOOT_HTTPS_CERT_FILE"
21-
defaultHttpsCertFile = "/etc/certs/cluster.pem"
22-
httpsKeyFileKey = "SALTBOOT_HTTPS_KEY_FILE"
23-
defaultHttpsKeyFile = "/etc/certs/cluster-key.pem"
24-
httpsCaCertFileKey = "SALTBOOT_HTTPS_CACERT_FILE"
25-
defaultHttpsCaCertFileKey = "/etc/certs/ca.pem"
16+
httpsEnabledKey = "SALTBOOT_HTTPS_ENABLED"
17+
portKey = "SALTBOOT_PORT"
18+
defaultPort = 7070
19+
httpsPortKey = "SALTBOOT_HTTPS_PORT"
20+
defaultHttpsPort = 7071
21+
httpsCertFileKey = "SALTBOOT_HTTPS_CERT_FILE"
22+
defaultHttpsCertFile = "/etc/certs/cluster.pem"
23+
httpsKeyFileKey = "SALTBOOT_HTTPS_KEY_FILE"
24+
defaultHttpsKeyFile = "/etc/certs/cluster-key.pem"
25+
httpsCaCertFileKey = "SALTBOOT_HTTPS_CACERT_FILE"
26+
defaultHttpsCaCertFile = "/etc/certs/ca.pem"
27+
minTlsVersionKey = "SALTBOOT_MIN_TLS_VERSION"
28+
defaultMinTlsVersion = tls.VersionTLS12
29+
maxTlsVersionKey = "SALTBOOT_MAX_TLS_VERSION"
30+
defaultMaxTlsVersion = tls.VersionTLS13
31+
cipherSuitesKey = "SALTBOOT_CIPHER_SUITES"
2632

2733
userKey = "SALTBOOT_USER"
2834
passwdKey = "SALTBOOT_PASSWORD"
@@ -31,10 +37,49 @@ const (
3137
defaultConfigLoc = "/etc/salt-bootstrap/security-config.yml"
3238
)
3339

40+
var tlsVersionMap = map[string]uint16{
41+
"1.0": tls.VersionTLS10,
42+
"1.1": tls.VersionTLS11,
43+
"1.2": tls.VersionTLS12,
44+
"1.3": tls.VersionTLS13,
45+
}
46+
47+
/**
48+
* This slice matches the list of cipher suites specified in https://pkg.go.dev/crypto/tls@go1.14.3#pkg-constants
49+
* Only the TLS 1.0-1.2 cipher suites are listed, since according to the go documentation "...TLS 1.3 ciphersuites are not configurable."
50+
*/
51+
var cipherSuiteMap = map[string]uint16{
52+
"TLS_RSA_WITH_RC4_128_SHA": tls.TLS_RSA_WITH_RC4_128_SHA,
53+
"TLS_RSA_WITH_3DES_EDE_CBC_SHA": tls.TLS_RSA_WITH_3DES_EDE_CBC_SHA,
54+
"TLS_RSA_WITH_AES_128_CBC_SHA": tls.TLS_RSA_WITH_AES_128_CBC_SHA,
55+
"TLS_RSA_WITH_AES_256_CBC_SHA": tls.TLS_RSA_WITH_AES_256_CBC_SHA,
56+
"TLS_RSA_WITH_AES_128_CBC_SHA256": tls.TLS_RSA_WITH_AES_128_CBC_SHA256,
57+
"TLS_RSA_WITH_AES_128_GCM_SHA256": tls.TLS_RSA_WITH_AES_128_GCM_SHA256,
58+
"TLS_RSA_WITH_AES_256_GCM_SHA384": tls.TLS_RSA_WITH_AES_256_GCM_SHA384,
59+
"TLS_ECDHE_ECDSA_WITH_RC4_128_SHA": tls.TLS_ECDHE_ECDSA_WITH_RC4_128_SHA,
60+
"TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA": tls.TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA,
61+
"TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA": tls.TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA,
62+
"TLS_ECDHE_RSA_WITH_RC4_128_SHA": tls.TLS_ECDHE_RSA_WITH_RC4_128_SHA,
63+
"TLS_ECDHE_RSA_WITH_3DES_EDE_CBC_SHA": tls.TLS_ECDHE_RSA_WITH_3DES_EDE_CBC_SHA,
64+
"TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA": tls.TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA,
65+
"TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA": tls.TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA,
66+
"TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA256": tls.TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA256,
67+
"TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA256": tls.TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA256,
68+
"TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256": tls.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256,
69+
"TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256": tls.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256,
70+
"TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384": tls.TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384,
71+
"TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384": tls.TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384,
72+
"TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256": tls.TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256,
73+
"TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256": tls.TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256,
74+
}
75+
3476
type HttpsConfig struct {
35-
CertFile string `json:"certFile" yaml:"certFile"`
36-
KeyFile string `json:"keyFile" yaml:"keyFile"`
37-
CaCertFile string `json:"caCertFile" yaml:"caCertFile"`
77+
CertFile string `json:"certFile" yaml:"certFile"`
78+
KeyFile string `json:"keyFile" yaml:"keyFile"`
79+
CaCertFile string `json:"caCertFile" yaml:"caCertFile"`
80+
MinTlsVersion uint16 `json:"minTlsVersion" yaml:"minTlsVersion"`
81+
MaxTlsVersion uint16 `json:"maxTlsVersion" yaml:"maxTlsVersion"`
82+
CipherSuites []uint16 `json:"cipherSuites" yaml:"cipherSuites"`
3883
}
3984

4085
type SecurityConfig struct {
@@ -43,6 +88,19 @@ type SecurityConfig struct {
4388
SignVerifyKey string `json:"signKey" yaml:"signKey"`
4489
}
4590

91+
func defaultCipherSuites() []uint16 {
92+
return []uint16{
93+
tls.TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384,
94+
tls.TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384,
95+
tls.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256,
96+
tls.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256,
97+
tls.TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA,
98+
tls.TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA,
99+
tls.TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA,
100+
tls.TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA,
101+
}
102+
}
103+
46104
func defaultSecurityConfigLoc() string {
47105
return defaultConfigLoc
48106
}
@@ -107,9 +165,9 @@ func GetHttpsConfig() HttpsConfig {
107165
certFileStr := os.Getenv(httpsCertFileKey)
108166
keyFileStr := os.Getenv(httpsKeyFileKey)
109167
caCertFileStr := os.Getenv(httpsCaCertFileKey)
110-
log.Printf("[GetHttpsConfig] %s: %s", httpsCertFileKey, certFileStr)
111-
log.Printf("[GetHttpsConfig] %s: %s", httpsKeyFileKey, keyFileStr)
112-
log.Printf("[GetHttpsConfig] %s: %s", httpsCaCertFileKey, caCertFileStr)
168+
minTlsVersionStr := os.Getenv(minTlsVersionKey)
169+
maxTlsVersionStr := os.Getenv(maxTlsVersionKey)
170+
cipherSuitesStr := os.Getenv(cipherSuitesKey)
113171

114172
if certFileStr == "" {
115173
httpsConfig.CertFile = defaultHttpsCertFile
@@ -124,11 +182,43 @@ func GetHttpsConfig() HttpsConfig {
124182
httpsConfig.KeyFile = keyFileStr
125183
}
126184
if caCertFileStr == "" {
127-
httpsConfig.CaCertFile = defaultHttpsCaCertFileKey
128-
log.Printf("[GetHttpsConfig] using default ca cert file: %s", defaultHttpsCaCertFileKey)
185+
httpsConfig.CaCertFile = defaultHttpsCaCertFile
186+
log.Printf("[GetHttpsConfig] using default ca cert file: %s", defaultHttpsCaCertFile)
129187
} else {
130188
httpsConfig.CaCertFile = caCertFileStr
131189
}
190+
if minTlsVersionStr == "" {
191+
httpsConfig.MinTlsVersion = defaultMinTlsVersion
192+
log.Printf("[GetHttpsConfig] using default min TLS version: %s", tlsVersionToString(defaultMinTlsVersion))
193+
} else {
194+
minTlsVersion, valid := tlsVersionMap[minTlsVersionStr]
195+
if !valid {
196+
log.Fatalf("[GetHttpsConfig] The specified TLS version is not a valid TLS version: %s", minTlsVersionStr)
197+
}
198+
httpsConfig.MinTlsVersion = minTlsVersion
199+
}
200+
if maxTlsVersionStr == "" {
201+
httpsConfig.MaxTlsVersion = defaultMaxTlsVersion
202+
log.Printf("[GetHttpsConfig] using default max TLS version: %s", tlsVersionToString(defaultMaxTlsVersion))
203+
} else {
204+
maxTlsVersion, valid := tlsVersionMap[maxTlsVersionStr]
205+
if !valid {
206+
log.Fatalf("[GetHttpsConfig] The specified TLS version is not a valid TLS version: %s", maxTlsVersionStr)
207+
}
208+
httpsConfig.MaxTlsVersion = maxTlsVersion
209+
}
210+
if cipherSuitesStr == "" {
211+
httpsConfig.CipherSuites = defaultCipherSuites()
212+
log.Printf("[GetHttpsConfig] using default list of cipher suites: %s", MapUint16ToString(defaultCipherSuites(), cipherSuiteToString))
213+
} else {
214+
httpsConfig.CipherSuites = MapStringToUint16(strings.Split(cipherSuitesStr, ","), func(s string) uint16 {
215+
cipherSuite, valid := cipherSuiteMap[s]
216+
if !valid {
217+
log.Fatalf("[GetHttpsConfig] The specified cipher suite is not a valid cipher suite: %s", s)
218+
}
219+
return cipherSuite
220+
})
221+
}
132222
return httpsConfig
133223
}
134224

@@ -158,6 +248,24 @@ func GetConcatenatedCertFilePath(httpsConfig HttpsConfig) (string, error) {
158248
return tmpFile.Name(), nil
159249
}
160250

251+
func tlsVersionToString(tlsVersion uint16) string {
252+
for str, constant := range tlsVersionMap {
253+
if constant == tlsVersion {
254+
return str
255+
}
256+
}
257+
return fmt.Sprintf("(0x%04x)", tlsVersion)
258+
}
259+
260+
func cipherSuiteToString(cipherSuite uint16) string {
261+
for str, constant := range cipherSuiteMap {
262+
if constant == cipherSuite {
263+
return str
264+
}
265+
}
266+
return fmt.Sprintf("(0x%04x)", cipherSuite)
267+
}
268+
161269
func DetermineSecurityDetails(getEnv func(key string) string, securityConfig func() string) (*SecurityConfig, error) {
162270
var config SecurityConfig
163271
configLoc := strings.TrimSpace(getEnv(configLocKey))

saltboot/netutil_test.go

+94-13
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,10 @@
11
package saltboot
22

33
import (
4+
"crypto/tls"
5+
"log"
46
"os"
7+
"os/exec"
58
"strings"
69
"testing"
710
)
@@ -54,14 +57,13 @@ func TestDetermineHttpsPortDefault(t *testing.T) {
5457

5558
func TestDetermineHttpsPortCustom(t *testing.T) {
5659
os.Setenv(httpsPortKey, "8080")
60+
defer os.Unsetenv(httpsPortKey)
5761

5862
port := DetermineHttpsPort()
5963

6064
if port != 8080 {
6165
t.Errorf("port does not match the custom HTTPS port %d == %d", 8080, port)
6266
}
63-
64-
os.Unsetenv(httpsPortKey)
6567
}
6668

6769
func TestDetermineHttpPortDefault(t *testing.T) {
@@ -74,14 +76,13 @@ func TestDetermineHttpPortDefault(t *testing.T) {
7476

7577
func TestDetermineHttpPortCustom(t *testing.T) {
7678
os.Setenv(portKey, "8080")
79+
defer os.Unsetenv(portKey)
7780

7881
port := DetermineHttpPort()
7982

8083
if port != 8080 {
8184
t.Errorf("port does not match the custom port %d == %d", 8080, port)
8285
}
83-
84-
os.Unsetenv(portKey)
8586
}
8687

8788
func TestGetHttpsConfigDefault(t *testing.T) {
@@ -93,35 +94,115 @@ func TestGetHttpsConfigDefault(t *testing.T) {
9394
if httpsConfig.KeyFile != defaultHttpsKeyFile {
9495
t.Errorf("key file does not match the default %s == %s", defaultHttpsKeyFile, httpsConfig.KeyFile)
9596
}
96-
if httpsConfig.CaCertFile != defaultHttpsCaCertFileKey {
97-
t.Errorf("ca cert file does not match the default %s == %s", defaultHttpsCaCertFileKey, httpsConfig.CaCertFile)
97+
if httpsConfig.CaCertFile != defaultHttpsCaCertFile {
98+
t.Errorf("ca cert file does not match the default %s == %s", defaultHttpsCaCertFile, httpsConfig.CaCertFile)
99+
}
100+
if httpsConfig.MinTlsVersion != defaultMinTlsVersion {
101+
t.Errorf("min TLS version does not match the default %d == %d", defaultMinTlsVersion, httpsConfig.MinTlsVersion)
102+
}
103+
if httpsConfig.MaxTlsVersion != defaultMaxTlsVersion {
104+
t.Errorf("max TLS version does not match the default %d == %d", defaultMaxTlsVersion, httpsConfig.MaxTlsVersion)
105+
}
106+
if !EqualUint16Slices(httpsConfig.CipherSuites, defaultCipherSuites()) {
107+
t.Errorf("list of cipher suites does not match the default list %d == %d", defaultCipherSuites(), httpsConfig.CipherSuites)
98108
}
99109
}
100110

101111
func TestGetHttpsConfigCustom(t *testing.T) {
102112
os.Setenv(httpsCertFileKey, "path/certfile.pem")
103113
os.Setenv(httpsKeyFileKey, "path/keyfile.pem")
104114
os.Setenv(httpsCaCertFileKey, "path/ca.pem")
115+
os.Setenv(minTlsVersionKey, "1.1")
116+
os.Setenv(maxTlsVersionKey, "1.2")
117+
os.Setenv(cipherSuitesKey, "TLS_ECDHE_RSA_WITH_RC4_128_SHA,TLS_RSA_WITH_AES_256_GCM_SHA384")
118+
defer os.Unsetenv(httpsCertFileKey)
119+
defer os.Unsetenv(httpsKeyFileKey)
120+
defer os.Unsetenv(httpsCaCertFileKey)
121+
defer os.Unsetenv(minTlsVersionKey)
122+
defer os.Unsetenv(maxTlsVersionKey)
123+
defer os.Unsetenv(cipherSuitesKey)
105124

106125
httpsConfig := GetHttpsConfig()
107126

108127
if httpsConfig.CertFile != "path/certfile.pem" {
109-
t.Errorf("cert file does not match the default %s == %s", "path/certfile.pem", httpsConfig.CertFile)
128+
t.Errorf("cert file does not match the one specified %s == %s", "path/certfile.pem", httpsConfig.CertFile)
110129
}
111130
if httpsConfig.KeyFile != "path/keyfile.pem" {
112-
t.Errorf("key file does not match the default %s == %s", "path/keyfile.pem", httpsConfig.KeyFile)
131+
t.Errorf("key file does not match the one specified %s == %s", "path/keyfile.pem", httpsConfig.KeyFile)
113132
}
114133
if httpsConfig.CaCertFile != "path/ca.pem" {
115-
t.Errorf("ca cert file does not match the default %s == %s", "path/ca.pem", httpsConfig.CaCertFile)
134+
t.Errorf("ca cert file does not match the one specified %s == %s", "path/ca.pem", httpsConfig.CaCertFile)
135+
}
136+
if httpsConfig.MinTlsVersion != tls.VersionTLS11 {
137+
t.Errorf("min TLS version does not match the one specified %d == %d", tls.VersionTLS11, httpsConfig.MinTlsVersion)
138+
}
139+
if httpsConfig.MaxTlsVersion != tls.VersionTLS12 {
140+
t.Errorf("max TLS version does not match the one specified %d == %d", tls.VersionTLS12, httpsConfig.MaxTlsVersion)
141+
}
142+
expectedCipherSuites := []uint16{tls.TLS_ECDHE_RSA_WITH_RC4_128_SHA, tls.TLS_RSA_WITH_AES_256_GCM_SHA384}
143+
if !EqualUint16Slices(httpsConfig.CipherSuites, expectedCipherSuites) {
144+
t.Errorf("list of cipher suites does not match the one specified %d == %d", expectedCipherSuites, httpsConfig.CipherSuites)
145+
}
146+
}
147+
148+
func TestGetHttpsConfigInvalidMinTlsVersion(t *testing.T) {
149+
os.Setenv(minTlsVersionKey, "0.9")
150+
defer os.Unsetenv(minTlsVersionKey)
151+
if os.Getenv("SALTBOOT_MIN_TLS_CRASHER") == "1" {
152+
GetHttpsConfig()
153+
return
154+
}
155+
cmd := exec.Command(os.Args[0], "-test.run=TestGetHttpsConfigInvalidMinTlsVersion")
156+
cmd.Env = append(cmd.Env, "SALTBOOT_MIN_TLS_CRASHER=1")
157+
cmd.Stderr = os.Stderr
158+
159+
err := cmd.Run()
160+
161+
if e, ok := err.(*exec.ExitError); ok && !e.Success() {
162+
log.Printf("Process exited as expected: %v", err)
163+
return
164+
}
165+
t.Errorf("Process did not exit as expected: %v", err)
166+
}
167+
168+
func TestGetHttpsConfigInvalidMaxTlsVersion(t *testing.T) {
169+
os.Setenv(maxTlsVersionKey, "0.9")
170+
defer os.Unsetenv(maxTlsVersionKey)
171+
if os.Getenv("SALTBOOT_MAX_TLS_CRASHER") == "1" {
172+
GetHttpsConfig()
173+
return
116174
}
175+
cmd := exec.Command(os.Args[0], "-test.run=TestGetHttpsConfigInvalidMaxTlsVersion")
176+
cmd.Env = append(cmd.Env, "SALTBOOT_MAX_TLS_CRASHER=1")
177+
cmd.Stderr = os.Stderr
178+
179+
err := cmd.Run()
117180

118-
os.Unsetenv(httpsCertFileKey)
119-
os.Unsetenv(httpsKeyFileKey)
120-
os.Unsetenv(httpsCaCertFileKey)
181+
if e, ok := err.(*exec.ExitError); ok && !e.Success() {
182+
log.Printf("Process exited as expected: %v", err)
183+
return
184+
}
185+
t.Errorf("Process did not exit as expected: %v", err)
121186
}
122187

123-
func TestGetConcatenatedCertFilePath(t *testing.T) {
188+
func TestGetHttpsConfigInvalidCipherSuite(t *testing.T) {
189+
os.Setenv(cipherSuitesKey, "TLS_ECDHE_RSA_WITH_RC4_128_SHA,NOT_GOOD_VERY_BAD_CIPHER_SUITE,TLS_RSA_WITH_AES_256_GCM_SHA384")
190+
defer os.Unsetenv(cipherSuitesKey)
191+
if os.Getenv("SALTBOOT_CIPHER_SUITES_CRASHER") == "1" {
192+
GetHttpsConfig()
193+
return
194+
}
195+
cmd := exec.Command(os.Args[0], "-test.run=TestGetHttpsConfigInvalidCipherSuite")
196+
cmd.Env = append(cmd.Env, "SALTBOOT_CIPHER_SUITES_CRASHER=1")
197+
cmd.Stderr = os.Stderr
198+
199+
err := cmd.Run()
124200

201+
if e, ok := err.(*exec.ExitError); ok && !e.Success() {
202+
log.Printf("Process exited as expected: %v", err)
203+
return
204+
}
205+
t.Errorf("Process did not exit as expected: %v", err)
125206
}
126207

127208
func TestConfigfileFoundByEnv(t *testing.T) {

0 commit comments

Comments
 (0)