Skip to content

Commit

Permalink
Add TLSv1.3 support.
Browse files Browse the repository at this point in the history
Added optional TLS min/max protocol version and command line switches to set
versions for the etcd server.

If max version is not explicitly set by the user, let Go select the max
version which is currently TLSv1.3. Previously max version was set to TLSv1.2.

Signed-off-by: Tero Saarni <[email protected]>
  • Loading branch information
tsaarni committed Jan 25, 2023
1 parent 3b612ce commit fc5ab82
Show file tree
Hide file tree
Showing 9 changed files with 288 additions and 11 deletions.
36 changes: 36 additions & 0 deletions client/pkg/tlsutil/versions.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
// Copyright 2023 The etcd Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package tlsutil

import (
"crypto/tls"
"fmt"
)

// tlsVersions is a map of TLS version string to the value of tls.Config.Min/MaxVersion.
var tlsVersions = map[string]uint16{
"": 0, // If version was not given use 0 (uninitialized version) to let Go decide.
"TLS12": tls.VersionTLS12,
"TLS13": tls.VersionTLS13,
}

// GetTLSVersion returns the corresponding TLS version.
func GetTLSVersion(version string) (uint16, error) {
v, ok := tlsVersions[version]
if !ok {
return 0, fmt.Errorf("unexpected TLS version %q", version)
}
return v, nil
}
46 changes: 46 additions & 0 deletions client/pkg/tlsutil/versions_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
// Copyright 2023 The etcd Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package tlsutil

import (
"crypto/tls"
"testing"
)

func TestGetVersion_success(t *testing.T) {
versions := map[string]uint16{
"": 0,
"TLS12": tls.VersionTLS12,
"TLS13": tls.VersionTLS13,
}

// Iterate versions
for version, expected := range versions {
got, err := GetTLSVersion(version)
if err != nil {
t.Fatal(err)
}
if got != expected {
t.Fatalf("Got unexpected TLS version expected=%v got=%v", expected, got)
}
}
}

func TestGetVersion_not_existing(t *testing.T) {
_, err := GetTLSVersion("not_existing")
if err == nil {
t.Fatal("Expected error")
}
}
29 changes: 18 additions & 11 deletions client/pkg/transport/listener.go
Original file line number Diff line number Diff line change
Expand Up @@ -166,6 +166,14 @@ type TLSInfo struct {
// Note that cipher suites are prioritized in the given order.
CipherSuites []uint16

// MinVersion is the minimum TLS version that is acceptable.
// If not set, the minimum version is TLS 1.2.
MinVersion uint16

// MaxVersion is the maximum TLS version that is acceptable.
// If not set, the default used by Go is selected (see tls.Config.MaxVersion).
MaxVersion uint16

selfCert bool

// parseFunc exists to simplify testing. Typically, parseFunc
Expand Down Expand Up @@ -380,8 +388,17 @@ func (info TLSInfo) baseConfig() (*tls.Config, error) {
}
}

var minVersion uint16
if info.MinVersion != 0 {
minVersion = info.MinVersion
} else {
// Default minimum version is TLS 1.2, previous versions are insecure and deprecated.
minVersion = tls.VersionTLS12
}

cfg := &tls.Config{
MinVersion: tls.VersionTLS12,
MinVersion: minVersion,
MaxVersion: info.MaxVersion,
ServerName: info.ServerName,
}

Expand Down Expand Up @@ -512,11 +529,6 @@ func (info TLSInfo) ServerConfig() (*tls.Config, error) {
// "h2" NextProtos is necessary for enabling HTTP2 for go's HTTP server
cfg.NextProtos = []string{"h2"}

// go1.13 enables TLS 1.3 by default
// and in TLS 1.3, cipher suites are not configurable
// setting Max TLS version to TLS 1.2 for go 1.13
cfg.MaxVersion = tls.VersionTLS12

return cfg, nil
}

Expand Down Expand Up @@ -571,11 +583,6 @@ func (info TLSInfo) ClientConfig() (*tls.Config, error) {
}
}

// go1.13 enables TLS 1.3 by default
// and in TLS 1.3, cipher suites are not configurable
// setting Max TLS version to TLS 1.2 for go 1.13
cfg.MaxVersion = tls.VersionTLS12

return cfg, nil
}

Expand Down
26 changes: 26 additions & 0 deletions server/embed/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -226,6 +226,11 @@ type Config struct {
// Note that cipher suites are prioritized in the given order.
CipherSuites []string `json:"cipher-suites"`

// TlsMinVersion is the minimum accepted TLS version between client/server and peers.
TlsMinVersion string `json:"tls-min-version"`
// TlsMaxVersion is the maximum accepted TLS version between client/server and peers.
TlsMaxVersion string `json:"tls-max-version"`

ClusterState string `json:"initial-cluster-state"`
DNSCluster string `json:"discovery-srv"`
DNSClusterServiceName string `json:"discovery-srv-name"`
Expand Down Expand Up @@ -660,6 +665,27 @@ func updateCipherSuites(tls *transport.TLSInfo, ss []string) error {
return nil
}

func updateMinMaxVersions(info *transport.TLSInfo, min, max string) error {
minVersion, err := tlsutil.GetTLSVersion(min)
if err != nil {
return err
}

maxVersion, err := tlsutil.GetTLSVersion(max)
if err != nil {
return err
}

// Check if both min and max were provided and min is greater than max
if minVersion != 0 && maxVersion != 0 && minVersion > maxVersion {
return fmt.Errorf("min version (%s) is greater than max version (%s)", min, max)
}

info.MinVersion = minVersion
info.MaxVersion = maxVersion
return nil
}

// Validate ensures that '*embed.Config' fields are properly configured.
func (cfg *Config) Validate() error {
if err := cfg.setupLogging(); err != nil {
Expand Down
78 changes: 78 additions & 0 deletions server/embed/config_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
package embed

import (
"crypto/tls"
"errors"
"fmt"
"net"
Expand Down Expand Up @@ -429,3 +430,80 @@ func TestLogRotation(t *testing.T) {
})
}
}

func TestTLSVersionMinMax(t *testing.T) {
tests := []struct {
name string
config Config
expectError bool
expectedMinTLSVersion uint16
expectedMaxTLSVersion uint16
}{
{
name: "Minimum TLS version is set",
config: Config{
TlsMinVersion: "TLS13",
},
expectedMinTLSVersion: tls.VersionTLS13,
expectedMaxTLSVersion: 0,
},
{
name: "Maximum TLS version is set",
config: Config{
TlsMaxVersion: "TLS12",
},
expectedMinTLSVersion: 0,
expectedMaxTLSVersion: tls.VersionTLS12,
},
{
name: "Minimum and Maximum TLS versions are set",
config: Config{
TlsMinVersion: "TLS13",
TlsMaxVersion: "TLS13",
},
expectedMinTLSVersion: tls.VersionTLS13,
expectedMaxTLSVersion: tls.VersionTLS13,
},
{
name: "Minimum and Maximum TLS versions are set in reverse order",
config: Config{
TlsMinVersion: "TLS13",
TlsMaxVersion: "TLS12",
},
expectError: true,
},
{
name: "Invalid minimum TLS version",
config: Config{
TlsMinVersion: "invalid version",
},
expectError: true,
},
{
name: "Invalid maximum TLS version",
config: Config{
TlsMaxVersion: "invalid version",
},
expectError: true,
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
err := updateMinMaxVersions(&tt.config.PeerTLSInfo, tt.config.TlsMinVersion, tt.config.TlsMaxVersion)
if (err != nil) != tt.expectError {
t.Errorf("updateMinMaxVersions() = %q, expected error: %v", err, tt.expectError)
}

err = updateMinMaxVersions(&tt.config.ClientTLSInfo, tt.config.TlsMinVersion, tt.config.TlsMaxVersion)
if (err != nil) != tt.expectError {
t.Errorf("updateMinMaxVersions() = %q, expected error: %v", err, tt.expectError)
}

assert.Equal(t, tt.expectedMinTLSVersion, tt.config.PeerTLSInfo.MinVersion)
assert.Equal(t, tt.expectedMaxTLSVersion, tt.config.PeerTLSInfo.MaxVersion)
assert.Equal(t, tt.expectedMinTLSVersion, tt.config.ClientTLSInfo.MinVersion)
assert.Equal(t, tt.expectedMaxTLSVersion, tt.config.ClientTLSInfo.MaxVersion)
})
}
}
6 changes: 6 additions & 0 deletions server/embed/etcd.go
Original file line number Diff line number Diff line change
Expand Up @@ -499,6 +499,9 @@ func configurePeerListeners(cfg *Config) (peers []*peerListener, err error) {
if err = cfg.PeerSelfCert(); err != nil {
cfg.logger.Fatal("failed to get peer self-signed certs", zap.Error(err))
}
if err = updateMinMaxVersions(&cfg.PeerTLSInfo, cfg.TlsMinVersion, cfg.TlsMaxVersion); err != nil {
return nil, err
}
if !cfg.PeerTLSInfo.Empty() {
cfg.logger.Info(
"starting with peer TLS",
Expand Down Expand Up @@ -611,6 +614,9 @@ func configureClientListeners(cfg *Config) (sctxs map[string]*serveCtx, err erro
if err = cfg.ClientSelfCert(); err != nil {
cfg.logger.Fatal("failed to get client self-signed certs", zap.Error(err))
}
if err = updateMinMaxVersions(&cfg.ClientTLSInfo, cfg.TlsMinVersion, cfg.TlsMaxVersion); err != nil {
return nil, err
}
if cfg.EnablePprof {
cfg.logger.Info("pprof is enabled", zap.String("path", debugutil.HTTPPrefixPProf))
}
Expand Down
2 changes: 2 additions & 0 deletions server/etcdmain/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -215,6 +215,8 @@ func newConfig() *config {
fs.StringVar(&cfg.ec.PeerTLSInfo.AllowedHostname, "peer-cert-allowed-hostname", "", "Allowed TLS hostname for inter peer authentication.")
fs.Var(flags.NewStringsValue(""), "cipher-suites", "Comma-separated list of supported TLS cipher suites between client/server and peers (empty will be auto-populated by Go).")
fs.BoolVar(&cfg.ec.PeerTLSInfo.SkipClientSANVerify, "experimental-peer-skip-client-san-verification", false, "Skip verification of SAN field in client certificate for peer connections.")
fs.StringVar(&cfg.ec.TlsMinVersion, "tls-min-version", "TLS12", "Minimum TLS version supported by etcd. Possible values: TLS12, TLS13.")
fs.StringVar(&cfg.ec.TlsMaxVersion, "tls-max-version", "", "Maximum TLS version supported by etcd. Possible values: TLS12, TLS13 (empty will be auto-populated by Go).")

fs.Var(
flags.NewUniqueURLsWithExceptions("*", "*"),
Expand Down
4 changes: 4 additions & 0 deletions server/etcdmain/help.go
Original file line number Diff line number Diff line change
Expand Up @@ -199,6 +199,10 @@ Security:
Comma-separated whitelist of origins for CORS, or cross-origin resource sharing, (empty or * means allow all).
--host-whitelist '*'
Acceptable hostnames from HTTP client requests, if server is not secure (empty or * means allow all).
--tls-min-version 'TLS12'
Minimum TLS version supported by etcd. Possible values: TLS12, TLS13.
--tls-max-version ''
Maximum TLS version supported by etcd. Possible values: TLS12, TLS13 (empty will be auto-populated by Go).
Auth:
--auth-token 'simple'
Expand Down
72 changes: 72 additions & 0 deletions tests/integration/v3_tls_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,12 @@ func testTLSCipherSuites(t *testing.T, valid bool) {
srvTLS.CipherSuites, cliTLS.CipherSuites = cipherSuites[:2], cipherSuites[2:]
}

// go1.13 enables TLS 1.3 by default
// and in TLS 1.3, cipher suites are not configurable,
// so setting Max TLS version to TLS 1.2 to test cipher config.
srvTLS.MaxVersion = tls.VersionTLS12
cliTLS.MaxVersion = tls.VersionTLS12

clus := integration.NewCluster(t, &integration.ClusterConfig{Size: 1, ClientTLS: &srvTLS})
defer clus.Terminate(t)

Expand All @@ -72,3 +78,69 @@ func testTLSCipherSuites(t *testing.T, valid bool) {
t.Fatalf("expected TLS handshake success, got %v", cerr)
}
}

func TestTLSMinMaxVersion(t *testing.T) {
integration.BeforeTest(t)

tests := []struct {
name string
minVersion uint16
maxVersion uint16
expectError bool
}{
{
name: "Connect with default TLS version should succeed",
minVersion: 0,
maxVersion: 0,
},
{
name: "Connect with TLS 1.2 only should fail",
minVersion: tls.VersionTLS12,
maxVersion: tls.VersionTLS12,
expectError: true,
},
{
name: "Connect with TLS 1.2 and 1.3 should succeed",
minVersion: tls.VersionTLS12,
maxVersion: tls.VersionTLS13,
},
{
name: "Connect with TLS 1.3 only should succeed",
minVersion: tls.VersionTLS13,
maxVersion: tls.VersionTLS13,
},
}

// Configure server to support TLS 1.3 only.
srvTLS := integration.TestTLSInfo
srvTLS.MinVersion = tls.VersionTLS13
srvTLS.MaxVersion = tls.VersionTLS13
clus := integration.NewCluster(t, &integration.ClusterConfig{Size: 1, ClientTLS: &srvTLS})
defer clus.Terminate(t)

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
cc, err := integration.TestTLSInfo.ClientConfig()
if err != nil {
t.Fatal(err)
}
cc.MinVersion = tt.minVersion
cc.MaxVersion = tt.maxVersion
cli, cerr := integration.NewClient(t, clientv3.Config{
Endpoints: []string{clus.Members[0].GRPCURL()},
DialTimeout: time.Second,
DialOptions: []grpc.DialOption{grpc.WithBlock()},
TLS: cc,
})
if cli != nil {
cli.Close()
}
if tt.expectError && cerr != context.DeadlineExceeded {
t.Fatalf("expected %v with TLS handshake failure, got %v", context.DeadlineExceeded, cerr)
}
if (cerr != nil) != tt.expectError {
t.Fatalf("expected TLS handshake success, got %v", cerr)
}
})
}
}

0 comments on commit fc5ab82

Please sign in to comment.