Skip to content

Commit 22d681d

Browse files
Network component should refresh certificates if they expire
A single process openshift start node has both kubelet and network. Kubelet rotates its client certs - network does not. Until we split out network we need to do something minimal to ensure the cert is refreshed. Use the same code as the kubelet, but when the cert expires check disk and see if it was refreshed. That should work for bootstrapped environments because the file is updated.
1 parent 7968b96 commit 22d681d

File tree

2 files changed

+207
-0
lines changed

2 files changed

+207
-0
lines changed

pkg/cmd/server/kubernetes/network/network_config.go

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,16 +3,19 @@ package network
33
import (
44
"fmt"
55
"net"
6+
"time"
67

78
miekgdns "github.com/miekg/dns"
89

10+
utilruntime "k8s.io/apimachinery/pkg/util/runtime"
911
kclientset "k8s.io/client-go/kubernetes"
1012
"k8s.io/kubernetes/pkg/apis/componentconfig"
1113
kclientsetexternal "k8s.io/kubernetes/pkg/client/clientset_generated/clientset"
1214
kclientsetinternal "k8s.io/kubernetes/pkg/client/clientset_generated/internalclientset"
1315
kinternalinformers "k8s.io/kubernetes/pkg/client/informers/informers_generated/internalversion"
1416

1517
configapi "github.com/openshift/origin/pkg/cmd/server/api"
18+
"github.com/openshift/origin/pkg/cmd/server/kubernetes/network/transport"
1619
"github.com/openshift/origin/pkg/dns"
1720
"github.com/openshift/origin/pkg/network"
1821
networkclient "github.com/openshift/origin/pkg/network/generated/internalclientset"
@@ -48,6 +51,12 @@ func New(options configapi.NodeConfig, clusterDomain string, proxyConfig *compon
4851
if err != nil {
4952
return nil, err
5053
}
54+
// if a client cert is specified, when the certificate expires attempt to refresh it from disk
55+
if len(kubeConfig.TLSClientConfig.KeyFile) > 0 && len(kubeConfig.TLSClientConfig.CertFile) > 0 {
56+
if err := transport.RefreshCertificateAfterExpiry(nil, 10*time.Second, kubeConfig); err != nil {
57+
utilruntime.HandleError(fmt.Errorf("Unable to enable client certificate rotation for network components: %v", err))
58+
}
59+
}
5160
internalKubeClient, err := kclientsetinternal.NewForConfig(kubeConfig)
5261
if err != nil {
5362
return nil, err
Lines changed: 198 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,198 @@
1+
/*
2+
Copyright 2017 The Kubernetes Authors.
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
17+
// Extracted from k8s.io/kubernetes/pkg/kubelet/certificate/transport.go, will be removed
18+
// when openshift-sdn and the network components move out of the Kubelet. Is intended ONLY
19+
// to provide certificate rollover until 3.8/3.9.
20+
package transport
21+
22+
import (
23+
"context"
24+
"crypto/tls"
25+
"fmt"
26+
"net"
27+
"net/http"
28+
"sync"
29+
"time"
30+
31+
"github.com/golang/glog"
32+
33+
utilnet "k8s.io/apimachinery/pkg/util/net"
34+
"k8s.io/apimachinery/pkg/util/wait"
35+
restclient "k8s.io/client-go/rest"
36+
)
37+
38+
// RefreshCertificateAfterExpiry instruments a restconfig with a transport that checks
39+
// disk to reload expired certificates.
40+
//
41+
// The config must not already provide an explicit transport.
42+
//
43+
// The returned transport periodically checks the manager to determine if the
44+
// certificate has changed. If it has, the transport shuts down all existing client
45+
// connections, forcing the client to re-handshake with the server and use the
46+
// new certificate.
47+
//
48+
// stopCh should be used to indicate when the transport is unused and doesn't need
49+
// to continue checking the manager.
50+
func RefreshCertificateAfterExpiry(stopCh <-chan struct{}, period time.Duration, clientConfig *restclient.Config) error {
51+
if clientConfig.Transport != nil {
52+
return fmt.Errorf("there is already a transport configured")
53+
}
54+
tlsConfig, err := restclient.TLSConfigFor(clientConfig)
55+
if err != nil {
56+
return fmt.Errorf("unable to configure TLS for the rest client: %v", err)
57+
}
58+
if tlsConfig == nil {
59+
tlsConfig = &tls.Config{}
60+
}
61+
manager := &certificateManager{config: *clientConfig, minimumRefresh: period}
62+
tlsConfig.Certificates = nil
63+
tlsConfig.GetClientCertificate = func(requestInfo *tls.CertificateRequestInfo) (*tls.Certificate, error) {
64+
cert := manager.Current()
65+
if cert == nil {
66+
return &tls.Certificate{Certificate: nil}, nil
67+
}
68+
return cert, nil
69+
}
70+
71+
// Custom dialer that will track all connections it creates.
72+
t := &connTracker{
73+
dialer: &net.Dialer{Timeout: 30 * time.Second, KeepAlive: 30 * time.Second},
74+
conns: make(map[*closableConn]struct{}),
75+
}
76+
77+
lastCert := manager.Current()
78+
go wait.Until(func() {
79+
curr := manager.Current()
80+
if curr == nil || lastCert == curr {
81+
// Cert hasn't been rotated.
82+
return
83+
}
84+
lastCert = curr
85+
86+
glog.Infof("certificate rotation detected, shutting down client connections to start using new credentials")
87+
// The cert has been rotated. Close all existing connections to force the client
88+
// to reperform its TLS handshake with new cert.
89+
//
90+
// See: https://github.com/kubernetes-incubator/bootkube/pull/663#issuecomment-318506493
91+
t.closeAllConns()
92+
}, period, stopCh)
93+
94+
clientConfig.Transport = utilnet.SetTransportDefaults(&http.Transport{
95+
Proxy: http.ProxyFromEnvironment,
96+
TLSHandshakeTimeout: 10 * time.Second,
97+
TLSClientConfig: tlsConfig,
98+
MaxIdleConnsPerHost: 25,
99+
DialContext: t.DialContext, // Use custom dialer.
100+
})
101+
102+
// Zero out all existing TLS options since our new transport enforces them.
103+
clientConfig.CertData = nil
104+
clientConfig.KeyData = nil
105+
clientConfig.CertFile = ""
106+
clientConfig.KeyFile = ""
107+
clientConfig.CAData = nil
108+
clientConfig.CAFile = ""
109+
clientConfig.Insecure = false
110+
return nil
111+
}
112+
113+
type certificateManager struct {
114+
config restclient.Config
115+
minimumRefresh time.Duration
116+
117+
lock sync.Mutex
118+
cert *tls.Certificate
119+
lastCheck time.Time
120+
}
121+
122+
func (m *certificateManager) Current() *tls.Certificate {
123+
m.lock.Lock()
124+
defer m.lock.Unlock()
125+
cert := m.cert
126+
if cert != nil {
127+
now := time.Now()
128+
if now.After(m.cert.Leaf.NotAfter) {
129+
if now.Sub(m.lastCheck) > m.minimumRefresh {
130+
cert = nil
131+
m.lastCheck = now
132+
}
133+
}
134+
}
135+
if cert == nil {
136+
c, err := tls.LoadX509KeyPair(m.config.CertFile, m.config.KeyFile)
137+
if err != nil {
138+
return nil
139+
}
140+
m.cert = &c
141+
}
142+
return m.cert
143+
}
144+
145+
// connTracker is a dialer that tracks all open connections it creates.
146+
type connTracker struct {
147+
dialer *net.Dialer
148+
149+
mu sync.Mutex
150+
conns map[*closableConn]struct{}
151+
}
152+
153+
// closeAllConns forcibly closes all tracked connections.
154+
func (c *connTracker) closeAllConns() {
155+
c.mu.Lock()
156+
conns := c.conns
157+
c.conns = make(map[*closableConn]struct{})
158+
c.mu.Unlock()
159+
160+
for conn := range conns {
161+
conn.Close()
162+
}
163+
}
164+
165+
func (c *connTracker) DialContext(ctx context.Context, network, address string) (net.Conn, error) {
166+
conn, err := c.dialer.DialContext(ctx, network, address)
167+
if err != nil {
168+
return nil, err
169+
}
170+
171+
closable := &closableConn{Conn: conn}
172+
173+
// Start tracking the connection
174+
c.mu.Lock()
175+
c.conns[closable] = struct{}{}
176+
c.mu.Unlock()
177+
178+
// When the connection is closed, remove it from the map. This will
179+
// be no-op if the connection isn't in the map, e.g. if closeAllConns()
180+
// is called.
181+
closable.onClose = func() {
182+
c.mu.Lock()
183+
delete(c.conns, closable)
184+
c.mu.Unlock()
185+
}
186+
187+
return closable, nil
188+
}
189+
190+
type closableConn struct {
191+
onClose func()
192+
net.Conn
193+
}
194+
195+
func (c *closableConn) Close() error {
196+
go c.onClose()
197+
return c.Conn.Close()
198+
}

0 commit comments

Comments
 (0)