Skip to content

Commit 22943e7

Browse files
committed
integration: test TLS reload
Signed-off-by: Gyu-Ho Lee <gyuhox@gmail.com>
1 parent 4e21f87 commit 22943e7

File tree

3 files changed

+274
-0
lines changed

3 files changed

+274
-0
lines changed

integration/cluster.go

+7
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,13 @@ var (
7676
ClientCertAuth: true,
7777
}
7878

79+
testTLSInfoExpired = transport.TLSInfo{
80+
KeyFile: "./fixtures-expired/server-key.pem",
81+
CertFile: "./fixtures-expired/server.pem",
82+
TrustedCAFile: "./fixtures-expired/etcd-root-ca.pem",
83+
ClientCertAuth: true,
84+
}
85+
7986
plog = capnslog.NewPackageLogger("github.com/coreos/etcd", "integration")
8087
)
8188

integration/util_test.go

+62
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
// Copyright 2017 The etcd Authors
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
package integration
16+
17+
import (
18+
"io"
19+
"os"
20+
"path/filepath"
21+
22+
"github.com/coreos/etcd/pkg/transport"
23+
)
24+
25+
// copyTLSFiles clones certs files to dst directory.
26+
func copyTLSFiles(ti transport.TLSInfo, dst string) (transport.TLSInfo, error) {
27+
ci := transport.TLSInfo{
28+
KeyFile: filepath.Join(dst, "server-key.pem"),
29+
CertFile: filepath.Join(dst, "server.pem"),
30+
TrustedCAFile: filepath.Join(dst, "etcd-root-ca.pem"),
31+
ClientCertAuth: ti.ClientCertAuth,
32+
}
33+
if err := copyFile(ti.KeyFile, ci.KeyFile); err != nil {
34+
return transport.TLSInfo{}, err
35+
}
36+
if err := copyFile(ti.CertFile, ci.CertFile); err != nil {
37+
return transport.TLSInfo{}, err
38+
}
39+
if err := copyFile(ti.TrustedCAFile, ci.TrustedCAFile); err != nil {
40+
return transport.TLSInfo{}, err
41+
}
42+
return ci, nil
43+
}
44+
45+
func copyFile(src, dst string) error {
46+
f, err := os.Open(src)
47+
if err != nil {
48+
return err
49+
}
50+
defer f.Close()
51+
52+
w, err := os.Create(dst)
53+
if err != nil {
54+
return err
55+
}
56+
defer w.Close()
57+
58+
if _, err = io.Copy(w, f); err != nil {
59+
return err
60+
}
61+
return w.Sync()
62+
}

integration/v3_grpc_test.go

+205
Original file line numberDiff line numberDiff line change
@@ -16,17 +16,21 @@ package integration
1616

1717
import (
1818
"bytes"
19+
"crypto/tls"
1920
"fmt"
21+
"io/ioutil"
2022
"math/rand"
2123
"os"
2224
"reflect"
2325
"testing"
2426
"time"
2527

28+
"github.com/coreos/etcd/clientv3"
2629
"github.com/coreos/etcd/etcdserver/api/v3rpc"
2730
"github.com/coreos/etcd/etcdserver/api/v3rpc/rpctypes"
2831
pb "github.com/coreos/etcd/etcdserver/etcdserverpb"
2932
"github.com/coreos/etcd/pkg/testutil"
33+
3034
"golang.org/x/net/context"
3135
"google.golang.org/grpc"
3236
"google.golang.org/grpc/metadata"
@@ -1374,6 +1378,207 @@ func TestTLSGRPCAcceptSecureAll(t *testing.T) {
13741378
}
13751379
}
13761380

1381+
// TestTLSReloadAtomicReplace ensures server reloads expired/valid certs
1382+
// when all certs are atomically replaced by directory renaming.
1383+
// And expects server to reject client requests, and vice versa.
1384+
func TestTLSReloadAtomicReplace(t *testing.T) {
1385+
defer testutil.AfterTest(t)
1386+
1387+
// clone valid,expired certs to separate directories for atomic renaming
1388+
vDir, err := ioutil.TempDir(os.TempDir(), "fixtures-valid")
1389+
if err != nil {
1390+
t.Fatal(err)
1391+
}
1392+
defer os.RemoveAll(vDir)
1393+
ts, err := copyTLSFiles(testTLSInfo, vDir)
1394+
if err != nil {
1395+
t.Fatal(err)
1396+
}
1397+
eDir, err := ioutil.TempDir(os.TempDir(), "fixtures-expired")
1398+
if err != nil {
1399+
t.Fatal(err)
1400+
}
1401+
defer os.RemoveAll(eDir)
1402+
if _, err = copyTLSFiles(testTLSInfoExpired, eDir); err != nil {
1403+
t.Fatal(err)
1404+
}
1405+
1406+
tDir, err := ioutil.TempDir(os.TempDir(), "fixtures")
1407+
if err != nil {
1408+
t.Fatal(err)
1409+
}
1410+
os.RemoveAll(tDir)
1411+
defer os.RemoveAll(tDir)
1412+
1413+
// start with valid certs
1414+
clus := NewClusterV3(t, &ClusterConfig{Size: 1, PeerTLS: &ts, ClientTLS: &ts})
1415+
defer clus.Terminate(t)
1416+
1417+
// concurrent client dialing while certs transition from valid to expired
1418+
errc := make(chan error, 1)
1419+
go func() {
1420+
for {
1421+
cc, err := ts.ClientConfig()
1422+
if err != nil {
1423+
if os.IsNotExist(err) {
1424+
// from concurrent renaming
1425+
continue
1426+
}
1427+
t.Fatal(err)
1428+
}
1429+
cli, cerr := clientv3.New(clientv3.Config{
1430+
Endpoints: []string{clus.Members[0].GRPCAddr()},
1431+
DialTimeout: time.Second,
1432+
TLS: cc,
1433+
})
1434+
if cerr != nil {
1435+
errc <- cerr
1436+
return
1437+
}
1438+
cli.Close()
1439+
}
1440+
}()
1441+
1442+
// replace certs directory with expired ones
1443+
if err = os.Rename(vDir, tDir); err != nil {
1444+
t.Fatal(err)
1445+
}
1446+
if err = os.Rename(eDir, vDir); err != nil {
1447+
t.Fatal(err)
1448+
}
1449+
1450+
// after rename,
1451+
// 'vDir' contains expired certs
1452+
// 'tDir' contains valid certs
1453+
// 'eDir' does not exist
1454+
1455+
select {
1456+
case err = <-errc:
1457+
if err != grpc.ErrClientConnTimeout {
1458+
t.Fatalf("expected %v, got %v", grpc.ErrClientConnTimeout, err)
1459+
}
1460+
case <-time.After(5 * time.Second):
1461+
t.Fatal("failed to receive dial timeout error")
1462+
}
1463+
1464+
// now, replace expired certs back with valid ones
1465+
if err = os.Rename(tDir, eDir); err != nil {
1466+
t.Fatal(err)
1467+
}
1468+
if err = os.Rename(vDir, tDir); err != nil {
1469+
t.Fatal(err)
1470+
}
1471+
if err = os.Rename(eDir, vDir); err != nil {
1472+
t.Fatal(err)
1473+
}
1474+
1475+
// new incoming client request should trigger
1476+
// listener to reload valid certs
1477+
var tls *tls.Config
1478+
tls, err = ts.ClientConfig()
1479+
if err != nil {
1480+
t.Fatal(err)
1481+
}
1482+
var cl *clientv3.Client
1483+
cl, err = clientv3.New(clientv3.Config{
1484+
Endpoints: []string{clus.Members[0].GRPCAddr()},
1485+
DialTimeout: time.Second,
1486+
TLS: tls,
1487+
})
1488+
if err != nil {
1489+
t.Fatalf("expected no error, got %v", err)
1490+
}
1491+
cl.Close()
1492+
}
1493+
1494+
// TestTLSReloadCopy ensures server reloads expired/valid certs
1495+
// when new certs are copied over, one by one. And expects server
1496+
// to reject client requests, and vice versa.
1497+
func TestTLSReloadCopy(t *testing.T) {
1498+
defer testutil.AfterTest(t)
1499+
1500+
// clone certs directory, free to overwrite
1501+
cDir, err := ioutil.TempDir(os.TempDir(), "fixtures-test")
1502+
if err != nil {
1503+
t.Fatal(err)
1504+
}
1505+
defer os.RemoveAll(cDir)
1506+
ts, err := copyTLSFiles(testTLSInfo, cDir)
1507+
if err != nil {
1508+
t.Fatal(err)
1509+
}
1510+
1511+
// start with valid certs
1512+
clus := NewClusterV3(t, &ClusterConfig{Size: 1, PeerTLS: &ts, ClientTLS: &ts})
1513+
defer clus.Terminate(t)
1514+
1515+
// concurrent client dialing while certs transition from valid to expired
1516+
errc := make(chan error, 1)
1517+
go func() {
1518+
for {
1519+
cc, err := ts.ClientConfig()
1520+
if err != nil {
1521+
// from concurrent certs overwriting
1522+
switch err.Error() {
1523+
case "tls: private key does not match public key":
1524+
fallthrough
1525+
case "tls: failed to find any PEM data in key input":
1526+
continue
1527+
}
1528+
t.Fatal(err)
1529+
}
1530+
cli, cerr := clientv3.New(clientv3.Config{
1531+
Endpoints: []string{clus.Members[0].GRPCAddr()},
1532+
DialTimeout: time.Second,
1533+
TLS: cc,
1534+
})
1535+
if cerr != nil {
1536+
errc <- cerr
1537+
return
1538+
}
1539+
cli.Close()
1540+
}
1541+
}()
1542+
1543+
// overwrite valid certs with expired ones
1544+
// (e.g. simulate cert expiration in practice)
1545+
if _, err = copyTLSFiles(testTLSInfoExpired, cDir); err != nil {
1546+
t.Fatal(err)
1547+
}
1548+
1549+
select {
1550+
case gerr := <-errc:
1551+
if gerr != grpc.ErrClientConnTimeout {
1552+
t.Fatalf("expected %v, got %v", grpc.ErrClientConnTimeout, gerr)
1553+
}
1554+
case <-time.After(5 * time.Second):
1555+
t.Fatal("failed to receive dial timeout error")
1556+
}
1557+
1558+
// now, replace expired certs back with valid ones
1559+
if _, err = copyTLSFiles(testTLSInfo, cDir); err != nil {
1560+
t.Fatal(err)
1561+
}
1562+
1563+
// new incoming client request should trigger
1564+
// listener to reload valid certs
1565+
var tls *tls.Config
1566+
tls, err = ts.ClientConfig()
1567+
if err != nil {
1568+
t.Fatal(err)
1569+
}
1570+
var cl *clientv3.Client
1571+
cl, err = clientv3.New(clientv3.Config{
1572+
Endpoints: []string{clus.Members[0].GRPCAddr()},
1573+
DialTimeout: time.Second,
1574+
TLS: tls,
1575+
})
1576+
if err != nil {
1577+
t.Fatalf("expected no error, got %v", err)
1578+
}
1579+
cl.Close()
1580+
}
1581+
13771582
func TestGRPCRequireLeader(t *testing.T) {
13781583
defer testutil.AfterTest(t)
13791584

0 commit comments

Comments
 (0)