You are viewing a plain text version of this content. The canonical link for it is here.
Posted to notifications@skywalking.apache.org by ha...@apache.org on 2023/10/20 11:48:21 UTC

[skywalking-banyandb] branch main updated: Support etcd client authentication (#331)

This is an automated email from the ASF dual-hosted git repository.

hanahmily pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/skywalking-banyandb.git


The following commit(s) were added to refs/heads/main by this push:
     new d722db6b Support etcd client authentication (#331)
d722db6b is described below

commit d722db6bcd52554ef429961600d617d28d561ae5
Author: hailin0 <wa...@apache.org>
AuthorDate: Fri Oct 20 19:48:16 2023 +0800

    Support etcd client authentication (#331)
    
    * Support etcd client authentication
    
    * Set authentication parameters from startup command
    
    ---------
    
    Co-authored-by: Gao Hongtao <ha...@gmail.com>
---
 CHANGES.md                                         |   6 +
 banyand/metadata/client.go                         |  23 ++
 banyand/metadata/schema/etcd.go                    |  53 ++++
 docs/installation/cluster.md                       |  44 +++
 go.mod                                             |   2 +-
 test/integration/etcd/client_suite_test.go         |  31 +++
 test/integration/etcd/client_test.go               | 302 +++++++++++++++++++++
 test/integration/etcd/testdata/ca.crt              |  22 ++
 .../etcd/testdata/client-clientusage.crt           |  24 ++
 .../etcd/testdata/client-clientusage.key.insecure  |  27 ++
 .../etcd/testdata/server-serverusage.crt           |  24 ++
 .../etcd/testdata/server-serverusage.key.insecure  |  27 ++
 test/integration/etcd/testdata/server.crt          |  24 ++
 test/integration/etcd/testdata/server.key.insecure |  27 ++
 14 files changed, 635 insertions(+), 1 deletion(-)

diff --git a/CHANGES.md b/CHANGES.md
index fd2e3767..9e740579 100644
--- a/CHANGES.md
+++ b/CHANGES.md
@@ -2,6 +2,12 @@
 
 Release Notes.
 
+## 0.6.0
+
+### Features
+
+- Support etcd client authentication.
+
 ## 0.5.0
 
 ### Features
diff --git a/banyand/metadata/client.go b/banyand/metadata/client.go
index 411b3c03..a687a3e2 100644
--- a/banyand/metadata/client.go
+++ b/banyand/metadata/client.go
@@ -39,6 +39,16 @@ const (
 	flagEtcdEndpointsName = "etcd-endpoints"
 )
 
+const flagEtcdUsername = "etcd-username"
+
+const flagEtcdPassword = "etcd-password"
+
+const flagEtcdTLSCAFile = "etcd-tls-ca-file"
+
+const flagEtcdTLSCertFile = "etcd-tls-cert-file"
+
+const flagEtcdTLSKeyFile = "etcd-tls-key-file"
+
 // NewClient returns a new metadata client.
 func NewClient(forceRegisterNode bool) (Service, error) {
 	return &clientService{
@@ -51,6 +61,11 @@ type clientService struct {
 	schemaRegistry    schema.Registry
 	closer            *run.Closer
 	namespace         string
+	etcdUsername      string
+	etcdPassword      string
+	etcdTLSCAFile     string
+	etcdTLSCertFile   string
+	etcdTLSKeyFile    string
 	endpoints         []string
 	forceRegisterNode bool
 }
@@ -63,6 +78,11 @@ func (s *clientService) FlagSet() *run.FlagSet {
 	fs := run.NewFlagSet("metadata")
 	fs.StringVar(&s.namespace, "namespace", DefaultNamespace, "The namespace of the metadata stored in etcd")
 	fs.StringArrayVar(&s.endpoints, flagEtcdEndpointsName, []string{"http://localhost:2379"}, "A comma-delimited list of etcd endpoints")
+	fs.StringVar(&s.etcdUsername, flagEtcdUsername, "", "A username of etcd")
+	fs.StringVar(&s.etcdPassword, flagEtcdPassword, "", "A password of etcd user")
+	fs.StringVar(&s.etcdTLSCAFile, flagEtcdTLSCAFile, "", "Trusted certificate authority")
+	fs.StringVar(&s.etcdTLSCertFile, flagEtcdTLSCertFile, "", "Etcd client certificate")
+	fs.StringVar(&s.etcdTLSKeyFile, flagEtcdTLSKeyFile, "", "Private key for the etcd client certificate.")
 	return fs
 }
 
@@ -78,6 +98,9 @@ func (s *clientService) PreRun(ctx context.Context) error {
 	s.schemaRegistry, err = schema.NewEtcdSchemaRegistry(
 		schema.Namespace(s.namespace),
 		schema.ConfigureServerEndpoints(s.endpoints),
+		schema.ConfigureEtcdUser(s.etcdUsername, s.etcdPassword),
+		schema.ConfigureEtcdTLSCAFile(s.etcdTLSCAFile),
+		schema.ConfigureEtcdTLSCertAndKey(s.etcdTLSCertFile, s.etcdTLSKeyFile),
 	)
 	if err != nil {
 		return err
diff --git a/banyand/metadata/schema/etcd.go b/banyand/metadata/schema/etcd.go
index 3abb630a..213f02a5 100644
--- a/banyand/metadata/schema/etcd.go
+++ b/banyand/metadata/schema/etcd.go
@@ -19,12 +19,14 @@ package schema
 
 import (
 	"context"
+	"crypto/tls"
 	"fmt"
 	"path"
 	"sync"
 	"time"
 
 	"github.com/pkg/errors"
+	"go.etcd.io/etcd/client/pkg/v3/transport"
 	clientv3 "go.etcd.io/etcd/client/v3"
 	"go.uber.org/zap"
 	"google.golang.org/protobuf/proto"
@@ -69,6 +71,33 @@ func ConfigureServerEndpoints(url []string) RegistryOption {
 	}
 }
 
+// ConfigureEtcdUser sets a username & password of the etcd.
+func ConfigureEtcdUser(username string, password string) RegistryOption {
+	return func(config *etcdSchemaRegistryConfig) {
+		if username != "" && password != "" {
+			config.username = username
+			config.password = password
+		}
+	}
+}
+
+// ConfigureEtcdTLSCAFile sets a trusted ca file of the etcd tls config.
+func ConfigureEtcdTLSCAFile(file string) RegistryOption {
+	return func(config *etcdSchemaRegistryConfig) {
+		config.tlsCAFile = file
+	}
+}
+
+// ConfigureEtcdTLSCertAndKey sets a cert & key of the etcd tls config.
+func ConfigureEtcdTLSCertAndKey(certFile string, keyFile string) RegistryOption {
+	return func(config *etcdSchemaRegistryConfig) {
+		if certFile != "" && keyFile != "" {
+			config.tlsCertFile = certFile
+			config.tlsKeyFile = keyFile
+		}
+	}
+}
+
 type etcdSchemaRegistry struct {
 	namespace string
 	client    *clientv3.Client
@@ -80,6 +109,11 @@ type etcdSchemaRegistry struct {
 
 type etcdSchemaRegistryConfig struct {
 	namespace       string
+	username        string
+	password        string
+	tlsCAFile       string
+	tlsCertFile     string
+	tlsKeyFile      string
 	serverEndpoints []string
 }
 
@@ -135,6 +169,9 @@ func NewEtcdSchemaRegistry(options ...RegistryOption) (Registry, error) {
 		DialKeepAliveTimeout: 10 * time.Second,
 		AutoSyncInterval:     5 * time.Minute,
 		Logger:               l,
+		Username:             registryConfig.username,
+		Password:             registryConfig.password,
+		TLS:                  extractTLSConfig(registryConfig),
 	}
 	client, err := clientv3.New(config)
 	if err != nil {
@@ -399,3 +436,19 @@ func formatKey(entityPrefix string, metadata *commonv1.Metadata) string {
 		listPrefixesForEntity(metadata.GetGroup(), entityPrefix),
 		metadata.GetName())
 }
+
+func extractTLSConfig(cfg *etcdSchemaRegistryConfig) *tls.Config {
+	if cfg.tlsCAFile == "" && cfg.tlsCertFile == "" && cfg.tlsKeyFile == "" {
+		return nil
+	}
+	tlsInfo := transport.TLSInfo{
+		TrustedCAFile: cfg.tlsCAFile,
+		CertFile:      cfg.tlsCertFile,
+		KeyFile:       cfg.tlsKeyFile,
+	}
+	tlsConfig, err := tlsInfo.ClientConfig()
+	if err != nil {
+		return nil
+	}
+	return tlsConfig
+}
diff --git a/docs/installation/cluster.md b/docs/installation/cluster.md
index fce7dc5f..02f48c75 100644
--- a/docs/installation/cluster.md
+++ b/docs/installation/cluster.md
@@ -28,3 +28,47 @@ The host is registered to the etcd cluster by the `banyand-server` automatically
 - `node-host-provider=hostname` : Default. The OS's hostname is registered as the host part in the address.
 - `node-host-provider=ip` : The OS's the first non-loopback active IP address(IPv4) is registered as the host part in the address.
 - `node-host-provider=flag` : `node-host` is registered as the host part in the address.
+
+## Etcd Authentication
+
+`etcd` supports through tls certificates and RBAC-based authentication for both clients to server communication. This section tends to help users set up authentication for BanyanDB.
+
+### Authentication with username/password
+
+The etcd user can be setup by the [etcd authentication guide](https://etcd.io/docs/v3.5/op-guide/authentication/)
+
+The username/password is configured in the following command:
+
+- `etcd-username`: The username for etcd client authentication.
+- `etcd-password`: The password for etcd client authentication.
+
+***Note: recommended using environment variables to set username/password for higher security.***
+
+```shell
+$ ./banyand-server storage --etcd-endpoints=your-endpoints --etcd-username=your-username --etcd-password=your-password <flags>
+$ ./banyand-server liaison --etcd-endpoints=your-endpoints --etcd-username=your-username --etcd-password=your-password <flags>
+```
+
+### Transport security with HTTPS
+
+The etcd trusted certificate file can be setup by the [etcd transport security model](https://etcd.io/docs/v3.5/op-guide/security/#example-1-client-to-server-transport-security-with-https)
+
+- `etcd-tls-ca-file`: The path of the trusted certificate file.
+
+```shell
+$ ./banyand-server storage --etcd-endpoints=your-https-endpoints --etcd-tls-ca-file=youf-file-path <flags>
+$ ./banyand-server liaison --etcd-endpoints=your-https-endpoints --etcd-tls-ca-file=youf-file-path <flags>
+```
+
+### Authentication with HTTPS client certificates
+
+The etcd client certificates can be setup by the [etcd transport security model](https://etcd.io/docs/v3.5/op-guide/security/#example-2-client-to-server-authentication-with-https-client-certificates)
+
+- `etcd-tls-ca-file`: The path of the trusted certificate file.
+- `etcd-tls-cert-file`: Certificate used for SSL/TLS connections to etcd. When this option is set, advertise-client-urls can use the HTTPS schema.
+- `etcd-tls-key-file`: Key for the certificate. Must be unencrypted.
+
+```shell
+$ ./banyand-server storage --etcd-endpoints=your-https-endpoints --etcd-tls-ca-file=youf-file-path --etcd-tls-cert-file=youf-file-path --etcd-tls-key-file=youf-file-path <flags>
+$ ./banyand-server liaison --etcd-endpoints=your-https-endpoints --etcd-tls-ca-file=youf-file-path --etcd-tls-cert-file=youf-file-path --etcd-tls-key-file=youf-file-path <flags>
+```
\ No newline at end of file
diff --git a/go.mod b/go.mod
index 10bd2daa..1dcf2b57 100644
--- a/go.mod
+++ b/go.mod
@@ -122,7 +122,7 @@ require (
 	github.com/yusufpapurcu/wmi v1.2.2 // indirect
 	go.etcd.io/bbolt v1.3.7 // indirect
 	go.etcd.io/etcd/api/v3 v3.5.9
-	go.etcd.io/etcd/client/pkg/v3 v3.5.9 // indirect
+	go.etcd.io/etcd/client/pkg/v3 v3.5.9
 	go.etcd.io/etcd/client/v2 v2.305.9 // indirect
 	go.etcd.io/etcd/pkg/v3 v3.5.9 // indirect
 	go.etcd.io/etcd/raft/v3 v3.5.9 // indirect
diff --git a/test/integration/etcd/client_suite_test.go b/test/integration/etcd/client_suite_test.go
new file mode 100644
index 00000000..23da32bf
--- /dev/null
+++ b/test/integration/etcd/client_suite_test.go
@@ -0,0 +1,31 @@
+// Licensed to Apache Software Foundation (ASF) under one or more contributor
+// license agreements. See the NOTICE file distributed with
+// this work for additional information regarding copyright
+// ownership. Apache Software Foundation (ASF) licenses this file to you 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 integration_setup_test is a integration test suite.
+package integration_etcd_test
+
+import (
+	"testing"
+
+	. "github.com/onsi/ginkgo/v2"
+	. "github.com/onsi/gomega"
+)
+
+func TestSetup(t *testing.T) {
+	RegisterFailHandler(Fail)
+	RunSpecs(t, "Client Suite")
+}
diff --git a/test/integration/etcd/client_test.go b/test/integration/etcd/client_test.go
new file mode 100644
index 00000000..a78d8345
--- /dev/null
+++ b/test/integration/etcd/client_test.go
@@ -0,0 +1,302 @@
+// Licensed to Apache Software Foundation (ASF) under one or more contributor
+// license agreements. See the NOTICE file distributed with
+// this work for additional information regarding copyright
+// ownership. Apache Software Foundation (ASF) licenses this file to you 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 integration_etcd_test
+
+import (
+	"context"
+	"crypto/tls"
+	"fmt"
+	"log"
+	"net/url"
+	"path/filepath"
+	"runtime"
+	"time"
+
+	. "github.com/onsi/ginkgo/v2"
+	. "github.com/onsi/gomega"
+	"github.com/onsi/gomega/gleak"
+	"go.etcd.io/etcd/client/pkg/v3/transport"
+	clientv3 "go.etcd.io/etcd/client/v3"
+	"go.etcd.io/etcd/server/v3/embed"
+
+	databasev1 "github.com/apache/skywalking-banyandb/api/proto/banyandb/database/v1"
+	"github.com/apache/skywalking-banyandb/banyand/metadata/schema"
+	"github.com/apache/skywalking-banyandb/pkg/test"
+	"github.com/apache/skywalking-banyandb/pkg/test/flags"
+	"github.com/apache/skywalking-banyandb/pkg/test/helpers"
+	"github.com/apache/skywalking-banyandb/pkg/test/setup"
+)
+
+const host = "127.0.0.1"
+
+const namespace = "liaison-test"
+
+const nodeHost = "liaison-1"
+
+var _ = Describe("Client Test", func() {
+	var (
+		dir                string
+		dirSpaceDef        func()
+		caFilePath         string
+		serverKeyFilePath  string
+		serverCertFilePath string
+		clientKeyFilePath  string
+		clientCertFilePath string
+		goods              []gleak.Goroutine
+	)
+	BeforeEach(func() {
+		var err error
+		dir, dirSpaceDef, err = test.NewSpace()
+		Expect(err).ShouldNot(HaveOccurred())
+		_, currentFile, _, _ := runtime.Caller(0)
+		basePath := filepath.Dir(currentFile)
+		caFilePath = filepath.Join(basePath, "testdata/ca.crt")
+		serverKeyFilePath = filepath.Join(basePath, "testdata/server-serverusage.key.insecure")
+		serverCertFilePath = filepath.Join(basePath, "testdata/server-serverusage.crt")
+		clientKeyFilePath = filepath.Join(basePath, "testdata/client-clientusage.key.insecure")
+		clientCertFilePath = filepath.Join(basePath, "testdata/client-clientusage.crt")
+
+		goods = gleak.Goroutines()
+	})
+	AfterEach(func() {
+		dirSpaceDef()
+		Eventually(gleak.Goroutines, flags.EventuallyTimeout).ShouldNot(gleak.HaveLeaked(goods))
+	})
+
+	It("should be using user/password connect etcd server successfully", func() {
+		serverTLSInfo := transport.TLSInfo{}
+		clientTLSInfo := transport.TLSInfo{}
+		clientConfig, err := clientTLSInfo.ClientConfig()
+		Expect(err).ShouldNot(HaveOccurred())
+		username := "banyandb"
+		password := "banyandb"
+
+		etcdServer, err := createEtcdServer(dir, serverTLSInfo)
+		Expect(err).ShouldNot(HaveOccurred())
+		// wait for e.Server to join the cluster
+		<-etcdServer.Server.ReadyNotify()
+		etcdEndpoint := etcdServer.Config().ListenClientUrls[0].String()
+		defer etcdServer.Close()
+
+		adminClient, err := clientv3.New(clientv3.Config{
+			Endpoints: []string{etcdEndpoint},
+		})
+		Expect(err).ShouldNot(HaveOccurred())
+		defer adminClient.Close()
+		adminClient.RoleAdd(context.Background(), "root")
+		adminClient.UserAdd(context.Background(), "root", "root")
+		adminClient.UserGrantRole(context.Background(), "root", "root")
+		adminClient.UserAdd(context.Background(), username, password)
+		adminClient.UserGrantRole(context.Background(), username, "root")
+		adminClient.AuthEnable(context.Background())
+
+		ports, err := test.AllocateFreePorts(2)
+		Expect(err).NotTo(HaveOccurred())
+		addr := fmt.Sprintf("%s:%d", host, ports[0])
+		httpAddr := fmt.Sprintf("%s:%d", host, ports[1])
+		closeFn := setup.CMD("liaison",
+			"--namespace", namespace,
+			"--grpc-host="+host,
+			fmt.Sprintf("--grpc-port=%d", ports[0]),
+			"--http-host="+host,
+			fmt.Sprintf("--http-port=%d", ports[1]),
+			"--http-grpc-addr="+addr,
+			"--node-host-provider", "flag",
+			"--node-host", nodeHost,
+			"--etcd-endpoints", etcdEndpoint,
+			"--etcd-username", username,
+			"--etcd-password", password)
+		defer closeFn()
+
+		Eventually(helpers.HTTPHealthCheck(httpAddr), flags.EventuallyTimeout).Should(Succeed())
+		Eventually(func() (map[string]*databasev1.Node, error) {
+			return listKeys(etcdEndpoint, username, password, clientConfig, fmt.Sprintf("/%s/nodes/%s:%d", namespace, nodeHost, ports[0]))
+		}, flags.EventuallyTimeout).Should(HaveLen(1))
+	})
+
+	It("should be using cacert connect etcd server successfully", func() {
+		serverTLSInfo := transport.TLSInfo{
+			KeyFile:  serverKeyFilePath,
+			CertFile: serverCertFilePath,
+		}
+		clientTLSInfo := transport.TLSInfo{
+			TrustedCAFile: caFilePath,
+		}
+		clientConfig, err := clientTLSInfo.ClientConfig()
+		Expect(err).ShouldNot(HaveOccurred())
+
+		etcdServer, err := createEtcdServer(dir, serverTLSInfo)
+		Expect(err).ShouldNot(HaveOccurred())
+		// wait for e.Server to join the cluster
+		<-etcdServer.Server.ReadyNotify()
+		etcdEndpoint := etcdServer.Config().ListenClientUrls[0].String()
+		defer etcdServer.Close()
+
+		ports, err := test.AllocateFreePorts(2)
+		Expect(err).NotTo(HaveOccurred())
+		addr := fmt.Sprintf("%s:%d", host, ports[0])
+		httpAddr := fmt.Sprintf("%s:%d", host, ports[1])
+		closeFn := setup.CMD("liaison",
+			"--namespace", namespace,
+			"--grpc-host="+host,
+			fmt.Sprintf("--grpc-port=%d", ports[0]),
+			"--http-host="+host,
+			fmt.Sprintf("--http-port=%d", ports[1]),
+			"--http-grpc-addr="+addr,
+			"--node-host-provider", "flag",
+			"--node-host", nodeHost,
+			"--etcd-endpoints", etcdEndpoint,
+			"--etcd-tls-ca-file", caFilePath)
+		defer closeFn()
+
+		Eventually(helpers.HTTPHealthCheck(httpAddr), flags.EventuallyTimeout).Should(Succeed())
+		Eventually(func() (map[string]*databasev1.Node, error) {
+			return listKeys(etcdEndpoint, "", "", clientConfig, fmt.Sprintf("/%s/nodes/%s:%d", namespace, nodeHost, ports[0]))
+		}, flags.EventuallyTimeout).Should(HaveLen(1))
+	})
+
+	It("should be using key pair connect etcd server successfully", func() {
+		serverTLSInfo := transport.TLSInfo{
+			KeyFile:        serverKeyFilePath,
+			CertFile:       serverCertFilePath,
+			TrustedCAFile:  caFilePath,
+			ClientCertAuth: true,
+		}
+		clientTLSInfo := transport.TLSInfo{
+			KeyFile:       clientKeyFilePath,
+			CertFile:      clientCertFilePath,
+			TrustedCAFile: caFilePath,
+		}
+		clientConfig, err := clientTLSInfo.ClientConfig()
+		Expect(err).ShouldNot(HaveOccurred())
+
+		etcdServer, err := createEtcdServer(dir, serverTLSInfo)
+		Expect(err).ShouldNot(HaveOccurred())
+		// wait for e.Server to join the cluster
+		<-etcdServer.Server.ReadyNotify()
+		etcdEndpoint := etcdServer.Config().ListenClientUrls[0].String()
+		defer etcdServer.Close()
+
+		ports, err := test.AllocateFreePorts(2)
+		Expect(err).NotTo(HaveOccurred())
+		addr := fmt.Sprintf("%s:%d", host, ports[0])
+		httpAddr := fmt.Sprintf("%s:%d", host, ports[1])
+		closeFn := setup.CMD("liaison",
+			"--namespace", namespace,
+			"--grpc-host="+host,
+			fmt.Sprintf("--grpc-port=%d", ports[0]),
+			"--http-host="+host,
+			fmt.Sprintf("--http-port=%d", ports[1]),
+			"--http-grpc-addr="+addr,
+			"--node-host-provider", "flag",
+			"--node-host", nodeHost,
+			"--etcd-endpoints", etcdEndpoint,
+			"--etcd-tls-ca-file", caFilePath,
+			"--etcd-tls-cert-file", clientCertFilePath,
+			"--etcd-tls-key-file", clientKeyFilePath)
+		defer closeFn()
+
+		Eventually(helpers.HTTPHealthCheck(httpAddr), flags.EventuallyTimeout).Should(Succeed())
+		Eventually(func() (map[string]*databasev1.Node, error) {
+			return listKeys(etcdEndpoint, "", "", clientConfig, fmt.Sprintf("/%s/nodes/%s:%d", namespace, nodeHost, ports[0]))
+		}, flags.EventuallyTimeout).Should(HaveLen(1))
+	})
+})
+
+func parseURLs(urls []string) ([]url.URL, error) {
+	uu := make([]url.URL, 0, len(urls))
+	for _, u := range urls {
+		cURL, err := url.Parse(u)
+		if err != nil {
+			return nil, err
+		}
+		uu = append(uu, *cURL)
+	}
+	return uu, nil
+}
+
+func createEtcdServer(dir string, tlsInfo transport.TLSInfo) (e *embed.Etcd, err error) {
+	ports, err := test.AllocateFreePorts(2)
+	if err != nil {
+		return nil, err
+	}
+	enableTLS := tlsInfo.TrustedCAFile != "" || tlsInfo.CertFile != "" || tlsInfo.KeyFile != ""
+	httpProtocol := "http"
+	if enableTLS {
+		httpProtocol = "https"
+	}
+	clientURL := fmt.Sprintf("%s://%s:%d", httpProtocol, host, ports[0])
+	peerURL := fmt.Sprintf("%s://%s:%d", httpProtocol, host, ports[1])
+	cURLs, err := parseURLs([]string{clientURL})
+	if err != nil {
+		return nil, err
+	}
+	pURLs, err := parseURLs([]string{peerURL})
+	if err != nil {
+		return nil, err
+	}
+
+	etcdConfig := embed.NewConfig()
+	etcdConfig.Name = "test-etcd-certificate"
+	etcdConfig.Dir = dir
+	etcdConfig.ListenClientUrls, etcdConfig.AdvertiseClientUrls = cURLs, cURLs
+	etcdConfig.ListenPeerUrls, etcdConfig.AdvertisePeerUrls = pURLs, pURLs
+	etcdConfig.InitialCluster = etcdConfig.InitialClusterFromName(etcdConfig.Name)
+	etcdConfig.ClientTLSInfo = tlsInfo
+	etcdConfig.PeerTLSInfo = tlsInfo
+
+	return embed.StartEtcd(etcdConfig)
+}
+
+func listKeys(serverAddress string, username string, password string, tls *tls.Config, prefix string) (map[string]*databasev1.Node, error) {
+	defaultDialTimeout := 5 * time.Second
+	client, err := clientv3.New(clientv3.Config{
+		Endpoints:   []string{serverAddress},
+		TLS:         tls,
+		Username:    username,
+		Password:    password,
+		DialTimeout: defaultDialTimeout,
+	})
+	if err != nil {
+		log.Fatalf("Failed to create etcd client: %v", err)
+	}
+	defer client.Close()
+
+	ctx, cancel := context.WithTimeout(context.Background(), defaultDialTimeout)
+	defer cancel()
+
+	resp, err := client.Get(ctx, prefix, clientv3.WithPrefix())
+	if err != nil {
+		return nil, err
+	}
+
+	if resp.Count == 0 {
+		return nil, nil
+	}
+
+	nodeMap := make(map[string]*databasev1.Node)
+	for _, kv := range resp.Kvs {
+		md, err := schema.KindNode.Unmarshal(kv)
+		if err != nil {
+			return nil, err
+		}
+		nodeMap[string(kv.Key)] = md.Spec.(*databasev1.Node)
+	}
+
+	return nodeMap, nil
+}
diff --git a/test/integration/etcd/testdata/ca.crt b/test/integration/etcd/testdata/ca.crt
new file mode 100644
index 00000000..8e373720
--- /dev/null
+++ b/test/integration/etcd/testdata/ca.crt
@@ -0,0 +1,22 @@
+-----BEGIN CERTIFICATE-----
+MIIDrjCCApagAwIBAgIUNkN+TZ3hgHno+H9j56nWkmb4dBEwDQYJKoZIhvcNAQEL
+BQAwbzEMMAoGA1UEBhMDVVNBMRMwEQYDVQQIEwpDYWxpZm9ybmlhMRYwFAYDVQQH
+Ew1TYW4gRnJhbmNpc2NvMQ0wCwYDVQQKEwRldGNkMRYwFAYDVQQLEw1ldGNkIFNl
+Y3VyaXR5MQswCQYDVQQDEwJjYTAeFw0yMTAyMjgxMDQ4MDBaFw0zMTAyMjYxMDQ4
+MDBaMG8xDDAKBgNVBAYTA1VTQTETMBEGA1UECBMKQ2FsaWZvcm5pYTEWMBQGA1UE
+BxMNU2FuIEZyYW5jaXNjbzENMAsGA1UEChMEZXRjZDEWMBQGA1UECxMNZXRjZCBT
+ZWN1cml0eTELMAkGA1UEAxMCY2EwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEK
+AoIBAQDZwQPFZB+Kt6RIzYvTgbNlRIX/cLVknIy4ZqhLYDQNOdosJn04jjkCfS3k
+F5JZuabkUs6d6JcLTbLWV5hCrwZVlCFf3PDn6DvK12GZpybhuqMPZ2T8P2U17AFP
+mUj/Rm+25t8Er5r+8ijZmqVi1X1Ef041CFGESr3KjaMjec2kYf38cfEOp2Yq1JWO
+0wpVfLElnyDQY9XILdnBepCRZYPq1eW1OSkRk+dZQnJP6BO95IoyREDuBUeTrteR
+7dHHTF9AAgR5tnyZ+eLuVUZ2kskcWLxH3y9RyjvVJ+1uCzbdydVPf0H1pBoqWcuA
+PYjYkLKMOKBWfYJhSzykhf+QMC7xAgMBAAGjQjBAMA4GA1UdDwEB/wQEAwIBBjAP
+BgNVHRMBAf8EBTADAQH/MB0GA1UdDgQWBBQpJiv07dkY9WB0zgB6wOb/HMi8oDAN
+BgkqhkiG9w0BAQsFAAOCAQEA0TQ8rRmLt4wYjz0BKh+jElMIg6LBPsCPpfmGDLmK
+fdj4Jp7QFlLmXlQSmm8zKz3ftKoOFPYGQYHUkIirIrQB/tdHoXJLgxCzI0SrdCiM
+m/DPVjfOTa9Mm5rPcUR79rGDLj2BgzDB+NTETVDXo8mAL5MjFdUyh6jOGBctkCG/
+TWdUaN33ZLwUl488NLaw98fIZ/F4d/dsyCJvHEaoo++dgjduoQxmH9Scr2Frmd8G
+zYxOoZHG3ARBDp2mpr+I3UCR1/KTITF/NXL6gDcNY3wyZzoaGua7Bd/ysMSi1w3j
+CyvClSvRPJRLQemGUP7B/Y8FUkbJ2i/7tz6ozn8sLi3V2Q==
+-----END CERTIFICATE-----
diff --git a/test/integration/etcd/testdata/client-clientusage.crt b/test/integration/etcd/testdata/client-clientusage.crt
new file mode 100644
index 00000000..71b305fb
--- /dev/null
+++ b/test/integration/etcd/testdata/client-clientusage.crt
@@ -0,0 +1,24 @@
+-----BEGIN CERTIFICATE-----
+MIIECDCCAvCgAwIBAgIULbzkAv8zbkJzZIRDPnBwXl0/BH0wDQYJKoZIhvcNAQEL
+BQAwbzEMMAoGA1UEBhMDVVNBMRMwEQYDVQQIEwpDYWxpZm9ybmlhMRYwFAYDVQQH
+Ew1TYW4gRnJhbmNpc2NvMQ0wCwYDVQQKEwRldGNkMRYwFAYDVQQLEw1ldGNkIFNl
+Y3VyaXR5MQswCQYDVQQDEwJjYTAeFw0yMTAyMjgxMDQ4MDBaFw0zMTAyMjYxMDQ4
+MDBaMHgxDDAKBgNVBAYTA1VTQTETMBEGA1UECBMKQ2FsaWZvcm5pYTEWMBQGA1UE
+BxMNU2FuIEZyYW5jaXNjbzENMAsGA1UEChMEZXRjZDEWMBQGA1UECxMNZXRjZCBT
+ZWN1cml0eTEUMBIGA1UEAxMLZXhhbXBsZS5jb20wggEiMA0GCSqGSIb3DQEBAQUA
+A4IBDwAwggEKAoIBAQDWBNo9tYRoQKv76xabz0EPXGJKHIrUjf0NbXz3d9jbP2sH
+3hutXr/A221pULfZYIZdaUtmEuEr1905nYwJ2gnO9Y/iSc6fQ/4EjoT+VZLdINQw
+I1dG2rtv2ZuYL5oYfgCjLkV1LzYuyfY/zJ93WoJW0YA0t50MEQNGEqD7pYlhsPej
+iGyjagSi7zsoAkAagNprULH6RyAqDG7db+MfJOUzHUv4PWGBXPb0PHY3xA+WayFB
+nP5AZO16oDh/UnzvfEAJULXeIOLs4eOmtzKMwZwrWzgCB+jBeVlc1FOwXQcmBamN
+eYUs75GoO9aSSLROvnQiw2P0z0xVNmDokDXGsSRxAgMBAAGjgZIwgY8wDgYDVR0P
+AQH/BAQDAgWgMBMGA1UdJQQMMAoGCCsGAQUFBwMCMAwGA1UdEwEB/wQCMAAwHQYD
+VR0OBBYEFCB4ysDF81d6lkKIvebj08BcRWNoMB8GA1UdIwQYMBaAFCkmK/Tt2Rj1
+YHTOAHrA5v8cyLygMBoGA1UdEQQTMBGCCWxvY2FsaG9zdIcEfwAAATANBgkqhkiG
+9w0BAQsFAAOCAQEAo2B+piCBTjdpCLFj/kc+A0alZTbNdr0+BTsN+5aBE9k4JlZS
+smkIQL0vyzjKw/W/o2EyPVcVKJX52/GQsC3bQrBb2lH1jRYgt5pRo24kKHy4Nlc3
+IaYg++ssfT2ZdpYiL3lzLyOHEumcynz3nI5M81e5CCIdEennxaM8FuiYN5OXDOR3
+j+bCYHLYPaWYZopfiSrnq+Z4gRUS2sMI1yqtiPSUdIJLnTfyEEdexvs/KUtFWvFO
+4AcecKvT6HA8oNDiWfE6e854uDLTkbXW1rK+FWPU9pv5NR50+GBCvxvmDGtGXxQu
+yu+kOsx2gfgNc4idIv1pjZF/1YzrrKGAhChN2A==
+-----END CERTIFICATE-----
diff --git a/test/integration/etcd/testdata/client-clientusage.key.insecure b/test/integration/etcd/testdata/client-clientusage.key.insecure
new file mode 100644
index 00000000..ea139257
--- /dev/null
+++ b/test/integration/etcd/testdata/client-clientusage.key.insecure
@@ -0,0 +1,27 @@
+-----BEGIN RSA PRIVATE KEY-----
+MIIEogIBAAKCAQEA1gTaPbWEaECr++sWm89BD1xiShyK1I39DW1893fY2z9rB94b
+rV6/wNttaVC32WCGXWlLZhLhK9fdOZ2MCdoJzvWP4knOn0P+BI6E/lWS3SDUMCNX
+Rtq7b9mbmC+aGH4Aoy5FdS82Lsn2P8yfd1qCVtGANLedDBEDRhKg+6WJYbD3o4hs
+o2oEou87KAJAGoDaa1Cx+kcgKgxu3W/jHyTlMx1L+D1hgVz29Dx2N8QPlmshQZz+
+QGTteqA4f1J873xACVC13iDi7OHjprcyjMGcK1s4AgfowXlZXNRTsF0HJgWpjXmF
+LO+RqDvWkki0Tr50IsNj9M9MVTZg6JA1xrEkcQIDAQABAoIBAAGBZTub5EOLeOo7
+vBv6eD2wa6yTyNI38Xi/tWpUOH1KU+lpQY6VpQmpQXrFK5Xm3OsZS4N7TIQvb4nx
+NsP2+aywA4QW+tIZ+1Zy3jKfzXmqunNgPEPuU/U0dai7ZP0ZHc4IDEsHuvzXRNks
+Ck8fnt0XeixkwkEMeZZrmSBMCMxcHAWxiv+oXF+olN3vTD2aDC8T6YwahMyQUQfW
+IA9fuO8Dzzmk2I7mDHa29cbB+PW4E5tkJmHVZqEu8jPgMjCJGc2IR1YpLAXF8YBB
+vgh6ZgI6JOg1OiNETuQekamAMOblFVOdPUjPSxuyJzEE8VpIdD3Z9UMNq+FDQh/F
+j1lEEEECgYEA9nYwUh+e0H9c9IRBLNYAbq2PV4SpFKvFrHOTQpylMPisUTgdHKLT
+CvO1wbNprElBAulOWobCyKshWGd5ECFsCvsWS6xmGi442q3ov5xtAMmvSmtW8s+8
+tUeVRQGS/Yn5Uxj2msUPe6vJEniLgsxmbFbDYqvr65COrAsCDEY3DkkCgYEA3k09
+EGhiO1joDtJPI21vUzzecBuep32oKiwip3OgS/mct04/QR+6lp1x4sPMYlyxbyk9
+jPdkzU07d8r+mES9RweE5lc1aCaF5eA8y6qtL9vBgsXRiEXlpYLxb0TOQaYNU0qM
+aYumYPWjsjwYDvRKaVzThFUkYwapKFqtMV98BOkCgYAkIOkucLIwMCtpMKX5M5m2
+n7yegLTkcdW1VO/mWN4iUqG3+jjSRNAZD+a58VnxRn/ANIEm5hBRqDxoICrwAWY8
+Kdh32VrSRapR7CJtTDnyXp5Sk2+YgnlQPaEVD4kDn6Er3EHyKCb/4wvDqGYTE3GE
+OifEJB2eV3+Cms5/DB/v+QKBgFzV8r9saEGSkm7GI2iPJiOj0t0Mm8gksNrTzbES
+l4nC91CR69adkoWdwNbLoAof3bWnil3ZXw5hx4jyjDo40rbcDANJvjL9i4OBjsIb
+R/Ipmvmq9SMs1Ye2VG98U4qU9xGmm1bkjBoH21HuyLlOCdlQe8DS8bwtJu2EWLm6
+v4cpAoGAP3pqi6iIZAbJVqw6p5Xd/BLzn3iS+lwQjwF/IWp5UnFCGovnVSJG2wqP
+kxL9jy4asMDDuMKzEzO3+UT6WBRI+idV8PgDNEYkXcnVAA5lZ+2kCJwRICsC6MYH
+1nIHJtPngUrwT3TUhMp/WfpYUjTdiOC3aJmKq/NGZxE8/Sb3G6U=
+-----END RSA PRIVATE KEY-----
diff --git a/test/integration/etcd/testdata/server-serverusage.crt b/test/integration/etcd/testdata/server-serverusage.crt
new file mode 100644
index 00000000..b7aca3e5
--- /dev/null
+++ b/test/integration/etcd/testdata/server-serverusage.crt
@@ -0,0 +1,24 @@
+-----BEGIN CERTIFICATE-----
+MIIECDCCAvCgAwIBAgIUDVfELNb4NV52PJjENU2DNOIFx6IwDQYJKoZIhvcNAQEL
+BQAwbzEMMAoGA1UEBhMDVVNBMRMwEQYDVQQIEwpDYWxpZm9ybmlhMRYwFAYDVQQH
+Ew1TYW4gRnJhbmNpc2NvMQ0wCwYDVQQKEwRldGNkMRYwFAYDVQQLEw1ldGNkIFNl
+Y3VyaXR5MQswCQYDVQQDEwJjYTAeFw0yMTAyMjgxMDQ4MDBaFw0zMTAyMjYxMDQ4
+MDBaMHgxDDAKBgNVBAYTA1VTQTETMBEGA1UECBMKQ2FsaWZvcm5pYTEWMBQGA1UE
+BxMNU2FuIEZyYW5jaXNjbzENMAsGA1UEChMEZXRjZDEWMBQGA1UECxMNZXRjZCBT
+ZWN1cml0eTEUMBIGA1UEAxMLZXhhbXBsZS5jb20wggEiMA0GCSqGSIb3DQEBAQUA
+A4IBDwAwggEKAoIBAQDvx5mE6ZwV61si7QK18X+K6lJSWr8rE+0l4YbvbLh/bCdP
+bnPzTsiK+FN4FA9fur73L/RvQDxO9XG99pKIhKPGxiit6DSJ9rL24RpK7tJpgnnK
+Kdry6E+6/ZvVXBP3LtdJyX3kpdw+TSivlwi50CeEQA5argOXnyDNGIOWC8iMpeg7
+z3pE0DfpDEgLMjGE/I/9YHRCOiK/8kQchXqVfWHpALFakf9+QPNpNrEIPjV7IPja
+GEUeDG9MI0PYbBl45NkKIi0nelV/Nay9kyPSPKfvngJU2dTqsGSVXW+DlZo7OJBh
+RLEih+CWGfotGzFzXWdQRvD/VleaxYYUDKkyHVx1AgMBAAGjgZIwgY8wDgYDVR0P
+AQH/BAQDAgWgMBMGA1UdJQQMMAoGCCsGAQUFBwMBMAwGA1UdEwEB/wQCMAAwHQYD
+VR0OBBYEFKdzgCE8K0tYt9LaF5N2aQrrHru0MB8GA1UdIwQYMBaAFCkmK/Tt2Rj1
+YHTOAHrA5v8cyLygMBoGA1UdEQQTMBGCCWxvY2FsaG9zdIcEfwAAATANBgkqhkiG
+9w0BAQsFAAOCAQEAYcD+1ebiJna7dfydgw/yox+b6/KO16evaU4c5Spu5O+VtcZG
+rKMi8MT8V0U7kL9Xo9TszbzwpPWr1It0wcmM1cZrwykkT/baVJADaLtfSFUtlCDX
+HNB04C2UUBPPosFr1d5YtwyN55qxgyMg+IDeMubYZ4qwDWCYBTIiz9yHoQ6LuuV8
+Tkkpa6X5n4+fO2iUgA6SZUkwZGdbQLOz9VMa1qgyOz3ejuDeMc4sa08iADs4wG/X
+ohRGg0Df5THeXhR+Pn0HBf3T0eTAeZzLL5xtlIn9o6o9CEU573uEYQI1BG1kcDeQ
+Rs9J/2iuLqr8GAjr7k5aUW3FFYRtqC3YR0G5Eg==
+-----END CERTIFICATE-----
diff --git a/test/integration/etcd/testdata/server-serverusage.key.insecure b/test/integration/etcd/testdata/server-serverusage.key.insecure
new file mode 100644
index 00000000..a0ac946a
--- /dev/null
+++ b/test/integration/etcd/testdata/server-serverusage.key.insecure
@@ -0,0 +1,27 @@
+-----BEGIN RSA PRIVATE KEY-----
+MIIEpQIBAAKCAQEA78eZhOmcFetbIu0CtfF/iupSUlq/KxPtJeGG72y4f2wnT25z
+807IivhTeBQPX7q+9y/0b0A8TvVxvfaSiISjxsYoreg0ifay9uEaSu7SaYJ5yina
+8uhPuv2b1VwT9y7XScl95KXcPk0or5cIudAnhEAOWq4Dl58gzRiDlgvIjKXoO896
+RNA36QxICzIxhPyP/WB0Qjoiv/JEHIV6lX1h6QCxWpH/fkDzaTaxCD41eyD42hhF
+HgxvTCND2GwZeOTZCiItJ3pVfzWsvZMj0jyn754CVNnU6rBklV1vg5WaOziQYUSx
+Iofglhn6LRsxc11nUEbw/1ZXmsWGFAypMh1cdQIDAQABAoIBAQCnpgkisyuc78fy
+7YAdslKY0Cjqx+QtvGrtN3he4sdE4FvD39hWX9k7wVCq/muZZTqsHe1r85+3HUl/
+pmzh4suX6Wj73wUNCV4r20vE5KJdfwqkXQtnFyLX/QX98blL9IY2YxkQyx7ouI4f
+5xwEvxNCFn9yy4RbeLk4bVFjka2RF/x6qEUCHq5Q74vWvyC1i3kGKgYruM39RQw3
+D5fG8xdUexBc32nfzynP+0NcFAiy+yUQWOLcE4i8XaegFvg+QvWOx1iwjqU3FDeC
+JzKrtw9SLBWf7AGraxA59K4WJ63xqGqFugWcFaYh923X8zES/s0wrtV2T14Lgj3Q
+aWJ0DfQBAoGBAPNd1Aph0Z9PwZ1zivJprj8XDnFP2XIqlODYuiVddjbvIV13d6F/
+PE/ViW0MVduF0ejkPs9+hSxYOH58EWIt44Li/Nre1U42ny+fJrY1P5Mq5nriM4L4
+lx2YFaWzAoxzpMbbQ14kEMcQSicziDbBx62aaQYu4UwrvqXYdSYp+D+BAoGBAPw6
+Gtv6hytg19GtH6sQG9/4K4cLGX4lJE3pTL3eUicfoEI+hviZdG8FS77Uv05ga6fQ
+OlyqvpmmXp6fgTrSlHBeKO75A3sT7fP1v1foq1y+CdMGytOnJENUc80bN0L1dFI1
+zwYm7eLDP0KdUYpf+Rpgcap4StQbotpc6oy705b1AoGBAO9z26VXd+ybifKE9Cru
+Zp727aP6IAaf9RqCxCztl9oXUanoWVISoeIfRfeA0p2LPu06Xr7ESv5F01hIdMY4
+RonLE2W7KP+q6NfvbSSMogAIjvxLwslUFUPuFyaRSqmtQ2zR4qgnLkbfNUb7AkR2
+SCT9L+cAi3bp98ywfRvO4c6BAoGANkAJJudry1i5EtA5z4FXfYTTV+h7QzaZ6GgV
+qYD4CpIy1gy82xumf3qUICeCPkle3mlbJDNVa5btIxELqqtAYiregwfsR7yxoZdp
+4G6a7Qey9UCwv3Vjx1eS0LrZ1/0TV9ta++fDotJ7+Mf9kdWyromv6QqWjaikDnON
+v1dm20ECgYEA6i+uvBuomUzqJYjUTNgMtmfYjuXv8D58wLkY10si7T2ayAlFKBG2
+Dno/dojOcixO1dtaq7dA+KNXtESekjT1Y4VleGHWpEglRScE629iow4ASrluP/Ec
+F2DvTRW4daFDWQV4je1u0+wDj5B8KZjO/e759BztiRyRqTCzpxTa8Ms=
+-----END RSA PRIVATE KEY-----
diff --git a/test/integration/etcd/testdata/server.crt b/test/integration/etcd/testdata/server.crt
new file mode 100644
index 00000000..04c3d7b6
--- /dev/null
+++ b/test/integration/etcd/testdata/server.crt
@@ -0,0 +1,24 @@
+-----BEGIN CERTIFICATE-----
+MIIEEjCCAvqgAwIBAgIUPMH88Ekdi6JPq+5703+qGFH7VmMwDQYJKoZIhvcNAQEL
+BQAwbzEMMAoGA1UEBhMDVVNBMRMwEQYDVQQIEwpDYWxpZm9ybmlhMRYwFAYDVQQH
+Ew1TYW4gRnJhbmNpc2NvMQ0wCwYDVQQKEwRldGNkMRYwFAYDVQQLEw1ldGNkIFNl
+Y3VyaXR5MQswCQYDVQQDEwJjYTAeFw0yMTAyMjgxMDQ4MDBaFw0zMTAyMjYxMDQ4
+MDBaMHgxDDAKBgNVBAYTA1VTQTETMBEGA1UECBMKQ2FsaWZvcm5pYTEWMBQGA1UE
+BxMNU2FuIEZyYW5jaXNjbzENMAsGA1UEChMEZXRjZDEWMBQGA1UECxMNZXRjZCBT
+ZWN1cml0eTEUMBIGA1UEAxMLZXhhbXBsZS5jb20wggEiMA0GCSqGSIb3DQEBAQUA
+A4IBDwAwggEKAoIBAQDD3vhO7LT77scWKnsozEg1DiQQsAbgGFfAoQJOvgrRv7V7
+I5+9n7hlpqKEIYkOuX0LSqpLBJ+9ORxXPBNZFKsytryOc3ZWTAoozUkufUOKfUFa
+1QpExA5u/FpwtNXGRGC35wus/JVTtcIiifeml2PIdyoxdXfev6y4yJvhO38Osqru
+GoySDORGsPrmLdpoUieofhmHWEgONpoY3fVsqAwiP1NMDNuqbVHvDjMykxj9AmQa
+WBdspsRXcgcl5Dp7mf1KVPRbvnCOjLuDDiBCwTTgpU3sDFyhHm/Bq29lPwlHWi7Z
+WKQYGIwqQOfOjfjhH009/+z11Z/1ovj+FWLbcXP9AgMBAAGjgZwwgZkwDgYDVR0P
+AQH/BAQDAgWgMB0GA1UdJQQWMBQGCCsGAQUFBwMBBggrBgEFBQcDAjAMBgNVHRMB
+Af8EAjAAMB0GA1UdDgQWBBT0sUW5xBildDtaySEQYE9vX+8SZDAfBgNVHSMEGDAW
+gBQpJiv07dkY9WB0zgB6wOb/HMi8oDAaBgNVHREEEzARgglsb2NhbGhvc3SHBH8A
+AAEwDQYJKoZIhvcNAQELBQADggEBAATMlUra5N3Z4KB++xmGM6h9OhYmbKqn6IEV
+o4mqrTimyzgtvVsHh/v3XvBzxAdma6QjkZygfg+EIHSLbJVmZzxzr3YeENu4EyDc
+l+FfNPCyFHX9CH2Rk1ZThkQbmqVrzInXmG47G/PbTC2l8+kAZwvp37QfIJNCYIku
+XTp9R72sEkfNXxZxsZjwM7Z++LaB+cVEuLNJG0OpMhouTuxoN6pemzBmmFBP2mOr
+SeZnuEVtvDIbklJDdgcB/mPd7FE2xCVfa+p5Ol9Fcw5aPxTXAAQ+aVDtzh7jcFWk
+SV4K/ZYFqYN4E4H1UXlkHKj/qCmryvPxe8DVzu6Pd0ZyA0H4Lxk=
+-----END CERTIFICATE-----
diff --git a/test/integration/etcd/testdata/server.key.insecure b/test/integration/etcd/testdata/server.key.insecure
new file mode 100644
index 00000000..b52bdc79
--- /dev/null
+++ b/test/integration/etcd/testdata/server.key.insecure
@@ -0,0 +1,27 @@
+-----BEGIN RSA PRIVATE KEY-----
+MIIEpAIBAAKCAQEAw974Tuy0++7HFip7KMxINQ4kELAG4BhXwKECTr4K0b+1eyOf
+vZ+4ZaaihCGJDrl9C0qqSwSfvTkcVzwTWRSrMra8jnN2VkwKKM1JLn1Din1BWtUK
+RMQObvxacLTVxkRgt+cLrPyVU7XCIon3ppdjyHcqMXV33r+suMib4Tt/DrKq7hqM
+kgzkRrD65i3aaFInqH4Zh1hIDjaaGN31bKgMIj9TTAzbqm1R7w4zMpMY/QJkGlgX
+bKbEV3IHJeQ6e5n9SlT0W75wjoy7gw4gQsE04KVN7AxcoR5vwatvZT8JR1ou2Vik
+GBiMKkDnzo344R9NPf/s9dWf9aL4/hVi23Fz/QIDAQABAoIBAQCKvee5UCYqxkoz
+Q0gV8A29txSI1YcpOVT/V41g5XCYfmk4nlVKZlahelVnrrF8wpr2Yp8ZoF7eFBQl
+HqK92Mwjkhkh9lt+aUJRAIiz63rqICspAfrSFuX6a7pMV2uNk2XHHlvA3vGPaBHp
+kTzgvh+qIe67NfAA0liwUzlHY3NunpXW6UQm2OWtabOfc1zZ78E58It0VTaAWKZq
+KDOxQGdwS4BGIUbsCvktPgncDDzi8wIZY/6YDTXkYKUJWqVEWf3SregXbX6/cjQa
+x8v2v95LiMk0vp9E5GG9QKdTEC+fZyQsq983G9t4O9VPw2JBR2TUxIRrPH0gkvhQ
+F1n3yYABAoGBAM1qHX1wQoHiOpVsgaXrAX2QWrBeNpbGAgVh67MsD+FlUH/ZikcK
+dsz/pTg+TmUuURKadXWJx43E/IVb6Bw8io0uF30aXeWRSIDK99FAmx2GxxTBge5f
+MtAQSTctr//yYLaNZWSdTpmaQYRtenK8zN6OTQ251M0sWyWsv3UcpBABAoGBAPQb
+NFqaw8C8JgJtHa7wrYCU9ShNxzQzo/jgu5ZuO6CxKNYyFGR8ZOpWQx8vTrNkjDXN
+iq9oZ3gIcm7c4TQOg3ydISNyoAEYrhRgQC6+rDkl4Zgby6ssvDHMqCclMp5Ztn3Y
+bfXG2gk3ULLH8TkQ2KWRJrpPT3iSdvLVXmNHHaP9AoGADAxhVm4zOHMQhJssr5Kt
+L7Q73YRpJ0bN74rizEuVUt8ibZ1Q4wHWHggQpM/iwUSKNNEiepZuQf5/4UKWxrE2
+XzmI3ymgwEpZOlStXHSxpHW3T5xaBqVG0bVi1f20CQsqaQq6G8CuT4wgs6fIOtqg
+GZ23H0r7FF25qugLAs9/QAECgYBsi6ROHb+p9oAYWBj474DXSmVxVJSd+9CQHK6N
+h9rv65czF/XFcSMWqOET/t9KGg3W5t0ifpRz5Z2s+n8RvNpvERfpQVEw656M5Pfl
+UVgX2WZlUwbPyQauRkkHjxzhGRdzAkhzH8dYjcZOmWYEcB9GEDNeaWH3RXmrJYHh
+N4BQqQKBgQCtdULs1ju0Y5zwSdkxhBWJHxCpLjuCpxfpWmnVFlp/Fj9S7rBfzWrt
+hYjsNyfMUm13jg4+6CJNdf3pQbdczXWTt/E0t/uoo1m8Azcd1qFPKV8wVOSiZBXv
+mMWHW8fw55ygoFyBlQpzwI6M+Zpwv3Smli/H9gQClsS2RmdIjUhqtg==
+-----END RSA PRIVATE KEY-----