Skip to content

Commit fde8732

Browse files
bootstrap: Use kubeconfig contents as seed for cert dir if necessary
kubeadm uses certificate rotation to replace the initial high-power cert provided in --kubeconfig with a less powerful certificate on the masters. This requires that we pass the contents of the client config certData and keyData down into the cert store to populate the initial client. Add better comments to describe why the flow is required. Add a test that verifies initial cert contents are written to disk. Change the cert manager to not use MustRegister for prometheus so that it can be tested.
1 parent 486577d commit fde8732

File tree

4 files changed

+167
-46
lines changed

4 files changed

+167
-46
lines changed

cmd/kubelet/app/server.go

Lines changed: 41 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -739,6 +739,24 @@ func run(s *options.KubeletServer, kubeDeps *kubelet.Dependencies, stopCh <-chan
739739
// bootstrapping is enabled or client certificate rotation is enabled.
740740
func buildKubeletClientConfig(s *options.KubeletServer, nodeName types.NodeName) (*restclient.Config, func(), error) {
741741
if s.RotateCertificates && utilfeature.DefaultFeatureGate.Enabled(features.RotateKubeletClientCertificate) {
742+
// Rules for client rotation and the handling of kube config files:
743+
//
744+
// 1. If the client provides only a kubeconfig file, we must use that as the initial client
745+
// kubeadm needs the initial data in the kubeconfig to be placed into the cert store
746+
// 2. If the client provides only an initial bootstrap kubeconfig file, we must create a
747+
// kubeconfig file at the target location that points to the cert store, but until
748+
// the file is present the client config will have no certs
749+
// 3. If the client provides both and the kubeconfig is valid, we must ignore the bootstrap
750+
// kubeconfig.
751+
// 4. If the client provides both and the kubeconfig is expired or otherwise invalid, we must
752+
// replace the kubeconfig with a new file that points to the cert dir
753+
//
754+
// The desired configuration for bootstrapping is to use a bootstrap kubeconfig and to have
755+
// the kubeconfig file be managed by this process. For backwards compatibility with kubeadm,
756+
// which provides a high powered kubeconfig on the master with cert/key data, we must
757+
// bootstrap the cert manager with the contents of the initial client config.
758+
759+
klog.Infof("Client rotation is on, will bootstrap in background")
742760
certConfig, clientConfig, err := bootstrap.LoadClientConfig(s.KubeConfig, s.BootstrapKubeconfig, s.CertDirectory)
743761
if err != nil {
744762
return nil, nil, err
@@ -750,9 +768,8 @@ func buildKubeletClientConfig(s *options.KubeletServer, nodeName types.NodeName)
750768
}
751769

752770
// the rotating transport will use the cert from the cert manager instead of these files
753-
transportConfig := restclient.CopyConfig(clientConfig)
754-
transportConfig.CertFile = ""
755-
transportConfig.KeyFile = ""
771+
transportConfig := restclient.AnonymousClientConfig(clientConfig)
772+
kubeClientConfigOverrides(s, transportConfig)
756773

757774
// we set exitAfter to five minutes because we use this client configuration to request new certs - if we are unable
758775
// to request new certs, we will be unable to continue normal operation. Exiting the process allows a wrapper
@@ -774,10 +791,16 @@ func buildKubeletClientConfig(s *options.KubeletServer, nodeName types.NodeName)
774791
}
775792
}
776793

777-
clientConfig, err := createAPIServerClientConfig(s)
794+
clientConfig, err := clientcmd.NewNonInteractiveDeferredLoadingClientConfig(
795+
&clientcmd.ClientConfigLoadingRules{ExplicitPath: s.KubeConfig},
796+
&clientcmd.ConfigOverrides{},
797+
).ClientConfig()
778798
if err != nil {
779799
return nil, nil, fmt.Errorf("invalid kubeconfig: %v", err)
780800
}
801+
802+
kubeClientConfigOverrides(s, clientConfig)
803+
781804
return clientConfig, nil, nil
782805
}
783806

@@ -804,12 +827,26 @@ func buildClientCertificateManager(certConfig, clientConfig *restclient.Config,
804827
return kubeletcertificate.NewKubeletClientCertificateManager(
805828
certDir,
806829
nodeName,
830+
831+
// this preserves backwards compatibility with kubeadm which passes
832+
// a high powered certificate to the kubelet as --kubeconfig and expects
833+
// it to be rotated out immediately
834+
clientConfig.CertData,
835+
clientConfig.KeyData,
836+
807837
clientConfig.CertFile,
808838
clientConfig.KeyFile,
809839
newClientFn,
810840
)
811841
}
812842

843+
func kubeClientConfigOverrides(s *options.KubeletServer, clientConfig *restclient.Config) {
844+
clientConfig.ContentType = s.ContentType
845+
// Override kubeconfig qps/burst settings from flags
846+
clientConfig.QPS = float32(s.KubeAPIQPS)
847+
clientConfig.Burst = int(s.KubeAPIBurst)
848+
}
849+
813850
// getNodeName returns the node name according to the cloud provider
814851
// if cloud provider is specified. Otherwise, returns the hostname of the node.
815852
func getNodeName(cloud cloudprovider.Interface, hostname string) (types.NodeName, error) {
@@ -898,38 +935,6 @@ func InitializeTLS(kf *options.KubeletFlags, kc *kubeletconfiginternal.KubeletCo
898935
return tlsOptions, nil
899936
}
900937

901-
func kubeconfigClientConfig(s *options.KubeletServer) (*restclient.Config, error) {
902-
return clientcmd.NewNonInteractiveDeferredLoadingClientConfig(
903-
&clientcmd.ClientConfigLoadingRules{ExplicitPath: s.KubeConfig},
904-
&clientcmd.ConfigOverrides{},
905-
).ClientConfig()
906-
}
907-
908-
// createClientConfig creates a client configuration from the command line arguments.
909-
// If --kubeconfig is explicitly set, it will be used.
910-
func createClientConfig(s *options.KubeletServer) (*restclient.Config, error) {
911-
if len(s.BootstrapKubeconfig) > 0 || len(s.KubeConfig) > 0 {
912-
return kubeconfigClientConfig(s)
913-
}
914-
return nil, fmt.Errorf("createClientConfig called in standalone mode")
915-
}
916-
917-
// createAPIServerClientConfig generates a client.Config from command line flags
918-
// via createClientConfig and then injects chaos into the configuration via addChaosToClientConfig.
919-
func createAPIServerClientConfig(s *options.KubeletServer) (*restclient.Config, error) {
920-
clientConfig, err := createClientConfig(s)
921-
if err != nil {
922-
return nil, err
923-
}
924-
925-
clientConfig.ContentType = s.ContentType
926-
// Override kubeconfig qps/burst settings from flags
927-
clientConfig.QPS = float32(s.KubeAPIQPS)
928-
clientConfig.Burst = int(s.KubeAPIBurst)
929-
930-
return clientConfig, nil
931-
}
932-
933938
// RunKubelet is responsible for setting up and running a kubelet. It is used in three different applications:
934939
// 1 Integration tests
935940
// 2 Kubelet binary

cmd/kubelet/app/server_bootstrap_test.go

Lines changed: 98 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,10 +19,13 @@ package app
1919
import (
2020
"crypto/ecdsa"
2121
"crypto/elliptic"
22-
cryptorand "crypto/rand"
22+
"crypto/rand"
2323
"crypto/x509"
24+
"crypto/x509/pkix"
2425
"encoding/json"
26+
"encoding/pem"
2527
"io/ioutil"
28+
"math/big"
2629
"net/http"
2730
"net/http/httptest"
2831
"os"
@@ -53,7 +56,7 @@ func Test_buildClientCertificateManager(t *testing.T) {
5356
}
5457
defer func() { os.RemoveAll(testDir) }()
5558

56-
serverPrivateKey, err := ecdsa.GenerateKey(elliptic.P256(), cryptorand.Reader)
59+
serverPrivateKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
5760
if err != nil {
5861
t.Fatal(err)
5962
}
@@ -132,6 +135,65 @@ func Test_buildClientCertificateManager(t *testing.T) {
132135
}
133136
}
134137

138+
func Test_buildClientCertificateManager_populateCertDir(t *testing.T) {
139+
testDir, err := ioutil.TempDir("", "kubeletcert")
140+
if err != nil {
141+
t.Fatal(err)
142+
}
143+
defer func() { os.RemoveAll(testDir) }()
144+
145+
// when no cert is provided, write nothing to disk
146+
config1 := &restclient.Config{
147+
UserAgent: "FirstClient",
148+
Host: "http://localhost",
149+
}
150+
config2 := &restclient.Config{
151+
UserAgent: "SecondClient",
152+
Host: "http://localhost",
153+
}
154+
nodeName := types.NodeName("test")
155+
if _, err := buildClientCertificateManager(config1, config2, testDir, nodeName); err != nil {
156+
t.Fatal(err)
157+
}
158+
fi := getFileInfo(testDir)
159+
if len(fi) != 0 {
160+
t.Fatalf("Unexpected directory contents: %#v", fi)
161+
}
162+
163+
// an invalid cert should be ignored
164+
config2.CertData = []byte("invalid contents")
165+
config2.KeyData = []byte("invalid contents")
166+
if _, err := buildClientCertificateManager(config1, config2, testDir, nodeName); err == nil {
167+
t.Fatal("unexpected non error")
168+
}
169+
fi = getFileInfo(testDir)
170+
if len(fi) != 0 {
171+
t.Fatalf("Unexpected directory contents: %#v", fi)
172+
}
173+
174+
// an expired client certificate should be written to disk, because the cert manager can
175+
// use config1 to refresh it and the cert manager won't return it for clients.
176+
config2.CertData, config2.KeyData = genClientCert(t, time.Now().Add(-2*time.Hour), time.Now().Add(-time.Hour))
177+
if _, err := buildClientCertificateManager(config1, config2, testDir, nodeName); err != nil {
178+
t.Fatal(err)
179+
}
180+
fi = getFileInfo(testDir)
181+
if len(fi) != 2 {
182+
t.Fatalf("Unexpected directory contents: %#v", fi)
183+
}
184+
185+
// a valid, non-expired client certificate should be written to disk
186+
config2.CertData, config2.KeyData = genClientCert(t, time.Now().Add(-time.Hour), time.Now().Add(24*time.Hour))
187+
if _, err := buildClientCertificateManager(config1, config2, testDir, nodeName); err != nil {
188+
t.Fatal(err)
189+
}
190+
fi = getFileInfo(testDir)
191+
if len(fi) != 2 {
192+
t.Fatalf("Unexpected directory contents: %#v", fi)
193+
}
194+
195+
}
196+
135197
func getFileInfo(dir string) map[string]os.FileInfo {
136198
fi := make(map[string]os.FileInfo)
137199
filepath.Walk(dir, func(path string, info os.FileInfo, err error) error {
@@ -279,3 +341,37 @@ func (s *csrSimulator) ServeHTTP(w http.ResponseWriter, req *http.Request) {
279341
t.Fatalf("unexpected request: %s %s", req.Method, req.URL)
280342
}
281343
}
344+
345+
// genClientCert generates an x509 certificate for testing. Certificate and key
346+
// are returned in PEM encoding.
347+
func genClientCert(t *testing.T, from, to time.Time) ([]byte, []byte) {
348+
key, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
349+
if err != nil {
350+
t.Fatal(err)
351+
}
352+
keyRaw, err := x509.MarshalECPrivateKey(key)
353+
if err != nil {
354+
t.Fatal(err)
355+
}
356+
serialNumberLimit := new(big.Int).Lsh(big.NewInt(1), 128)
357+
serialNumber, err := rand.Int(rand.Reader, serialNumberLimit)
358+
if err != nil {
359+
t.Fatal(err)
360+
}
361+
cert := &x509.Certificate{
362+
SerialNumber: serialNumber,
363+
Subject: pkix.Name{Organization: []string{"Acme Co"}},
364+
NotBefore: from,
365+
NotAfter: to,
366+
367+
KeyUsage: x509.KeyUsageKeyEncipherment | x509.KeyUsageDigitalSignature,
368+
ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth},
369+
BasicConstraintsValid: true,
370+
}
371+
certRaw, err := x509.CreateCertificate(rand.Reader, cert, cert, key.Public(), key)
372+
if err != nil {
373+
t.Fatal(err)
374+
}
375+
return pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: certRaw}),
376+
pem.EncodeToMemory(&pem.Block{Type: "PRIVATE KEY", Bytes: keyRaw})
377+
}

pkg/kubelet/certificate/bootstrap/bootstrap.go

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -59,15 +59,16 @@ func LoadClientConfig(kubeconfigPath, bootstrapPath, certDir string) (certConfig
5959
if err != nil {
6060
return nil, nil, fmt.Errorf("unable to load kubeconfig: %v", err)
6161
}
62-
return clientConfig, clientConfig, nil
62+
klog.V(2).Infof("No bootstrapping requested, will use kubeconfig")
63+
return clientConfig, restclient.CopyConfig(clientConfig), nil
6364
}
6465

6566
store, err := certificate.NewFileStore("kubelet-client", certDir, certDir, "", "")
6667
if err != nil {
6768
return nil, nil, fmt.Errorf("unable to build bootstrap cert store")
6869
}
6970

70-
ok, err := verifyBootstrapClientConfig(kubeconfigPath)
71+
ok, err := isClientConfigStillValid(kubeconfigPath)
7172
if err != nil {
7273
return nil, nil, err
7374
}
@@ -78,7 +79,8 @@ func LoadClientConfig(kubeconfigPath, bootstrapPath, certDir string) (certConfig
7879
if err != nil {
7980
return nil, nil, fmt.Errorf("unable to load kubeconfig: %v", err)
8081
}
81-
return clientConfig, clientConfig, nil
82+
klog.V(2).Infof("Current kubeconfig file contents are still valid, no bootstrap necessary")
83+
return clientConfig, restclient.CopyConfig(clientConfig), nil
8284
}
8385

8486
bootstrapClientConfig, err := loadRESTClientConfig(bootstrapPath)
@@ -93,6 +95,7 @@ func LoadClientConfig(kubeconfigPath, bootstrapPath, certDir string) (certConfig
9395
if err := writeKubeconfigFromBootstrapping(clientConfig, kubeconfigPath, pemPath); err != nil {
9496
return nil, nil, err
9597
}
98+
klog.V(2).Infof("Use the bootstrap credentials to request a cert, and set kubeconfig to point to the certificate dir")
9699
return bootstrapClientConfig, clientConfig, nil
97100
}
98101

@@ -102,7 +105,7 @@ func LoadClientConfig(kubeconfigPath, bootstrapPath, certDir string) (certConfig
102105
// The certificate and key file are stored in certDir.
103106
func LoadClientCert(kubeconfigPath, bootstrapPath, certDir string, nodeName types.NodeName) error {
104107
// Short-circuit if the kubeconfig file exists and is valid.
105-
ok, err := verifyBootstrapClientConfig(kubeconfigPath)
108+
ok, err := isClientConfigStillValid(kubeconfigPath)
106109
if err != nil {
107110
return err
108111
}
@@ -219,10 +222,10 @@ func loadRESTClientConfig(kubeconfig string) (*restclient.Config, error) {
219222
).ClientConfig()
220223
}
221224

222-
// verifyBootstrapClientConfig checks the provided kubeconfig to see if it has a valid
225+
// isClientConfigStillValid checks the provided kubeconfig to see if it has a valid
223226
// client certificate. It returns true if the kubeconfig is valid, or an error if bootstrapping
224227
// should stop immediately.
225-
func verifyBootstrapClientConfig(kubeconfigPath string) (bool, error) {
228+
func isClientConfigStillValid(kubeconfigPath string) (bool, error) {
226229
_, err := os.Stat(kubeconfigPath)
227230
if os.IsNotExist(err) {
228231
return false, nil

pkg/kubelet/certificate/kubelet.go

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -147,7 +147,16 @@ func addressesToHostnamesAndIPs(addresses []v1.NodeAddress) (dnsNames []string,
147147
// NewKubeletClientCertificateManager sets up a certificate manager without a
148148
// client that can be used to sign new certificates (or rotate). If a CSR
149149
// client is set later, it may begin rotating/renewing the client cert.
150-
func NewKubeletClientCertificateManager(certDirectory string, nodeName types.NodeName, certFile string, keyFile string, clientFn certificate.CSRClientFunc) (certificate.Manager, error) {
150+
func NewKubeletClientCertificateManager(
151+
certDirectory string,
152+
nodeName types.NodeName,
153+
bootstrapCertData []byte,
154+
bootstrapKeyData []byte,
155+
certFile string,
156+
keyFile string,
157+
clientFn certificate.CSRClientFunc,
158+
) (certificate.Manager, error) {
159+
151160
certificateStore, err := certificate.NewFileStore(
152161
"kubelet-client",
153162
certDirectory,
@@ -165,7 +174,7 @@ func NewKubeletClientCertificateManager(certDirectory string, nodeName types.Nod
165174
Help: "Gauge of the lifetime of a certificate. The value is the date the certificate will expire in seconds since January 1, 1970 UTC.",
166175
},
167176
)
168-
prometheus.MustRegister(certificateExpiration)
177+
prometheus.Register(certificateExpiration)
169178

170179
m, err := certificate.NewManager(&certificate.Config{
171180
ClientFn: clientFn,
@@ -190,6 +199,14 @@ func NewKubeletClientCertificateManager(certDirectory string, nodeName types.Nod
190199
// authenticate itself to the TLS server.
191200
certificates.UsageClientAuth,
192201
},
202+
203+
// For backwards compatibility, the kubelet supports the ability to
204+
// provide a higher privileged certificate as initial data that will
205+
// then be rotated immediately. This code path is used by kubeadm on
206+
// the masters.
207+
BootstrapCertificatePEM: bootstrapCertData,
208+
BootstrapKeyPEM: bootstrapKeyData,
209+
193210
CertificateStore: certificateStore,
194211
CertificateExpiration: certificateExpiration,
195212
})

0 commit comments

Comments
 (0)