Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[3.4] Backport tls 1.3 support #15486

Merged
merged 1 commit into from
Mar 23, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
35 changes: 35 additions & 0 deletions embed/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -195,6 +195,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 @@ -575,6 +580,17 @@ func updateCipherSuites(tls *transport.TLSInfo, ss []string) error {
return nil
}

func updateMinMaxVersions(info *transport.TLSInfo, min, max string) {
// Validate() has been called to check the user input, so it should never fail.
var err error
if info.MinVersion, err = tlsutil.GetTLSVersion(min); err != nil {
panic(err)
}
if info.MaxVersion, err = tlsutil.GetTLSVersion(max); err != nil {
panic(err)
}
}

// Validate ensures that '*embed.Config' fields are properly configured.
func (cfg *Config) Validate() error {
if err := cfg.setupLogging(); err != nil {
Expand Down Expand Up @@ -646,6 +662,25 @@ func (cfg *Config) Validate() error {
return fmt.Errorf("setting experimental-enable-lease-checkpoint-persist requires experimental-enable-lease-checkpoint")
}

minVersion, err := tlsutil.GetTLSVersion(cfg.TlsMinVersion)
if err != nil {
return err
}
maxVersion, err := tlsutil.GetTLSVersion(cfg.TlsMaxVersion)
if err != nil {
return err
}

// maxVersion == 0 means that Go selects the highest available version.
if maxVersion != 0 && minVersion > maxVersion {
return fmt.Errorf("min version (%s) is greater than max version (%s)", cfg.TlsMinVersion, cfg.TlsMaxVersion)
}

// Check if user attempted to configure ciphers for TLS1.3 only: Go does not support that currently.
if minVersion == tls.VersionTLS13 && len(cfg.CipherSuites) > 0 {
return fmt.Errorf("cipher suites cannot be configured when only TLS1.3 is enabled")
}

return nil
}

Expand Down
79 changes: 79 additions & 0 deletions embed/config_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,13 +15,15 @@
package embed

import (
"crypto/tls"
"fmt"
"io/ioutil"
"net/url"
"os"
"testing"
"time"

"github.com/stretchr/testify/assert"
"go.etcd.io/etcd/pkg/transport"

"sigs.k8s.io/yaml"
Expand Down Expand Up @@ -202,3 +204,80 @@ func TestAutoCompactionModeParse(t *testing.T) {
}
}
}

func TestTLSVersionMinMax(t *testing.T) {
tests := []struct {
name string
givenTLSMinVersion string
givenTLSMaxVersion string
givenCipherSuites []string
expectError bool
expectedMinTLSVersion uint16
expectedMaxTLSVersion uint16
}{
{
name: "Minimum TLS version is set",
givenTLSMinVersion: "TLS1.3",
expectedMinTLSVersion: tls.VersionTLS13,
expectedMaxTLSVersion: 0,
},
{
name: "Maximum TLS version is set",
givenTLSMaxVersion: "TLS1.2",
expectedMinTLSVersion: 0,
expectedMaxTLSVersion: tls.VersionTLS12,
},
{
name: "Minimum and Maximum TLS versions are set",
givenTLSMinVersion: "TLS1.3",
givenTLSMaxVersion: "TLS1.3",
expectedMinTLSVersion: tls.VersionTLS13,
expectedMaxTLSVersion: tls.VersionTLS13,
},
{
name: "Minimum and Maximum TLS versions are set in reverse order",
givenTLSMinVersion: "TLS1.3",
givenTLSMaxVersion: "TLS1.2",
expectError: true,
},
{
name: "Invalid minimum TLS version",
givenTLSMinVersion: "invalid version",
expectError: true,
},
{
name: "Invalid maximum TLS version",
givenTLSMaxVersion: "invalid version",
expectError: true,
},
{
name: "Cipher suites configured for TLS 1.3",
givenTLSMinVersion: "TLS1.3",
givenCipherSuites: []string{"TLS_AES_128_GCM_SHA256"},
expectError: true,
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
cfg := NewConfig()
cfg.TlsMinVersion = tt.givenTLSMinVersion
cfg.TlsMaxVersion = tt.givenTLSMaxVersion
cfg.CipherSuites = tt.givenCipherSuites

err := cfg.Validate()
if err != nil {
assert.True(t, tt.expectError, "Validate() returned error while expecting success: %v", err)
return
}

updateMinMaxVersions(&cfg.PeerTLSInfo, cfg.TlsMinVersion, cfg.TlsMaxVersion)
updateMinMaxVersions(&cfg.ClientTLSInfo, cfg.TlsMinVersion, cfg.TlsMaxVersion)

assert.Equal(t, tt.expectedMinTLSVersion, cfg.PeerTLSInfo.MinVersion)
assert.Equal(t, tt.expectedMaxTLSVersion, cfg.PeerTLSInfo.MaxVersion)
assert.Equal(t, tt.expectedMinTLSVersion, cfg.ClientTLSInfo.MinVersion)
assert.Equal(t, tt.expectedMaxTLSVersion, cfg.ClientTLSInfo.MaxVersion)
})
}
}
6 changes: 6 additions & 0 deletions embed/etcd.go
Original file line number Diff line number Diff line change
Expand Up @@ -471,6 +471,9 @@ func configurePeerListeners(cfg *Config) (peers []*peerListener, err error) {
plog.Fatalf("could not get certs (%v)", err)
}
}

updateMinMaxVersions(&cfg.PeerTLSInfo, cfg.TlsMinVersion, cfg.TlsMaxVersion)

if !cfg.PeerTLSInfo.Empty() {
if cfg.logger != nil {
cfg.logger.Info(
Expand Down Expand Up @@ -608,6 +611,9 @@ func configureClientListeners(cfg *Config) (sctxs map[string]*serveCtx, err erro
plog.Fatalf("could not get certs (%v)", err)
}
}

updateMinMaxVersions(&cfg.ClientTLSInfo, cfg.TlsMinVersion, cfg.TlsMaxVersion)

if cfg.EnablePprof {
if cfg.logger != nil {
cfg.logger.Info("pprof is enabled", zap.String("path", debugutil.HTTPPrefixPProf))
Expand Down
3 changes: 3 additions & 0 deletions etcdmain/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ import (
"go.etcd.io/etcd/embed"
"go.etcd.io/etcd/pkg/flags"
"go.etcd.io/etcd/pkg/logutil"
"go.etcd.io/etcd/pkg/tlsutil"
"go.etcd.io/etcd/pkg/types"
"go.etcd.io/etcd/version"

Expand Down Expand Up @@ -216,6 +217,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", string(tlsutil.TLSVersion12), "Minimum TLS version supported by etcd. Possible values: TLS1.2, TLS1.3.")
fs.StringVar(&cfg.ec.TlsMaxVersion, "tls-max-version", string(tlsutil.TLSVersionDefault), "Maximum TLS version supported by etcd. Possible values: TLS1.2, TLS1.3 (empty defers to Go).")

fs.Var(
flags.NewUniqueURLsWithExceptions("*", "*"),
Expand Down
4 changes: 4 additions & 0 deletions etcdmain/help.go
Original file line number Diff line number Diff line change
Expand Up @@ -158,6 +158,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 'TLS1.2'
Minimum TLS version supported by etcd. Possible values: TLS1.2, TLS1.3.
--tls-max-version ''
Maximum TLS version supported by etcd. Possible values: TLS1.2, TLS1.3 (empty will be auto-populated by Go).

Auth:
--auth-token 'simple'
Expand Down
69 changes: 69 additions & 0 deletions integration/v3_tls_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import (
"testing"
"time"

"github.com/stretchr/testify/assert"
"go.etcd.io/etcd/clientv3"
"go.etcd.io/etcd/pkg/testutil"

Expand Down Expand Up @@ -49,6 +50,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 := NewClusterV3(t, &ClusterConfig{Size: 1, ClientTLS: &srvTLS})
defer clus.Terminate(t)

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

func TestTLSMinMaxVersion(t *testing.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 := testTLSInfo
srvTLS.MinVersion = tls.VersionTLS13
srvTLS.MaxVersion = tls.VersionTLS13
clus := NewClusterV3(t, &ClusterConfig{Size: 1, ClientTLS: &srvTLS})
defer clus.Terminate(t)

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
cc, err := testTLSInfo.ClientConfig()
assert.NoError(t, err)

cc.MinVersion = tt.minVersion
cc.MaxVersion = tt.maxVersion
cli, cerr := clientv3.New(clientv3.Config{
Endpoints: []string{clus.Members[0].GRPCAddr()},
DialTimeout: time.Second,
DialOptions: []grpc.DialOption{grpc.WithBlock()},
TLS: cc,
})
if cerr != nil {
assert.True(t, tt.expectError, "got TLS handshake error while expecting success: %v", cerr)
assert.Equal(t, context.DeadlineExceeded, cerr, "expected %v with TLS handshake failure, got %v", context.DeadlineExceeded, cerr)
return
}

cli.Close()
})
}
}
47 changes: 47 additions & 0 deletions pkg/tlsutil/versions.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
// 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"
)

type TLSVersion string

// Constants for TLS versions.
const (
TLSVersionDefault TLSVersion = ""
TLSVersion12 TLSVersion = "TLS1.2"
TLSVersion13 TLSVersion = "TLS1.3"
)

// GetTLSVersion returns the corresponding tls.Version or error.
func GetTLSVersion(version string) (uint16, error) {
var v uint16

switch version {
case string(TLSVersionDefault):
v = 0 // 0 means let Go decide.
case string(TLSVersion12):
v = tls.VersionTLS12
case string(TLSVersion13):
v = tls.VersionTLS13
default:
return 0, fmt.Errorf("unexpected TLS version %q (must be one of: TLS1.2, TLS1.3)", version)
}

return v, nil
}
63 changes: 63 additions & 0 deletions pkg/tlsutil/versions_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
// 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"

"github.com/stretchr/testify/assert"
)

func TestGetVersion(t *testing.T) {
tests := []struct {
name string
version string
want uint16
expectError bool
}{
{
name: "TLS1.2",
version: "TLS1.2",
want: tls.VersionTLS12,
},
{
name: "TLS1.3",
version: "TLS1.3",
want: tls.VersionTLS13,
},
{
name: "Empty version",
version: "",
want: 0,
},
{
name: "Converting invalid version string to TLS version",
version: "not_existing",
expectError: true,
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := GetTLSVersion(tt.version)
if err != nil {
assert.True(t, tt.expectError, "GetTLSVersion() returned error while expecting success: %v", err)
return
}
assert.Equal(t, tt.want, got)
})
}
}
Loading