Skip to content

Commit ceadf12

Browse files
Merge pull request #17135 from smarterclayton/refresh_transport
Automatic merge from submit-queue. 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.
2 parents 12f0ec1 + 097350e commit ceadf12

File tree

3 files changed

+259
-1
lines changed

3 files changed

+259
-1
lines changed

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

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,16 +3,22 @@ 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"
11+
utilwait "k8s.io/apimachinery/pkg/util/wait"
912
kclientset "k8s.io/client-go/kubernetes"
13+
"k8s.io/client-go/rest"
1014
"k8s.io/kubernetes/pkg/apis/componentconfig"
1115
kclientsetexternal "k8s.io/kubernetes/pkg/client/clientset_generated/clientset"
1216
kclientsetinternal "k8s.io/kubernetes/pkg/client/clientset_generated/internalclientset"
1317
kinternalinformers "k8s.io/kubernetes/pkg/client/informers/informers_generated/internalversion"
18+
"k8s.io/kubernetes/pkg/kubelet/certificate"
1419

1520
configapi "github.com/openshift/origin/pkg/cmd/server/api"
21+
"github.com/openshift/origin/pkg/cmd/server/kubernetes/network/transport"
1622
"github.com/openshift/origin/pkg/dns"
1723
"github.com/openshift/origin/pkg/network"
1824
networkclient "github.com/openshift/origin/pkg/network/generated/internalclientset"
@@ -42,12 +48,33 @@ type NetworkConfig struct {
4248
SDNProxy network.ProxyInterface
4349
}
4450

51+
// configureKubeConfigForClientCertRotation attempts to watch for client certificate rotation on the kubelet's cert
52+
// dir, if configured. This allows the network component to participate in client cert rotation when it is in the
53+
// same process (since it can't share a client with the Kubelet). This code path will be removed or altered when
54+
// the network process is split into a daemonset.
55+
func configureKubeConfigForClientCertRotation(options configapi.NodeConfig, kubeConfig *rest.Config) error {
56+
v, ok := options.KubeletArguments["cert-dir"]
57+
if !ok || len(v) == 0 {
58+
return nil
59+
}
60+
certDir := v[0]
61+
// equivalent to values in pkg/kubelet/certificate/kubelet.go
62+
store, err := certificate.NewFileStore("kubelet-client", certDir, certDir, kubeConfig.TLSClientConfig.CertFile, kubeConfig.TLSClientConfig.KeyFile)
63+
if err != nil {
64+
return err
65+
}
66+
return transport.RefreshCertificateAfterExpiry(utilwait.NeverStop, 10*time.Second, kubeConfig, store)
67+
}
68+
4569
// New creates a new network config object for running the networking components of the OpenShift node.
4670
func New(options configapi.NodeConfig, clusterDomain string, proxyConfig *componentconfig.KubeProxyConfiguration, enableProxy, enableDNS bool) (*NetworkConfig, error) {
4771
kubeConfig, err := configapi.GetKubeConfigOrInClusterConfig(options.MasterKubeConfig, options.MasterClientConnectionOverrides)
4872
if err != nil {
4973
return nil, err
5074
}
75+
if err := configureKubeConfigForClientCertRotation(options, kubeConfig); err != nil {
76+
utilruntime.HandleError(fmt.Errorf("Unable to enable client certificate rotation for network components: %v", err))
77+
}
5178
internalKubeClient, err := kclientsetinternal.NewForConfig(kubeConfig)
5279
if err != nil {
5380
return nil, err
Lines changed: 224 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,224 @@
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+
utilruntime "k8s.io/apimachinery/pkg/util/runtime"
35+
"k8s.io/apimachinery/pkg/util/wait"
36+
restclient "k8s.io/client-go/rest"
37+
"k8s.io/kubernetes/pkg/kubelet/certificate"
38+
)
39+
40+
// RefreshCertificateAfterExpiry instruments a restconfig with a transport that checks
41+
// disk to reload expired certificates.
42+
//
43+
// The config must not already provide an explicit transport.
44+
//
45+
// The returned transport periodically checks the manager to determine if the
46+
// certificate has changed. If it has, the transport shuts down all existing client
47+
// connections, forcing the client to re-handshake with the server and use the
48+
// new certificate.
49+
//
50+
// stopCh should be used to indicate when the transport is unused and doesn't need
51+
// to continue checking the manager.
52+
func RefreshCertificateAfterExpiry(stopCh <-chan struct{}, period time.Duration, clientConfig *restclient.Config, store certificate.Store) error {
53+
if clientConfig.Transport != nil {
54+
return fmt.Errorf("there is already a transport configured")
55+
}
56+
tlsConfig, err := restclient.TLSConfigFor(clientConfig)
57+
if err != nil {
58+
return fmt.Errorf("unable to configure TLS for the rest client: %v", err)
59+
}
60+
if tlsConfig == nil {
61+
tlsConfig = &tls.Config{}
62+
}
63+
manager := &certificateManager{
64+
store: store,
65+
minimumRefresh: period,
66+
// begin attempting to refresh the certificate before it expires, assuming that rotation occurs before
67+
// expiration
68+
expireWindow: 3 * time.Minute,
69+
}
70+
tlsConfig.Certificates = nil
71+
tlsConfig.GetClientCertificate = func(requestInfo *tls.CertificateRequestInfo) (*tls.Certificate, error) {
72+
cert := manager.Current()
73+
if cert == nil {
74+
return &tls.Certificate{Certificate: nil}, nil
75+
}
76+
return cert, nil
77+
}
78+
79+
// Custom dialer that will track all connections it creates.
80+
t := &connTracker{
81+
dialer: &net.Dialer{Timeout: 30 * time.Second, KeepAlive: 30 * time.Second},
82+
conns: make(map[*closableConn]struct{}),
83+
}
84+
85+
// Watch for certs to change, and then close connections after at least one period has elapsed.
86+
lastCert := manager.Current()
87+
detectedCertChange := false
88+
go wait.Until(func() {
89+
if detectedCertChange {
90+
// To avoid races with new connections getting old certificates after the close happens, wait
91+
// at least period before closing connections.
92+
detectedCertChange = false
93+
glog.Infof("certificate rotation detected, shutting down client connections to start using new credentials")
94+
// The cert has been rotated. Close all existing connections to force the client
95+
// to reperform its TLS handshake with new cert.
96+
//
97+
// See: https://github.com/kubernetes-incubator/bootkube/pull/663#issuecomment-318506493
98+
t.closeAllConns()
99+
}
100+
curr := manager.Current()
101+
if curr == nil || lastCert == curr {
102+
// Cert hasn't been rotated.
103+
return
104+
}
105+
lastCert = curr
106+
detectedCertChange = true
107+
}, period, stopCh)
108+
109+
clientConfig.Transport = utilnet.SetTransportDefaults(&http.Transport{
110+
TLSClientConfig: tlsConfig,
111+
MaxIdleConnsPerHost: 25,
112+
DialContext: t.DialContext, // Use custom dialer.
113+
})
114+
115+
// Zero out all existing TLS options since our new transport enforces them.
116+
clientConfig.CertData = nil
117+
clientConfig.KeyData = nil
118+
clientConfig.CertFile = ""
119+
clientConfig.KeyFile = ""
120+
clientConfig.CAData = nil
121+
clientConfig.CAFile = ""
122+
clientConfig.Insecure = false
123+
return nil
124+
}
125+
126+
// certificateManager reloads the requested certificate from disk when requested.
127+
type certificateManager struct {
128+
store certificate.Store
129+
minimumRefresh time.Duration
130+
expireWindow time.Duration
131+
132+
lock sync.Mutex
133+
cert *tls.Certificate
134+
lastCheck time.Time
135+
}
136+
137+
// Current retrieves the latest certificate from disk if it exists, or nil if
138+
// no certificate could be found. The last successfully loaded certificate will be
139+
// returned.
140+
func (m *certificateManager) Current() *tls.Certificate {
141+
m.lock.Lock()
142+
defer m.lock.Unlock()
143+
144+
// check whether the cert has expired and whether we've waited long enough since our last
145+
// check to look again
146+
cert := m.cert
147+
if cert != nil {
148+
now := time.Now()
149+
if now.After(cert.Leaf.NotAfter.Add(-m.expireWindow)) {
150+
if now.Sub(m.lastCheck) > m.minimumRefresh {
151+
glog.V(2).Infof("Current client certificate is about to expire, checking from disk")
152+
cert = nil
153+
m.lastCheck = now
154+
}
155+
}
156+
}
157+
158+
// load the cert from disk and parse the leaf cert
159+
if cert == nil {
160+
glog.V(2).Infof("Refreshing client certificate from store")
161+
c, err := m.store.Current()
162+
if err != nil {
163+
utilruntime.HandleError(fmt.Errorf("Unable to load client certificate key pair from disk: %v", err))
164+
return nil
165+
}
166+
m.cert = c
167+
}
168+
return m.cert
169+
}
170+
171+
// connTracker is a dialer that tracks all open connections it creates.
172+
type connTracker struct {
173+
dialer *net.Dialer
174+
175+
mu sync.Mutex
176+
conns map[*closableConn]struct{}
177+
}
178+
179+
// closeAllConns forcibly closes all tracked connections.
180+
func (c *connTracker) closeAllConns() {
181+
c.mu.Lock()
182+
conns := c.conns
183+
c.conns = make(map[*closableConn]struct{})
184+
c.mu.Unlock()
185+
186+
for conn := range conns {
187+
conn.Close()
188+
}
189+
}
190+
191+
func (c *connTracker) DialContext(ctx context.Context, network, address string) (net.Conn, error) {
192+
conn, err := c.dialer.DialContext(ctx, network, address)
193+
if err != nil {
194+
return nil, err
195+
}
196+
197+
closable := &closableConn{Conn: conn}
198+
199+
// Start tracking the connection
200+
c.mu.Lock()
201+
c.conns[closable] = struct{}{}
202+
c.mu.Unlock()
203+
204+
// When the connection is closed, remove it from the map. This will
205+
// be no-op if the connection isn't in the map, e.g. if closeAllConns()
206+
// is called.
207+
closable.onClose = func() {
208+
c.mu.Lock()
209+
delete(c.conns, closable)
210+
c.mu.Unlock()
211+
}
212+
213+
return closable, nil
214+
}
215+
216+
type closableConn struct {
217+
onClose func()
218+
net.Conn
219+
}
220+
221+
func (c *closableConn) Close() error {
222+
go c.onClose()
223+
return c.Conn.Close()
224+
}

vendor/k8s.io/kubernetes/pkg/kubelet/certificate/certificate_store.go

Lines changed: 8 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)