From 9254f8f05bddfdc35e9cb0cd901e8cd9eed67863 Mon Sep 17 00:00:00 2001 From: Allen Ray Date: Wed, 19 Oct 2022 16:02:03 -0400 Subject: [PATCH] Release-3.4: server/etcdmain: add configurable cipher list to gRPC proxy listener Signed-off-by: Allen Ray --- embed/config.go | 10 ++-- etcdmain/grpc_proxy.go | 36 ++++++++----- pkg/tlsutil/cipher_suites.go | 67 +++++++++++++----------- pkg/tlsutil/cipher_suites_go1.15.go | 41 --------------- pkg/tlsutil/cipher_suites_go1.15_test.go | 52 ------------------ pkg/tlsutil/cipher_suites_test.go | 50 ++++++++++-------- tests/e2e/etcd_config_test.go | 38 ++++++++++++++ 7 files changed, 127 insertions(+), 167 deletions(-) delete mode 100644 pkg/tlsutil/cipher_suites_go1.15.go delete mode 100644 pkg/tlsutil/cipher_suites_go1.15_test.go diff --git a/embed/config.go b/embed/config.go index 247a4c1bd..7135274ec 100644 --- a/embed/config.go +++ b/embed/config.go @@ -566,13 +566,9 @@ func updateCipherSuites(tls *transport.TLSInfo, ss []string) error { return fmt.Errorf("TLSInfo.CipherSuites is already specified (given %v)", ss) } if len(ss) > 0 { - cs := make([]uint16, len(ss)) - for i, s := range ss { - var ok bool - cs[i], ok = tlsutil.GetCipherSuite(s) - if !ok { - return fmt.Errorf("unexpected TLS cipher suite %q", s) - } + cs, err := tlsutil.GetCipherSuites(ss) + if err != nil { + return err } tls.CipherSuites = cs } diff --git a/etcdmain/grpc_proxy.go b/etcdmain/grpc_proxy.go index 066700332..25b9e7649 100644 --- a/etcdmain/grpc_proxy.go +++ b/etcdmain/grpc_proxy.go @@ -38,6 +38,7 @@ import ( pb "go.etcd.io/etcd/etcdserver/etcdserverpb" "go.etcd.io/etcd/pkg/debugutil" "go.etcd.io/etcd/pkg/logutil" + "go.etcd.io/etcd/pkg/tlsutil" "go.etcd.io/etcd/pkg/transport" "go.etcd.io/etcd/proxy/grpcproxy" @@ -70,11 +71,12 @@ var ( // tls for clients connecting to proxy - grpcProxyListenCA string - grpcProxyListenCert string - grpcProxyListenKey string - grpcProxyListenAutoTLS bool - grpcProxyListenCRL string + grpcProxyListenCA string + grpcProxyListenCert string + grpcProxyListenKey string + grpcProxyListenCipherSuites []string + grpcProxyListenAutoTLS bool + grpcProxyListenCRL string grpcProxyAdvertiseClientURL string grpcProxyResolverPrefix string @@ -140,6 +142,7 @@ func newGRPCProxyStartCommand() *cobra.Command { cmd.Flags().StringVar(&grpcProxyListenCert, "cert-file", "", "identify secure connections to the proxy using this TLS certificate file") cmd.Flags().StringVar(&grpcProxyListenKey, "key-file", "", "identify secure connections to the proxy using this TLS key file") cmd.Flags().StringVar(&grpcProxyListenCA, "trusted-ca-file", "", "verify certificates of TLS-enabled secure proxy using this CA bundle") + cmd.Flags().StringSliceVar(&grpcProxyListenCipherSuites, "listen-cipher-suites", grpcProxyListenCipherSuites, "Comma-separated list of supported TLS cipher suites between client/proxy (empty will be auto-populated by Go).") cmd.Flags().BoolVar(&grpcProxyListenAutoTLS, "auto-tls", false, "proxy TLS using generated certificates") cmd.Flags().StringVar(&grpcProxyListenCRL, "client-crl-file", "", "proxy client certificate revocation list file.") @@ -176,20 +179,27 @@ func startGRPCProxy(cmd *cobra.Command, args []string) { } grpclog.SetLoggerV2(gl) - tlsinfo := newTLS(grpcProxyListenCA, grpcProxyListenCert, grpcProxyListenKey) - if tlsinfo == nil && grpcProxyListenAutoTLS { + tlsInfo := newTLS(grpcProxyListenCA, grpcProxyListenCert, grpcProxyListenKey) + if len(grpcProxyListenCipherSuites) > 0 { + cs, err := tlsutil.GetCipherSuites(grpcProxyListenCipherSuites) + if err != nil { + log.Fatal(err) + } + tlsInfo.CipherSuites = cs + } + if tlsInfo == nil && grpcProxyListenAutoTLS { host := []string{"https://" + grpcProxyListenAddr} dir := filepath.Join(grpcProxyDataDir, "fixtures", "proxy") autoTLS, err := transport.SelfCert(lg, dir, host) if err != nil { log.Fatal(err) } - tlsinfo = &autoTLS + tlsInfo = &autoTLS } - if tlsinfo != nil { - lg.Info("gRPC proxy server TLS", zap.String("tls-info", fmt.Sprintf("%+v", tlsinfo))) + if tlsInfo != nil { + lg.Info("gRPC proxy server TLS", zap.String("tls-info", fmt.Sprintf("%+v", tlsInfo))) } - m := mustListenCMux(lg, tlsinfo) + m := mustListenCMux(lg, tlsInfo) grpcl := m.Match(cmux.HTTP2()) defer func() { grpcl.Close() @@ -199,7 +209,7 @@ func startGRPCProxy(cmd *cobra.Command, args []string) { client := mustNewClient(lg) httpClient := mustNewHTTPClient(lg) - srvhttp, httpl := mustHTTPListener(lg, m, tlsinfo, client) + srvhttp, httpl := mustHTTPListener(lg, m, tlsInfo, client) if err := http2.ConfigureServer(srvhttp, &http2.Server{ MaxConcurrentStreams: maxConcurrentStreams, @@ -212,7 +222,7 @@ func startGRPCProxy(cmd *cobra.Command, args []string) { go func() { errc <- srvhttp.Serve(httpl) }() go func() { errc <- m.Serve() }() if len(grpcProxyMetricsListenAddr) > 0 { - mhttpl := mustMetricsListener(lg, tlsinfo) + mhttpl := mustMetricsListener(lg, tlsInfo) go func() { mux := http.NewServeMux() grpcproxy.HandleMetrics(mux, httpClient, client.Endpoints()) diff --git a/pkg/tlsutil/cipher_suites.go b/pkg/tlsutil/cipher_suites.go index eac2497c9..d1f1bab94 100644 --- a/pkg/tlsutil/cipher_suites.go +++ b/pkg/tlsutil/cipher_suites.go @@ -1,5 +1,3 @@ -// +build !go1.15 - // Copyright 2018 The etcd Authors // // Licensed under the Apache License, Version 2.0 (the "License"); @@ -16,38 +14,43 @@ package tlsutil -import "crypto/tls" - -// cipher suites implemented by Go -// https://github.com/golang/go/blob/dev.boringcrypto.go1.10/src/crypto/tls/cipher_suites.go -var cipherSuites = map[string]uint16{ - "TLS_RSA_WITH_RC4_128_SHA": tls.TLS_RSA_WITH_RC4_128_SHA, - "TLS_RSA_WITH_3DES_EDE_CBC_SHA": tls.TLS_RSA_WITH_3DES_EDE_CBC_SHA, - "TLS_RSA_WITH_AES_128_CBC_SHA": tls.TLS_RSA_WITH_AES_128_CBC_SHA, - "TLS_RSA_WITH_AES_256_CBC_SHA": tls.TLS_RSA_WITH_AES_256_CBC_SHA, - "TLS_RSA_WITH_AES_128_CBC_SHA256": tls.TLS_RSA_WITH_AES_128_CBC_SHA256, - "TLS_RSA_WITH_AES_128_GCM_SHA256": tls.TLS_RSA_WITH_AES_128_GCM_SHA256, - "TLS_RSA_WITH_AES_256_GCM_SHA384": tls.TLS_RSA_WITH_AES_256_GCM_SHA384, - "TLS_ECDHE_ECDSA_WITH_RC4_128_SHA": tls.TLS_ECDHE_ECDSA_WITH_RC4_128_SHA, - "TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA": tls.TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA, - "TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA": tls.TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA, - "TLS_ECDHE_RSA_WITH_RC4_128_SHA": tls.TLS_ECDHE_RSA_WITH_RC4_128_SHA, - "TLS_ECDHE_RSA_WITH_3DES_EDE_CBC_SHA": tls.TLS_ECDHE_RSA_WITH_3DES_EDE_CBC_SHA, - "TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA": tls.TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA, - "TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA": tls.TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA, - "TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA256": tls.TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA256, - "TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA256": tls.TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA256, - "TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256": tls.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256, - "TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256": tls.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256, - "TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384": tls.TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384, - "TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384": tls.TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384, - "TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305": tls.TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305, - "TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305": tls.TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305, -} +import ( + "crypto/tls" + "fmt" +) // GetCipherSuite returns the corresponding cipher suite, // and boolean value if it is supported. func GetCipherSuite(s string) (uint16, bool) { - v, ok := cipherSuites[s] - return v, ok + for _, c := range tls.CipherSuites() { + if s == c.Name { + return c.ID, true + } + } + for _, c := range tls.InsecureCipherSuites() { + if s == c.Name { + return c.ID, true + } + } + switch s { + case "TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305": + return tls.TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256, true + case "TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305": + return tls.TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256, true + } + return 0, false +} + +// GetCipherSuites returns a list of corresponding cipher suite IDs. +func GetCipherSuites(ss []string) ([]uint16, error) { + cs := make([]uint16, len(ss)) + for i, s := range ss { + var ok bool + cs[i], ok = GetCipherSuite(s) + if !ok { + return nil, fmt.Errorf("unexpected TLS cipher suite %q", s) + } + } + + return cs, nil } diff --git a/pkg/tlsutil/cipher_suites_go1.15.go b/pkg/tlsutil/cipher_suites_go1.15.go deleted file mode 100644 index de2044214..000000000 --- a/pkg/tlsutil/cipher_suites_go1.15.go +++ /dev/null @@ -1,41 +0,0 @@ -// +build go1.15 - -// Copyright 2018 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" - -// GetCipherSuite returns the corresponding cipher suite, -// and boolean value if it is supported. -func GetCipherSuite(s string) (uint16, bool) { - for _, c := range tls.CipherSuites() { - if s == c.Name { - return c.ID, true - } - } - for _, c := range tls.InsecureCipherSuites() { - if s == c.Name { - return c.ID, true - } - } - switch s { - case "TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305": - return tls.TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256, true - case "TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305": - return tls.TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256, true - } - return 0, false -} diff --git a/pkg/tlsutil/cipher_suites_go1.15_test.go b/pkg/tlsutil/cipher_suites_go1.15_test.go deleted file mode 100644 index c00eef373..000000000 --- a/pkg/tlsutil/cipher_suites_go1.15_test.go +++ /dev/null @@ -1,52 +0,0 @@ -// +build go1.15 - -// Copyright 2018 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 TestGetCipherSuite_not_existing(t *testing.T) { - _, ok := GetCipherSuite("not_existing") - if ok { - t.Fatal("Expected not ok") - } -} - -func CipherSuiteExpectedToExist(tb testing.TB, cipher string, expectedId uint16) { - vid, ok := GetCipherSuite(cipher) - if !ok { - tb.Errorf("Expected %v cipher to exist", cipher) - } - if vid != expectedId { - tb.Errorf("For %v expected=%v found=%v", cipher, expectedId, vid) - } -} - -func TestGetCipherSuite_success(t *testing.T) { - CipherSuiteExpectedToExist(t, "TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA", tls.TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA) - CipherSuiteExpectedToExist(t, "TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256", tls.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256) - - // Explicit test for legacy names - CipherSuiteExpectedToExist(t, "TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305", tls.TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256) - CipherSuiteExpectedToExist(t, "TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305", tls.TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256) -} - -func TestGetCipherSuite_insecure(t *testing.T) { - CipherSuiteExpectedToExist(t, "TLS_ECDHE_RSA_WITH_RC4_128_SHA", tls.TLS_ECDHE_RSA_WITH_RC4_128_SHA) -} diff --git a/pkg/tlsutil/cipher_suites_test.go b/pkg/tlsutil/cipher_suites_test.go index 4b60cff05..a17b46c2f 100644 --- a/pkg/tlsutil/cipher_suites_test.go +++ b/pkg/tlsutil/cipher_suites_test.go @@ -1,5 +1,3 @@ -// +build !go1.15 - // Copyright 2018 The etcd Authors // // Licensed under the Apache License, Version 2.0 (the "License"); @@ -17,28 +15,36 @@ package tlsutil import ( - "go/importer" - "reflect" - "strings" + "crypto/tls" "testing" ) -func TestGetCipherSuites(t *testing.T) { - pkg, err := importer.For("source", nil).Import("crypto/tls") - if err != nil { - t.Fatal(err) - } - cm := make(map[string]uint16) - for _, s := range pkg.Scope().Names() { - if strings.HasPrefix(s, "TLS_RSA_") || strings.HasPrefix(s, "TLS_ECDHE_") { - v, ok := GetCipherSuite(s) - if !ok { - t.Fatalf("Go implements missing cipher suite %q (%v)", s, v) - } - cm[s] = v - } - } - if !reflect.DeepEqual(cm, cipherSuites) { - t.Fatalf("found unmatched cipher suites %v (Go) != %v", cm, cipherSuites) +func TestGetCipherSuite_not_existing(t *testing.T) { + _, ok := GetCipherSuite("not_existing") + if ok { + t.Fatal("Expected not ok") } } + +func CipherSuiteExpectedToExist(tb testing.TB, cipher string, expectedId uint16) { + vid, ok := GetCipherSuite(cipher) + if !ok { + tb.Errorf("Expected %v cipher to exist", cipher) + } + if vid != expectedId { + tb.Errorf("For %v expected=%v found=%v", cipher, expectedId, vid) + } +} + +func TestGetCipherSuite_success(t *testing.T) { + CipherSuiteExpectedToExist(t, "TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA", tls.TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA) + CipherSuiteExpectedToExist(t, "TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256", tls.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256) + + // Explicit test for legacy names + CipherSuiteExpectedToExist(t, "TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305", tls.TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256) + CipherSuiteExpectedToExist(t, "TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305", tls.TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256) +} + +func TestGetCipherSuite_insecure(t *testing.T) { + CipherSuiteExpectedToExist(t, "TLS_ECDHE_RSA_WITH_RC4_128_SHA", tls.TLS_ECDHE_RSA_WITH_RC4_128_SHA) +} diff --git a/tests/e2e/etcd_config_test.go b/tests/e2e/etcd_config_test.go index 8688cfc83..1d57e6067 100644 --- a/tests/e2e/etcd_config_test.go +++ b/tests/e2e/etcd_config_test.go @@ -298,3 +298,41 @@ func TestGrpcproxyAndCommonName(t *testing.T) { } p.Stop() } + +func TestGrpcproxyAndListenCipherSuite(t *testing.T) { + cases := []struct { + name string + args []string + }{ + { + name: "ArgsWithCipherSuites", + args: []string{ + binDir + "/etcd", + "grpc-proxy", + "start", + "--listen-cipher-suites", "TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256,TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256,TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384,TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384,TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256,TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256", + }, + }, + { + name: "ArgsWithoutCipherSuites", + args: []string{ + binDir + "/etcd", + "grpc-proxy", + "start", + "--listen-cipher-suites", "", + }, + }, + } + + for _, test := range cases { + t.Run(test.name, func(t *testing.T) { + pw, err := spawnCmd(test.args) + if err != nil { + t.Fatal(err) + } + if err = pw.Stop(); err != nil { + t.Fatal(err) + } + }) + } +}