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 <[email protected]>
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 <[email protected]>
---
 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-----


Reply via email to