Skip to content

Commit e702c2e

Browse files
authored
Merge pull request kubernetes#124574 from zhangweikop/master
enable kubelet server to dynamically load tls certificate files
2 parents c8a51aa + af2b0bd commit e702c2e

File tree

4 files changed

+258
-9
lines changed

4 files changed

+258
-9
lines changed

pkg/features/kube_features.go

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -640,6 +640,14 @@ const (
640640
// Allow almost all printable ASCII characters in environment variables
641641
RelaxedEnvironmentVariableValidation featuregate.Feature = "RelaxedEnvironmentVariableValidation"
642642

643+
// owner: @zhangweikop
644+
// beta: v1.31
645+
//
646+
// Enable kubelet tls server to update certificate if the specified certificate files are changed.
647+
// This feature is useful when specifying tlsCertFile & tlsPrivateKeyFile in kubelet Configuration.
648+
// No effect for other cases such as using serverTLSbootstap.
649+
ReloadKubeletServerCertificateFile featuregate.Feature = "ReloadKubeletServerCertificateFile"
650+
643651
// owner: @mikedanese
644652
// alpha: v1.7
645653
// beta: v1.12
@@ -1117,6 +1125,8 @@ var defaultKubernetesFeatureGates = map[featuregate.Feature]featuregate.FeatureS
11171125

11181126
RelaxedEnvironmentVariableValidation: {Default: false, PreRelease: featuregate.Alpha},
11191127

1128+
ReloadKubeletServerCertificateFile: {Default: true, PreRelease: featuregate.Beta},
1129+
11201130
RotateKubeletServerCertificate: {Default: true, PreRelease: featuregate.Beta},
11211131

11221132
RuntimeClassInImageCriAPI: {Default: false, PreRelease: featuregate.Alpha},

pkg/kubelet/certificate/kubelet.go

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,22 +17,26 @@ limitations under the License.
1717
package certificate
1818

1919
import (
20+
"context"
2021
"crypto/tls"
2122
"crypto/x509"
2223
"crypto/x509/pkix"
2324
"fmt"
2425
"math"
2526
"net"
2627
"sort"
28+
"sync/atomic"
2729
"time"
2830

2931
certificates "k8s.io/api/certificates/v1"
3032
v1 "k8s.io/api/core/v1"
3133
"k8s.io/apimachinery/pkg/types"
34+
"k8s.io/apiserver/pkg/server/dynamiccertificates"
3235
clientset "k8s.io/client-go/kubernetes"
3336
"k8s.io/client-go/util/certificate"
3437
compbasemetrics "k8s.io/component-base/metrics"
3538
"k8s.io/component-base/metrics/legacyregistry"
39+
"k8s.io/klog/v2"
3640
kubeletconfig "k8s.io/kubernetes/pkg/kubelet/apis/config"
3741
"k8s.io/kubernetes/pkg/kubelet/metrics"
3842
netutils "k8s.io/utils/net"
@@ -234,3 +238,66 @@ func NewKubeletClientCertificateManager(
234238

235239
return m, nil
236240
}
241+
242+
// NewKubeletServerCertificateDynamicFileManager creates a certificate manager based on reading and watching certificate and key files.
243+
// The returned struct implements certificate.Manager interface, enabling using it like other CertificateManager in this package.
244+
// But the struct doesn't communicate with API server to perform certificate request at all.
245+
func NewKubeletServerCertificateDynamicFileManager(certFile, keyFile string) (certificate.Manager, error) {
246+
c, err := dynamiccertificates.NewDynamicServingContentFromFiles("kubelet-server-cert-files", certFile, keyFile)
247+
if err != nil {
248+
return nil, fmt.Errorf("unable to set up dynamic certificate manager for kubelet server cert files: %w", err)
249+
}
250+
m := &kubeletServerCertificateDynamicFileManager{
251+
dynamicCertificateContent: c,
252+
certFile: certFile,
253+
keyFile: keyFile,
254+
}
255+
m.Enqueue()
256+
c.AddListener(m)
257+
return m, nil
258+
}
259+
260+
// kubeletServerCertificateDynamicFileManager uses a dynamic CertKeyContentProvider based on cert and key files.
261+
type kubeletServerCertificateDynamicFileManager struct {
262+
cancelFn context.CancelFunc
263+
certFile string
264+
keyFile string
265+
dynamicCertificateContent *dynamiccertificates.DynamicCertKeyPairContent
266+
currentTLSCertificate atomic.Pointer[tls.Certificate]
267+
}
268+
269+
// Enqueue implements the functions to be notified when the serving cert content changes.
270+
func (m *kubeletServerCertificateDynamicFileManager) Enqueue() {
271+
certContent, keyContent := m.dynamicCertificateContent.CurrentCertKeyContent()
272+
cert, err := tls.X509KeyPair(certContent, keyContent)
273+
if err != nil {
274+
klog.ErrorS(err, "invalid certificate and key pair from file", "certFile", m.certFile, "keyFile", m.keyFile)
275+
return
276+
}
277+
m.currentTLSCertificate.Store(&cert)
278+
klog.V(4).InfoS("loaded certificate and key pair in kubelet server certificate manager", "certFile", m.certFile, "keyFile", m.keyFile)
279+
}
280+
281+
// Current returns the last valid certificate key pair loaded from files.
282+
func (m *kubeletServerCertificateDynamicFileManager) Current() *tls.Certificate {
283+
return m.currentTLSCertificate.Load()
284+
}
285+
286+
// Start starts watching the certificate and key files
287+
func (m *kubeletServerCertificateDynamicFileManager) Start() {
288+
var ctx context.Context
289+
ctx, m.cancelFn = context.WithCancel(context.Background())
290+
go m.dynamicCertificateContent.Run(ctx, 1)
291+
}
292+
293+
// Stop stops watching the certificate and key files
294+
func (m *kubeletServerCertificateDynamicFileManager) Stop() {
295+
if m.cancelFn != nil {
296+
m.cancelFn()
297+
}
298+
}
299+
300+
// ServerHealthy always returns true since the file manager doesn't communicate with any server
301+
func (m *kubeletServerCertificateDynamicFileManager) ServerHealthy() bool {
302+
return true
303+
}

pkg/kubelet/certificate/kubelet_test.go

Lines changed: 161 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,11 +17,19 @@ limitations under the License.
1717
package certificate
1818

1919
import (
20+
"bytes"
21+
"context"
22+
"fmt"
2023
"net"
24+
"os"
25+
"path/filepath"
2126
"reflect"
2227
"testing"
28+
"time"
2329

2430
v1 "k8s.io/api/core/v1"
31+
"k8s.io/apimachinery/pkg/util/wait"
32+
"k8s.io/client-go/util/cert"
2533
netutils "k8s.io/utils/net"
2634
)
2735

@@ -100,3 +108,156 @@ func TestAddressesToHostnamesAndIPs(t *testing.T) {
100108
})
101109
}
102110
}
111+
112+
func removeThenCreate(name string, data []byte, perm os.FileMode) error {
113+
if err := os.Remove(name); err != nil {
114+
if !os.IsNotExist(err) {
115+
return err
116+
}
117+
}
118+
return os.WriteFile(name, data, perm)
119+
}
120+
121+
func createCertAndKeyFiles(certDir string) (string, string, error) {
122+
cert, key, err := cert.GenerateSelfSignedCertKey("k8s.io", nil, nil)
123+
if err != nil {
124+
return "", "", nil
125+
}
126+
127+
certPath := filepath.Join(certDir, "kubelet.cert")
128+
keyPath := filepath.Join(certDir, "kubelet.key")
129+
if err := removeThenCreate(certPath, cert, os.FileMode(0644)); err != nil {
130+
return "", "", err
131+
}
132+
133+
if err := removeThenCreate(keyPath, key, os.FileMode(0600)); err != nil {
134+
return "", "", err
135+
}
136+
137+
return certPath, keyPath, nil
138+
}
139+
140+
// createCertAndKeyFilesUsingRename creates cert and key files under a parent dir `identity` as
141+
// <certDir>/identity/kubelet.cert, <certDir>/identity/kubelet.key
142+
func createCertAndKeyFilesUsingRename(certDir string) (string, string, error) {
143+
cert, key, err := cert.GenerateSelfSignedCertKey("k8s.io", nil, nil)
144+
if err != nil {
145+
return "", "", nil
146+
}
147+
148+
var certKeyPathFn = func(dataDir string) (string, string, string) {
149+
outputDir := filepath.Join(certDir, dataDir)
150+
return outputDir, filepath.Join(outputDir, "kubelet.cert"), filepath.Join(outputDir, "kubelet.key")
151+
}
152+
153+
writeDir, writeCertPath, writeKeyPath := certKeyPathFn("identity.tmp")
154+
if err := os.Mkdir(writeDir, 0777); err != nil {
155+
return "", "", err
156+
}
157+
158+
if err := removeThenCreate(writeCertPath, cert, os.FileMode(0644)); err != nil {
159+
return "", "", err
160+
}
161+
162+
if err := removeThenCreate(writeKeyPath, key, os.FileMode(0600)); err != nil {
163+
return "", "", err
164+
}
165+
166+
targetDir, certPath, keyPath := certKeyPathFn("identity")
167+
if err := os.RemoveAll(targetDir); err != nil {
168+
if !os.IsNotExist(err) {
169+
return "", "", err
170+
}
171+
}
172+
if err := os.Rename(writeDir, targetDir); err != nil {
173+
return "", "", err
174+
}
175+
176+
return certPath, keyPath, nil
177+
}
178+
179+
func TestKubeletServerCertificateFromFiles(t *testing.T) {
180+
// test two common ways of certificate file updates:
181+
// 1. delete and write the cert and key files directly
182+
// 2. create the cert and key files under a child dir and perform dir rename during update
183+
tests := []struct {
184+
name string
185+
useRename bool
186+
}{
187+
{
188+
name: "remove and create",
189+
useRename: false,
190+
},
191+
{
192+
name: "rename cert dir",
193+
useRename: true,
194+
},
195+
}
196+
197+
for _, tt := range tests {
198+
t.Run(tt.name, func(t *testing.T) {
199+
createFn := createCertAndKeyFiles
200+
if tt.useRename {
201+
createFn = createCertAndKeyFilesUsingRename
202+
}
203+
204+
certDir := t.TempDir()
205+
certPath, keyPath, err := createFn(certDir)
206+
if err != nil {
207+
t.Fatalf("Unable to setup cert files: %v", err)
208+
}
209+
210+
m, err := NewKubeletServerCertificateDynamicFileManager(certPath, keyPath)
211+
if err != nil {
212+
t.Fatalf("Unable to create certificte provider: %v", err)
213+
}
214+
215+
m.Start()
216+
defer m.Stop()
217+
218+
c := m.Current()
219+
if c == nil {
220+
t.Fatal("failed to provide valid certificate")
221+
}
222+
time.Sleep(100 * time.Millisecond)
223+
c2 := m.Current()
224+
if c2 == nil {
225+
t.Fatal("failed to provide valid certificate")
226+
}
227+
if c2 != c {
228+
t.Errorf("expected the same loaded certificate object when there is no cert file change, got different")
229+
}
230+
231+
// simulate certificate files updated in the background
232+
if _, _, err := createFn(certDir); err != nil {
233+
t.Fatalf("got errors when rotating certificate files in the test: %v", err)
234+
}
235+
236+
err = wait.PollUntilContextTimeout(context.Background(),
237+
100*time.Millisecond, 10*time.Second, true,
238+
func(_ context.Context) (bool, error) {
239+
c3 := m.Current()
240+
if c3 == nil {
241+
return false, fmt.Errorf("expected valid certificate regardless of file changes, but got nil")
242+
}
243+
if bytes.Equal(c.Certificate[0], c3.Certificate[0]) {
244+
t.Logf("loaded certificate is not updated")
245+
return false, nil
246+
}
247+
return true, nil
248+
})
249+
if err != nil {
250+
t.Errorf("failed to provide the updated certificate after file changes: %v", err)
251+
}
252+
253+
if err = os.Remove(certPath); err != nil {
254+
t.Errorf("could not delete file in order to perform test")
255+
}
256+
257+
time.Sleep(1 * time.Second)
258+
if m.Current() == nil {
259+
t.Errorf("expected the manager still provides cached content when certificate file was not available")
260+
}
261+
})
262+
}
263+
}

pkg/kubelet/kubelet.go

Lines changed: 20 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -781,17 +781,28 @@ func NewMainKubelet(kubeCfg *kubeletconfiginternal.KubeletConfiguration,
781781
}
782782
klet.imageManager = imageManager
783783

784-
if kubeCfg.ServerTLSBootstrap && kubeDeps.TLSOptions != nil && utilfeature.DefaultFeatureGate.Enabled(features.RotateKubeletServerCertificate) {
785-
klet.serverCertificateManager, err = kubeletcertificate.NewKubeletServerCertificateManager(klet.kubeClient, kubeCfg, klet.nodeName, klet.getLastObservedNodeAddresses, certDirectory)
786-
if err != nil {
787-
return nil, fmt.Errorf("failed to initialize certificate manager: %v", err)
784+
if kubeDeps.TLSOptions != nil {
785+
if kubeCfg.ServerTLSBootstrap && utilfeature.DefaultFeatureGate.Enabled(features.RotateKubeletServerCertificate) {
786+
klet.serverCertificateManager, err = kubeletcertificate.NewKubeletServerCertificateManager(klet.kubeClient, kubeCfg, klet.nodeName, klet.getLastObservedNodeAddresses, certDirectory)
787+
if err != nil {
788+
return nil, fmt.Errorf("failed to initialize certificate manager: %w", err)
789+
}
790+
791+
} else if kubeDeps.TLSOptions.CertFile != "" && kubeDeps.TLSOptions.KeyFile != "" && utilfeature.DefaultFeatureGate.Enabled(features.ReloadKubeletServerCertificateFile) {
792+
klet.serverCertificateManager, err = kubeletcertificate.NewKubeletServerCertificateDynamicFileManager(kubeDeps.TLSOptions.CertFile, kubeDeps.TLSOptions.KeyFile)
793+
if err != nil {
794+
return nil, fmt.Errorf("failed to initialize file based certificate manager: %w", err)
795+
}
788796
}
789-
kubeDeps.TLSOptions.Config.GetCertificate = func(*tls.ClientHelloInfo) (*tls.Certificate, error) {
790-
cert := klet.serverCertificateManager.Current()
791-
if cert == nil {
792-
return nil, fmt.Errorf("no serving certificate available for the kubelet")
797+
798+
if klet.serverCertificateManager != nil {
799+
kubeDeps.TLSOptions.Config.GetCertificate = func(*tls.ClientHelloInfo) (*tls.Certificate, error) {
800+
cert := klet.serverCertificateManager.Current()
801+
if cert == nil {
802+
return nil, fmt.Errorf("no serving certificate available for the kubelet")
803+
}
804+
return cert, nil
793805
}
794-
return cert, nil
795806
}
796807
}
797808

0 commit comments

Comments
 (0)