mirror of
https://github.com/etcd-io/etcd.git
synced 2024-09-27 06:25:44 +00:00
Merge pull request #15486 from jmhbnz/release-3.4
[3.4] Backport tls 1.3 support
This commit is contained in:
commit
82de82ee80
@ -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"`
|
||||
@ -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 {
|
||||
@ -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
|
||||
}
|
||||
|
||||
|
@ -15,6 +15,7 @@
|
||||
package embed
|
||||
|
||||
import (
|
||||
"crypto/tls"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"net/url"
|
||||
@ -22,6 +23,7 @@ import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"go.etcd.io/etcd/pkg/transport"
|
||||
|
||||
"sigs.k8s.io/yaml"
|
||||
@ -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)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
@ -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(
|
||||
@ -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))
|
||||
|
@ -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"
|
||||
|
||||
@ -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("*", "*"),
|
||||
|
@ -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'
|
||||
|
@ -20,6 +20,7 @@ import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"go.etcd.io/etcd/clientv3"
|
||||
"go.etcd.io/etcd/pkg/testutil"
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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
pkg/tlsutil/versions.go
Normal file
47
pkg/tlsutil/versions.go
Normal file
@ -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
pkg/tlsutil/versions_test.go
Normal file
63
pkg/tlsutil/versions_test.go
Normal file
@ -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)
|
||||
})
|
||||
}
|
||||
}
|
@ -84,6 +84,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
|
||||
@ -263,8 +271,17 @@ func (info TLSInfo) baseConfig() (*tls.Config, error) {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
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,
|
||||
}
|
||||
|
||||
|
@ -21,6 +21,7 @@ import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"go.etcd.io/etcd/pkg/expect"
|
||||
)
|
||||
|
||||
@ -336,3 +337,31 @@ func TestGrpcproxyAndListenCipherSuite(t *testing.T) {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestEtcdTLSVersion(t *testing.T) {
|
||||
|
||||
d := t.TempDir()
|
||||
proc, err := spawnCmd(
|
||||
[]string{
|
||||
binDir + "/etcd",
|
||||
"--data-dir", d,
|
||||
"--name", "e1",
|
||||
"--listen-client-urls", "https://0.0.0.0:0",
|
||||
"--advertise-client-urls", "https://0.0.0.0:0",
|
||||
"--listen-peer-urls", fmt.Sprintf("https://127.0.0.1:%d", etcdProcessBasePort),
|
||||
"--initial-advertise-peer-urls", fmt.Sprintf("https://127.0.0.1:%d", etcdProcessBasePort),
|
||||
"--initial-cluster", fmt.Sprintf("e1=https://127.0.0.1:%d", etcdProcessBasePort),
|
||||
"--peer-cert-file", certPath,
|
||||
"--peer-key-file", privateKeyPath,
|
||||
"--cert-file", certPath2,
|
||||
"--key-file", privateKeyPath2,
|
||||
|
||||
"--tls-min-version", "TLS1.2",
|
||||
"--tls-max-version", "TLS1.3",
|
||||
},
|
||||
)
|
||||
assert.NoError(t, err)
|
||||
assert.NoError(t, waitReadyExpectProc(proc, etcdServerReadyLines), "did not receive expected output from etcd process")
|
||||
assert.NoError(t, proc.Stop())
|
||||
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user