Skip to content

Commit 059044f

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

File tree

3 files changed

+272
-0
lines changed

3 files changed

+272
-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

+203
Original file line numberDiff line numberDiff line change
@@ -16,17 +16,22 @@ package integration
1616

1717
import (
1818
"bytes"
19+
"crypto/tls"
1920
"fmt"
21+
"io/ioutil"
2022
"math/rand"
2123
"os"
2224
"reflect"
25+
"strings"
2326
"testing"
2427
"time"
2528

29+
"github.com/coreos/etcd/clientv3"
2630
"github.com/coreos/etcd/etcdserver/api/v3rpc"
2731
"github.com/coreos/etcd/etcdserver/api/v3rpc/rpctypes"
2832
pb "github.com/coreos/etcd/etcdserver/etcdserverpb"
2933
"github.com/coreos/etcd/pkg/testutil"
34+
3035
"golang.org/x/net/context"
3136
"google.golang.org/grpc"
3237
"google.golang.org/grpc/metadata"
@@ -1374,6 +1379,204 @@ func TestTLSGRPCAcceptSecureAll(t *testing.T) {
13741379
}
13751380
}
13761381

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

0 commit comments

Comments
 (0)