diff --git a/server/etcdmain/grpc_proxy.go b/server/etcdmain/grpc_proxy.go index 8622605b0af..db672fe56d4 100644 --- a/server/etcdmain/grpc_proxy.go +++ b/server/etcdmain/grpc_proxy.go @@ -78,13 +78,16 @@ var ( // tls for clients connecting to proxy - grpcProxyListenCA string - grpcProxyListenCert string - grpcProxyListenKey string - grpcProxyListenCipherSuites []string - grpcProxyListenAutoTLS bool - grpcProxyListenCRL string - selfSignedCertValidity uint + grpcProxyListenCA string + grpcProxyListenCert string + grpcProxyListenKey string + grpcProxyListenCipherSuites []string + grpcProxyListenAutoTLS bool + grpcProxyListenCRL string + grpcProxyListenTLSMinVersion string + grpcProxyListenTLSMaxVersion string + + selfSignedCertValidity uint grpcProxyAdvertiseClientURL string grpcProxyResolverPrefix string @@ -166,6 +169,8 @@ func newGRPCProxyStartCommand() *cobra.Command { 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.") cmd.Flags().UintVar(&selfSignedCertValidity, "self-signed-cert-validity", 1, "The validity period of the proxy certificates, unit is year") + cmd.Flags().StringVar(&grpcProxyListenTLSMinVersion, "tls-min-version", string(tlsutil.TLSVersion12), "Minimum TLS version supported by grpc proxy. Possible values: TLS1.2, TLS1.3.") + cmd.Flags().StringVar(&grpcProxyListenTLSMaxVersion, "tls-max-version", string(tlsutil.TLSVersionDefault), "Maximum TLS version supported by grpc proxy. Possible values: TLS1.2, TLS1.3 (empty defers to Go).") // experimental flags cmd.Flags().BoolVar(&grpcProxyEnableOrdering, "experimental-serializable-ordering", false, "Ensure serializable reads have monotonically increasing store revisions across endpoints.") @@ -197,13 +202,6 @@ func startGRPCProxy(cmd *cobra.Command, args []string) { // The empty CN is required for grpcProxyCert. // Please see https://github.com/etcd-io/etcd/issues/11970#issuecomment-687875315 for more context. tlsInfo := newTLS(grpcProxyListenCA, grpcProxyListenCert, grpcProxyListenKey, false) - 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") @@ -213,10 +211,32 @@ func startGRPCProxy(cmd *cobra.Command, args []string) { } tlsInfo = &autoTLS } - if tlsInfo != nil { + if len(grpcProxyListenCipherSuites) > 0 { + cs, err := tlsutil.GetCipherSuites(grpcProxyListenCipherSuites) + if err != nil { + log.Fatal(err) + } + tlsInfo.CipherSuites = cs + } + if grpcProxyListenTLSMinVersion != "" { + version, err := tlsutil.GetTLSVersion(grpcProxyListenTLSMinVersion) + if err != nil { + log.Fatal(err) + } + tlsInfo.MinVersion = version + } + if grpcProxyListenTLSMaxVersion != "" { + version, err := tlsutil.GetTLSVersion(grpcProxyListenTLSMaxVersion) + if err != nil { + log.Fatal(err) + } + tlsInfo.MaxVersion = version + } + lg.Info("gRPC proxy server TLS", zap.String("tls-info", fmt.Sprintf("%+v", tlsInfo))) } + m := mustListenCMux(lg, tlsInfo) grpcl := m.Match(cmux.HTTP2()) defer func() { @@ -290,6 +310,29 @@ func checkArgs() { fmt.Fprintln(os.Stderr, fmt.Errorf("selfSignedCertValidity is invalid,it should be greater than 0")) os.Exit(1) } + + minVersion, err := tlsutil.GetTLSVersion(grpcProxyListenTLSMinVersion) + if err != nil { + fmt.Fprintln(os.Stderr, fmt.Errorf("tls-min-version is invalid: %w", err)) + os.Exit(1) + } + maxVersion, err := tlsutil.GetTLSVersion(grpcProxyListenTLSMaxVersion) + if err != nil { + fmt.Fprintln(os.Stderr, fmt.Errorf("tls-max-version is invalid: %w", err)) + os.Exit(1) + } + + // maxVersion == 0 means that Go selects the highest available version. + if maxVersion != 0 && minVersion > maxVersion { + fmt.Fprintln(os.Stderr, fmt.Errorf("min version (%s) is greater than max version (%s)", grpcProxyListenTLSMinVersion, grpcProxyListenTLSMaxVersion)) + os.Exit(1) + } + + // Check if user attempted to configure ciphers for TLS1.3 only: Go does not support that currently. + if minVersion == tls.VersionTLS13 && len(grpcProxyListenCipherSuites) > 0 { + fmt.Fprintln(os.Stderr, fmt.Errorf("cipher suites cannot be configured when only TLS1.3 is enabled")) + os.Exit(1) + } } func mustNewClient(lg *zap.Logger) *clientv3.Client { diff --git a/tests/e2e/etcd_grpcproxy_test.go b/tests/e2e/etcd_grpcproxy_test.go index 70c5fff5ffe..84fafba03bc 100644 --- a/tests/e2e/etcd_grpcproxy_test.go +++ b/tests/e2e/etcd_grpcproxy_test.go @@ -23,6 +23,7 @@ import ( "testing" "time" + "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "go.etcd.io/etcd/api/v3/etcdserverpb" "go.etcd.io/etcd/api/v3/v3rpc/rpctypes" @@ -126,6 +127,38 @@ func TestGrpcProxyAutoSync(t *testing.T) { require.NoError(t, proxyProc.Stop()) } +func TestGrpcProxyTlsVersions(t *testing.T) { + e2e.SkipInShortMode(t) + + epc, err := e2e.NewEtcdProcessCluster(t, e2e.NewConfigClientBoth()) + require.NoError(t, err) + defer func() { + assert.NoError(t, epc.Close()) + }() + + var ( + node1ClientURL = epc.Procs[0].Config().ClientHttpUrl + proxyClientURL = "127.0.0.1:42379" + ) + + // Run independent grpc-proxy instance + proxyProc, err := e2e.SpawnCmd([]string{e2e.BinDir + "/etcd", "grpc-proxy", "start", + "--advertise-client-url", proxyClientURL, + "--listen-addr", proxyClientURL, + "--endpoints", node1ClientURL, + "--endpoints-auto-sync-interval", "1s", + "--cert-file", e2e.CertPath2, + "--key-file", e2e.PrivateKeyPath2, + "--tls-min-version", "TLS1.2", + "--tls-max-version", "TLS1.3", + }, nil) + require.NoError(t, err) + defer proxyProc.Stop() + + _, err = proxyProc.Expect("listening for gRPC proxy client requests") + require.NoError(t, err) +} + func runEtcdNode(name, dataDir, clientURL, peerURL, clusterState, initialCluster string) (*expect.ExpectProcess, error) { proc, err := e2e.SpawnCmd([]string{e2e.BinPath, "--name", name,