Merge pull request #71174 from smarterclayton/debug_kubeadm

Restore bootstrap in the background with fix to preserve kubeadm behavior
pull/564/head
Kubernetes Prow Robot 2018-12-03 05:49:57 -08:00 committed by GitHub
commit dc9261bc3b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 759 additions and 173 deletions

View File

@ -8,8 +8,22 @@ load(
go_test( go_test(
name = "go_default_test", name = "go_default_test",
srcs = ["server_test.go"], srcs = [
"server_bootstrap_test.go",
"server_test.go",
],
embed = [":go_default_library"], embed = [":go_default_library"],
deps = [
"//staging/src/k8s.io/api/certificates/v1beta1:go_default_library",
"//staging/src/k8s.io/apimachinery/pkg/apis/meta/v1:go_default_library",
"//staging/src/k8s.io/apimachinery/pkg/runtime:go_default_library",
"//staging/src/k8s.io/apimachinery/pkg/types:go_default_library",
"//staging/src/k8s.io/client-go/rest:go_default_library",
"//staging/src/k8s.io/client-go/util/cert:go_default_library",
"//vendor/github.com/cloudflare/cfssl/config:go_default_library",
"//vendor/github.com/cloudflare/cfssl/signer:go_default_library",
"//vendor/github.com/cloudflare/cfssl/signer/local:go_default_library",
],
) )
go_library( go_library(
@ -113,6 +127,7 @@ go_library(
"//staging/src/k8s.io/client-go/kubernetes:go_default_library", "//staging/src/k8s.io/client-go/kubernetes:go_default_library",
"//staging/src/k8s.io/client-go/kubernetes/typed/authentication/v1beta1:go_default_library", "//staging/src/k8s.io/client-go/kubernetes/typed/authentication/v1beta1:go_default_library",
"//staging/src/k8s.io/client-go/kubernetes/typed/authorization/v1beta1:go_default_library", "//staging/src/k8s.io/client-go/kubernetes/typed/authorization/v1beta1:go_default_library",
"//staging/src/k8s.io/client-go/kubernetes/typed/certificates/v1beta1:go_default_library",
"//staging/src/k8s.io/client-go/kubernetes/typed/core/v1:go_default_library", "//staging/src/k8s.io/client-go/kubernetes/typed/core/v1:go_default_library",
"//staging/src/k8s.io/client-go/rest:go_default_library", "//staging/src/k8s.io/client-go/rest:go_default_library",
"//staging/src/k8s.io/client-go/tools/clientcmd:go_default_library", "//staging/src/k8s.io/client-go/tools/clientcmd:go_default_library",

View File

@ -50,6 +50,7 @@ import (
"k8s.io/apiserver/pkg/util/flag" "k8s.io/apiserver/pkg/util/flag"
"k8s.io/client-go/dynamic" "k8s.io/client-go/dynamic"
clientset "k8s.io/client-go/kubernetes" clientset "k8s.io/client-go/kubernetes"
certificatesclient "k8s.io/client-go/kubernetes/typed/certificates/v1beta1"
v1core "k8s.io/client-go/kubernetes/typed/core/v1" v1core "k8s.io/client-go/kubernetes/typed/core/v1"
restclient "k8s.io/client-go/rest" restclient "k8s.io/client-go/rest"
"k8s.io/client-go/tools/clientcmd" "k8s.io/client-go/tools/clientcmd"
@ -537,66 +538,39 @@ func run(s *options.KubeletServer, kubeDeps *kubelet.Dependencies, stopCh <-chan
return err return err
} }
if s.BootstrapKubeconfig != "" {
if err := bootstrap.LoadClientCert(s.KubeConfig, s.BootstrapKubeconfig, s.CertDirectory, nodeName); err != nil {
return err
}
}
// if in standalone mode, indicate as much by setting all clients to nil // if in standalone mode, indicate as much by setting all clients to nil
if standaloneMode { switch {
case standaloneMode:
kubeDeps.KubeClient = nil kubeDeps.KubeClient = nil
kubeDeps.DynamicKubeClient = nil kubeDeps.DynamicKubeClient = nil
kubeDeps.EventClient = nil kubeDeps.EventClient = nil
kubeDeps.HeartbeatClient = nil kubeDeps.HeartbeatClient = nil
klog.Warningf("standalone mode, no API client") klog.Warningf("standalone mode, no API client")
} else if kubeDeps.KubeClient == nil || kubeDeps.EventClient == nil || kubeDeps.HeartbeatClient == nil || kubeDeps.DynamicKubeClient == nil {
// initialize clients if not standalone mode and any of the clients are not provided
var kubeClient clientset.Interface
var eventClient v1core.EventsGetter
var heartbeatClient clientset.Interface
var dynamicKubeClient dynamic.Interface
clientConfig, err := createAPIServerClientConfig(s) case kubeDeps.KubeClient == nil, kubeDeps.EventClient == nil, kubeDeps.HeartbeatClient == nil, kubeDeps.DynamicKubeClient == nil:
if err != nil { clientConfig, closeAllConns, err := buildKubeletClientConfig(s, nodeName)
return fmt.Errorf("invalid kubeconfig: %v", err)
}
var clientCertificateManager certificate.Manager
if s.RotateCertificates && utilfeature.DefaultFeatureGate.Enabled(features.RotateKubeletClientCertificate) {
clientCertificateManager, err = kubeletcertificate.NewKubeletClientCertificateManager(s.CertDirectory, nodeName, clientConfig.CertData, clientConfig.KeyData, clientConfig.CertFile, clientConfig.KeyFile)
if err != nil {
return err
}
}
// we set exitAfter to five minutes because we use this client configuration to request new certs - if we are unable
// to request new certs, we will be unable to continue normal operation. Exiting the process allows a wrapper
// or the bootstrapping credentials to potentially lay down new initial config.
closeAllConns, err := kubeletcertificate.UpdateTransport(wait.NeverStop, clientConfig, clientCertificateManager, 5*time.Minute)
if err != nil { if err != nil {
return err return err
} }
kubeDeps.OnHeartbeatFailure = closeAllConns
kubeClient, err = clientset.NewForConfig(clientConfig) kubeDeps.KubeClient, err = clientset.NewForConfig(clientConfig)
if err != nil { if err != nil {
klog.Warningf("New kubeClient from clientConfig error: %v", err) return fmt.Errorf("failed to initialize kubelet client: %v", err)
} else if kubeClient.CertificatesV1beta1() != nil && clientCertificateManager != nil {
klog.V(2).Info("Starting client certificate rotation.")
clientCertificateManager.SetCertificateSigningRequestClient(kubeClient.CertificatesV1beta1().CertificateSigningRequests())
clientCertificateManager.Start()
} }
dynamicKubeClient, err = dynamic.NewForConfig(clientConfig)
kubeDeps.DynamicKubeClient, err = dynamic.NewForConfig(clientConfig)
if err != nil { if err != nil {
klog.Warningf("Failed to initialize dynamic KubeClient: %v", err) return fmt.Errorf("failed to initialize kubelet dynamic client: %v", err)
} }
// make a separate client for events // make a separate client for events
eventClientConfig := *clientConfig eventClientConfig := *clientConfig
eventClientConfig.QPS = float32(s.EventRecordQPS) eventClientConfig.QPS = float32(s.EventRecordQPS)
eventClientConfig.Burst = int(s.EventBurst) eventClientConfig.Burst = int(s.EventBurst)
eventClient, err = v1core.NewForConfig(&eventClientConfig) kubeDeps.EventClient, err = v1core.NewForConfig(&eventClientConfig)
if err != nil { if err != nil {
klog.Warningf("Failed to create API Server client for Events: %v", err) return fmt.Errorf("failed to initialize kubelet event client: %v", err)
} }
// make a separate client for heartbeat with throttling disabled and a timeout attached // make a separate client for heartbeat with throttling disabled and a timeout attached
@ -610,28 +584,18 @@ func run(s *options.KubeletServer, kubeDeps *kubelet.Dependencies, stopCh <-chan
} }
} }
heartbeatClientConfig.QPS = float32(-1) heartbeatClientConfig.QPS = float32(-1)
heartbeatClient, err = clientset.NewForConfig(&heartbeatClientConfig) kubeDeps.HeartbeatClient, err = clientset.NewForConfig(&heartbeatClientConfig)
if err != nil { if err != nil {
klog.Warningf("Failed to create API Server client for heartbeat: %v", err) return fmt.Errorf("failed to initialize kubelet heartbeat client: %v", err)
} }
// csiClient works with CRDs that support json only // CRDs are JSON only, and client renegotiation for streaming is not correct as per #67803
clientConfig.ContentType = "application/json" csiClientConfig := restclient.CopyConfig(clientConfig)
csiClient, err := csiclientset.NewForConfig(clientConfig) csiClientConfig.ContentType = "application/json"
kubeDeps.CSIClient, err = csiclientset.NewForConfig(csiClientConfig)
if err != nil { if err != nil {
klog.Warningf("Failed to create CSI API client: %v", err) return fmt.Errorf("failed to initialize kubelet storage client: %v", err)
} }
kubeDeps.KubeClient = kubeClient
kubeDeps.DynamicKubeClient = dynamicKubeClient
if heartbeatClient != nil {
kubeDeps.HeartbeatClient = heartbeatClient
kubeDeps.OnHeartbeatFailure = closeAllConns
}
if eventClient != nil {
kubeDeps.EventClient = eventClient
}
kubeDeps.CSIClient = csiClient
} }
// If the kubelet config controller is available, and dynamic config is enabled, start the config and status sync loops // If the kubelet config controller is available, and dynamic config is enabled, start the config and status sync loops
@ -771,6 +735,118 @@ func run(s *options.KubeletServer, kubeDeps *kubelet.Dependencies, stopCh <-chan
return nil return nil
} }
// buildKubeletClientConfig constructs the appropriate client config for the kubelet depending on whether
// bootstrapping is enabled or client certificate rotation is enabled.
func buildKubeletClientConfig(s *options.KubeletServer, nodeName types.NodeName) (*restclient.Config, func(), error) {
if s.RotateCertificates && utilfeature.DefaultFeatureGate.Enabled(features.RotateKubeletClientCertificate) {
// Rules for client rotation and the handling of kube config files:
//
// 1. If the client provides only a kubeconfig file, we must use that as the initial client
// kubeadm needs the initial data in the kubeconfig to be placed into the cert store
// 2. If the client provides only an initial bootstrap kubeconfig file, we must create a
// kubeconfig file at the target location that points to the cert store, but until
// the file is present the client config will have no certs
// 3. If the client provides both and the kubeconfig is valid, we must ignore the bootstrap
// kubeconfig.
// 4. If the client provides both and the kubeconfig is expired or otherwise invalid, we must
// replace the kubeconfig with a new file that points to the cert dir
//
// The desired configuration for bootstrapping is to use a bootstrap kubeconfig and to have
// the kubeconfig file be managed by this process. For backwards compatibility with kubeadm,
// which provides a high powered kubeconfig on the master with cert/key data, we must
// bootstrap the cert manager with the contents of the initial client config.
klog.Infof("Client rotation is on, will bootstrap in background")
certConfig, clientConfig, err := bootstrap.LoadClientConfig(s.KubeConfig, s.BootstrapKubeconfig, s.CertDirectory)
if err != nil {
return nil, nil, err
}
clientCertificateManager, err := buildClientCertificateManager(certConfig, clientConfig, s.CertDirectory, nodeName)
if err != nil {
return nil, nil, err
}
// the rotating transport will use the cert from the cert manager instead of these files
transportConfig := restclient.AnonymousClientConfig(clientConfig)
kubeClientConfigOverrides(s, transportConfig)
// we set exitAfter to five minutes because we use this client configuration to request new certs - if we are unable
// to request new certs, we will be unable to continue normal operation. Exiting the process allows a wrapper
// or the bootstrapping credentials to potentially lay down new initial config.
closeAllConns, err := kubeletcertificate.UpdateTransport(wait.NeverStop, transportConfig, clientCertificateManager, 5*time.Minute)
if err != nil {
return nil, nil, err
}
klog.V(2).Info("Starting client certificate rotation.")
clientCertificateManager.Start()
return transportConfig, closeAllConns, nil
}
if len(s.BootstrapKubeconfig) > 0 {
if err := bootstrap.LoadClientCert(s.KubeConfig, s.BootstrapKubeconfig, s.CertDirectory, nodeName); err != nil {
return nil, nil, err
}
}
clientConfig, err := clientcmd.NewNonInteractiveDeferredLoadingClientConfig(
&clientcmd.ClientConfigLoadingRules{ExplicitPath: s.KubeConfig},
&clientcmd.ConfigOverrides{},
).ClientConfig()
if err != nil {
return nil, nil, fmt.Errorf("invalid kubeconfig: %v", err)
}
kubeClientConfigOverrides(s, clientConfig)
return clientConfig, nil, nil
}
// buildClientCertificateManager creates a certificate manager that will use certConfig to request a client certificate
// if no certificate is available, or the most recent clientConfig (which is assumed to point to the cert that the manager will
// write out).
func buildClientCertificateManager(certConfig, clientConfig *restclient.Config, certDir string, nodeName types.NodeName) (certificate.Manager, error) {
newClientFn := func(current *tls.Certificate) (certificatesclient.CertificateSigningRequestInterface, error) {
// If we have a valid certificate, use that to fetch CSRs. Otherwise use the bootstrap
// credentials. In the future it would be desirable to change the behavior of bootstrap
// to always fall back to the external bootstrap credentials when such credentials are
// provided by a fundamental trust system like cloud VM identity or an HSM module.
config := certConfig
if current != nil {
config = clientConfig
}
client, err := clientset.NewForConfig(config)
if err != nil {
return nil, err
}
return client.CertificatesV1beta1().CertificateSigningRequests(), nil
}
return kubeletcertificate.NewKubeletClientCertificateManager(
certDir,
nodeName,
// this preserves backwards compatibility with kubeadm which passes
// a high powered certificate to the kubelet as --kubeconfig and expects
// it to be rotated out immediately
clientConfig.CertData,
clientConfig.KeyData,
clientConfig.CertFile,
clientConfig.KeyFile,
newClientFn,
)
}
func kubeClientConfigOverrides(s *options.KubeletServer, clientConfig *restclient.Config) {
clientConfig.ContentType = s.ContentType
// Override kubeconfig qps/burst settings from flags
clientConfig.QPS = float32(s.KubeAPIQPS)
clientConfig.Burst = int(s.KubeAPIBurst)
}
// getNodeName returns the node name according to the cloud provider // getNodeName returns the node name according to the cloud provider
// if cloud provider is specified. Otherwise, returns the hostname of the node. // if cloud provider is specified. Otherwise, returns the hostname of the node.
func getNodeName(cloud cloudprovider.Interface, hostname string) (types.NodeName, error) { func getNodeName(cloud cloudprovider.Interface, hostname string) (types.NodeName, error) {
@ -859,39 +935,6 @@ func InitializeTLS(kf *options.KubeletFlags, kc *kubeletconfiginternal.KubeletCo
return tlsOptions, nil return tlsOptions, nil
} }
func kubeconfigClientConfig(s *options.KubeletServer) (*restclient.Config, error) {
return clientcmd.NewNonInteractiveDeferredLoadingClientConfig(
&clientcmd.ClientConfigLoadingRules{ExplicitPath: s.KubeConfig},
&clientcmd.ConfigOverrides{},
).ClientConfig()
}
// createClientConfig creates a client configuration from the command line arguments.
// If --kubeconfig is explicitly set, it will be used.
func createClientConfig(s *options.KubeletServer) (*restclient.Config, error) {
if s.BootstrapKubeconfig != "" || len(s.KubeConfig) > 0 {
return kubeconfigClientConfig(s)
} else {
return nil, fmt.Errorf("createClientConfig called in standalone mode")
}
}
// createAPIServerClientConfig generates a client.Config from command line flags
// via createClientConfig and then injects chaos into the configuration via addChaosToClientConfig.
func createAPIServerClientConfig(s *options.KubeletServer) (*restclient.Config, error) {
clientConfig, err := createClientConfig(s)
if err != nil {
return nil, err
}
clientConfig.ContentType = s.ContentType
// Override kubeconfig qps/burst settings from flags
clientConfig.QPS = float32(s.KubeAPIQPS)
clientConfig.Burst = int(s.KubeAPIBurst)
return clientConfig, nil
}
// RunKubelet is responsible for setting up and running a kubelet. It is used in three different applications: // RunKubelet is responsible for setting up and running a kubelet. It is used in three different applications:
// 1 Integration tests // 1 Integration tests
// 2 Kubelet binary // 2 Kubelet binary

View File

@ -0,0 +1,377 @@
/*
Copyright 2016 The Kubernetes Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package app
import (
"crypto/ecdsa"
"crypto/elliptic"
"crypto/rand"
"crypto/x509"
"crypto/x509/pkix"
"encoding/json"
"encoding/pem"
"io/ioutil"
"math/big"
"net/http"
"net/http/httptest"
"os"
"path/filepath"
"sync"
"testing"
"time"
cfsslconfig "github.com/cloudflare/cfssl/config"
cfsslsigner "github.com/cloudflare/cfssl/signer"
cfssllocal "github.com/cloudflare/cfssl/signer/local"
certapi "k8s.io/api/certificates/v1beta1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/types"
restclient "k8s.io/client-go/rest"
certutil "k8s.io/client-go/util/cert"
)
// Test_buildClientCertificateManager validates that we can build a local client cert
// manager that will use the bootstrap client until we get a valid cert, then use our
// provided identity on subsequent requests.
func Test_buildClientCertificateManager(t *testing.T) {
testDir, err := ioutil.TempDir("", "kubeletcert")
if err != nil {
t.Fatal(err)
}
defer func() { os.RemoveAll(testDir) }()
serverPrivateKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
if err != nil {
t.Fatal(err)
}
serverCA, err := certutil.NewSelfSignedCACert(certutil.Config{
CommonName: "the-test-framework",
}, serverPrivateKey)
if err != nil {
t.Fatal(err)
}
server := &csrSimulator{
t: t,
serverPrivateKey: serverPrivateKey,
serverCA: serverCA,
}
s := httptest.NewServer(server)
defer s.Close()
config1 := &restclient.Config{
UserAgent: "FirstClient",
Host: s.URL,
}
config2 := &restclient.Config{
UserAgent: "SecondClient",
Host: s.URL,
}
nodeName := types.NodeName("test")
m, err := buildClientCertificateManager(config1, config2, testDir, nodeName)
if err != nil {
t.Fatal(err)
}
defer m.Stop()
r := m.(rotater)
// get an expired CSR (simulating historical output)
server.backdate = 2 * time.Hour
server.expectUserAgent = "FirstClient"
ok, err := r.RotateCerts()
if !ok || err != nil {
t.Fatalf("unexpected rotation err: %t %v", ok, err)
}
if cert := m.Current(); cert != nil {
t.Fatalf("Unexpected cert, should be expired: %#v", cert)
}
fi := getFileInfo(testDir)
if len(fi) != 2 {
t.Fatalf("Unexpected directory contents: %#v", fi)
}
// if m.Current() == nil, then we try again and get a valid
// client
server.backdate = 0
server.expectUserAgent = "FirstClient"
if ok, err := r.RotateCerts(); !ok || err != nil {
t.Fatalf("unexpected rotation err: %t %v", ok, err)
}
if cert := m.Current(); cert == nil {
t.Fatalf("Unexpected cert, should be valid: %#v", cert)
}
fi = getFileInfo(testDir)
if len(fi) != 2 {
t.Fatalf("Unexpected directory contents: %#v", fi)
}
// if m.Current() != nil, then we should use the second client
server.expectUserAgent = "SecondClient"
if ok, err := r.RotateCerts(); !ok || err != nil {
t.Fatalf("unexpected rotation err: %t %v", ok, err)
}
if cert := m.Current(); cert == nil {
t.Fatalf("Unexpected cert, should be valid: %#v", cert)
}
fi = getFileInfo(testDir)
if len(fi) != 2 {
t.Fatalf("Unexpected directory contents: %#v", fi)
}
}
func Test_buildClientCertificateManager_populateCertDir(t *testing.T) {
testDir, err := ioutil.TempDir("", "kubeletcert")
if err != nil {
t.Fatal(err)
}
defer func() { os.RemoveAll(testDir) }()
// when no cert is provided, write nothing to disk
config1 := &restclient.Config{
UserAgent: "FirstClient",
Host: "http://localhost",
}
config2 := &restclient.Config{
UserAgent: "SecondClient",
Host: "http://localhost",
}
nodeName := types.NodeName("test")
if _, err := buildClientCertificateManager(config1, config2, testDir, nodeName); err != nil {
t.Fatal(err)
}
fi := getFileInfo(testDir)
if len(fi) != 0 {
t.Fatalf("Unexpected directory contents: %#v", fi)
}
// an invalid cert should be ignored
config2.CertData = []byte("invalid contents")
config2.KeyData = []byte("invalid contents")
if _, err := buildClientCertificateManager(config1, config2, testDir, nodeName); err == nil {
t.Fatal("unexpected non error")
}
fi = getFileInfo(testDir)
if len(fi) != 0 {
t.Fatalf("Unexpected directory contents: %#v", fi)
}
// an expired client certificate should be written to disk, because the cert manager can
// use config1 to refresh it and the cert manager won't return it for clients.
config2.CertData, config2.KeyData = genClientCert(t, time.Now().Add(-2*time.Hour), time.Now().Add(-time.Hour))
if _, err := buildClientCertificateManager(config1, config2, testDir, nodeName); err != nil {
t.Fatal(err)
}
fi = getFileInfo(testDir)
if len(fi) != 2 {
t.Fatalf("Unexpected directory contents: %#v", fi)
}
// a valid, non-expired client certificate should be written to disk
config2.CertData, config2.KeyData = genClientCert(t, time.Now().Add(-time.Hour), time.Now().Add(24*time.Hour))
if _, err := buildClientCertificateManager(config1, config2, testDir, nodeName); err != nil {
t.Fatal(err)
}
fi = getFileInfo(testDir)
if len(fi) != 2 {
t.Fatalf("Unexpected directory contents: %#v", fi)
}
}
func getFileInfo(dir string) map[string]os.FileInfo {
fi := make(map[string]os.FileInfo)
filepath.Walk(dir, func(path string, info os.FileInfo, err error) error {
if path == dir {
return nil
}
fi[path] = info
if !info.IsDir() {
os.Remove(path)
}
return nil
})
return fi
}
type rotater interface {
RotateCerts() (bool, error)
}
func getCSR(req *http.Request) (*certapi.CertificateSigningRequest, error) {
if req.Body == nil {
return nil, nil
}
body, err := ioutil.ReadAll(req.Body)
if err != nil {
return nil, err
}
csr := &certapi.CertificateSigningRequest{}
if err := json.Unmarshal(body, csr); err != nil {
return nil, err
}
return csr, nil
}
func mustMarshal(obj interface{}) []byte {
data, err := json.Marshal(obj)
if err != nil {
panic(err)
}
return data
}
type csrSimulator struct {
t *testing.T
serverPrivateKey *ecdsa.PrivateKey
serverCA *x509.Certificate
backdate time.Duration
expectUserAgent string
lock sync.Mutex
csr *certapi.CertificateSigningRequest
}
func (s *csrSimulator) ServeHTTP(w http.ResponseWriter, req *http.Request) {
s.lock.Lock()
defer s.lock.Unlock()
t := s.t
t.Logf("Request %s %s %s", req.Method, req.URL, req.UserAgent())
if len(s.expectUserAgent) > 0 && req.UserAgent() != s.expectUserAgent {
t.Errorf("Unexpected user agent: %s", req.UserAgent())
}
switch {
case req.Method == "POST" && req.URL.Path == "/apis/certificates.k8s.io/v1beta1/certificatesigningrequests":
csr, err := getCSR(req)
if err != nil {
t.Fatal(err)
}
if csr.Name == "" {
csr.Name = "test-csr"
}
csr.UID = types.UID("1")
csr.ResourceVersion = "1"
data := mustMarshal(csr)
w.Header().Set("Content-Type", "application/json")
w.Write(data)
csr = csr.DeepCopy()
csr.ResourceVersion = "2"
var usages []string
for _, usage := range csr.Spec.Usages {
usages = append(usages, string(usage))
}
policy := &cfsslconfig.Signing{
Default: &cfsslconfig.SigningProfile{
Usage: usages,
Expiry: time.Hour,
ExpiryString: time.Hour.String(),
Backdate: s.backdate,
},
}
cfs, err := cfssllocal.NewSigner(s.serverPrivateKey, s.serverCA, cfsslsigner.DefaultSigAlgo(s.serverPrivateKey), policy)
if err != nil {
t.Fatal(err)
}
csr.Status.Certificate, err = cfs.Sign(cfsslsigner.SignRequest{
Request: string(csr.Spec.Request),
})
if err != nil {
t.Fatal(err)
}
csr.Status.Conditions = []certapi.CertificateSigningRequestCondition{
{Type: certapi.CertificateApproved},
}
s.csr = csr
case req.Method == "GET" && req.URL.Path == "/apis/certificates.k8s.io/v1beta1/certificatesigningrequests" && req.URL.RawQuery == "fieldSelector=metadata.name%3Dtest-csr&limit=500":
if s.csr == nil {
t.Fatalf("no csr")
}
csr := s.csr.DeepCopy()
data := mustMarshal(&certapi.CertificateSigningRequestList{
ListMeta: metav1.ListMeta{
ResourceVersion: "2",
},
Items: []certapi.CertificateSigningRequest{
*csr,
},
})
w.Header().Set("Content-Type", "application/json")
w.Write(data)
case req.Method == "GET" && req.URL.Path == "/apis/certificates.k8s.io/v1beta1/certificatesigningrequests" && req.URL.RawQuery == "fieldSelector=metadata.name%3Dtest-csr&resourceVersion=2&watch=true":
if s.csr == nil {
t.Fatalf("no csr")
}
csr := s.csr.DeepCopy()
data := mustMarshal(&metav1.WatchEvent{
Type: "ADDED",
Object: runtime.RawExtension{
Raw: mustMarshal(csr),
},
})
w.Header().Set("Content-Type", "application/json")
w.Write(data)
default:
t.Fatalf("unexpected request: %s %s", req.Method, req.URL)
}
}
// genClientCert generates an x509 certificate for testing. Certificate and key
// are returned in PEM encoding.
func genClientCert(t *testing.T, from, to time.Time) ([]byte, []byte) {
key, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
if err != nil {
t.Fatal(err)
}
keyRaw, err := x509.MarshalECPrivateKey(key)
if err != nil {
t.Fatal(err)
}
serialNumberLimit := new(big.Int).Lsh(big.NewInt(1), 128)
serialNumber, err := rand.Int(rand.Reader, serialNumberLimit)
if err != nil {
t.Fatal(err)
}
cert := &x509.Certificate{
SerialNumber: serialNumber,
Subject: pkix.Name{Organization: []string{"Acme Co"}},
NotBefore: from,
NotAfter: to,
KeyUsage: x509.KeyUsageKeyEncipherment | x509.KeyUsageDigitalSignature,
ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth},
BasicConstraintsValid: true,
}
certRaw, err := x509.CreateCertificate(rand.Reader, cert, cert, key.Public(), key)
if err != nil {
t.Fatal(err)
}
return pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: certRaw}),
pem.EncodeToMemory(&pem.Block{Type: "PRIVATE KEY", Bytes: keyRaw})
}

View File

@ -47,13 +47,65 @@ import (
const tmpPrivateKeyFile = "kubelet-client.key.tmp" const tmpPrivateKeyFile = "kubelet-client.key.tmp"
// LoadClientConfig tries to load the appropriate client config for retrieving certs and for use by users.
// If bootstrapPath is empty, only kubeconfigPath is checked. If bootstrap path is set and the contents
// of kubeconfigPath are valid, both certConfig and userConfig will point to that file. Otherwise the
// kubeconfigPath on disk is populated based on bootstrapPath but pointing to the location of the client cert
// in certDir. This preserves the historical behavior of bootstrapping where on subsequent restarts the
// most recent client cert is used to request new client certs instead of the initial token.
func LoadClientConfig(kubeconfigPath, bootstrapPath, certDir string) (certConfig, userConfig *restclient.Config, err error) {
if len(bootstrapPath) == 0 {
clientConfig, err := loadRESTClientConfig(kubeconfigPath)
if err != nil {
return nil, nil, fmt.Errorf("unable to load kubeconfig: %v", err)
}
klog.V(2).Infof("No bootstrapping requested, will use kubeconfig")
return clientConfig, restclient.CopyConfig(clientConfig), nil
}
store, err := certificate.NewFileStore("kubelet-client", certDir, certDir, "", "")
if err != nil {
return nil, nil, fmt.Errorf("unable to build bootstrap cert store")
}
ok, err := isClientConfigStillValid(kubeconfigPath)
if err != nil {
return nil, nil, err
}
// use the current client config
if ok {
clientConfig, err := loadRESTClientConfig(kubeconfigPath)
if err != nil {
return nil, nil, fmt.Errorf("unable to load kubeconfig: %v", err)
}
klog.V(2).Infof("Current kubeconfig file contents are still valid, no bootstrap necessary")
return clientConfig, restclient.CopyConfig(clientConfig), nil
}
bootstrapClientConfig, err := loadRESTClientConfig(bootstrapPath)
if err != nil {
return nil, nil, fmt.Errorf("unable to load bootstrap kubeconfig: %v", err)
}
clientConfig := restclient.AnonymousClientConfig(bootstrapClientConfig)
pemPath := store.CurrentPath()
clientConfig.KeyFile = pemPath
clientConfig.CertFile = pemPath
if err := writeKubeconfigFromBootstrapping(clientConfig, kubeconfigPath, pemPath); err != nil {
return nil, nil, err
}
klog.V(2).Infof("Use the bootstrap credentials to request a cert, and set kubeconfig to point to the certificate dir")
return bootstrapClientConfig, clientConfig, nil
}
// LoadClientCert requests a client cert for kubelet if the kubeconfigPath file does not exist. // LoadClientCert requests a client cert for kubelet if the kubeconfigPath file does not exist.
// The kubeconfig at bootstrapPath is used to request a client certificate from the API server. // The kubeconfig at bootstrapPath is used to request a client certificate from the API server.
// On success, a kubeconfig file referencing the generated key and obtained certificate is written to kubeconfigPath. // On success, a kubeconfig file referencing the generated key and obtained certificate is written to kubeconfigPath.
// The certificate and key file are stored in certDir. // The certificate and key file are stored in certDir.
func LoadClientCert(kubeconfigPath string, bootstrapPath string, certDir string, nodeName types.NodeName) error { func LoadClientCert(kubeconfigPath, bootstrapPath, certDir string, nodeName types.NodeName) error {
// Short-circuit if the kubeconfig file exists and is valid. // Short-circuit if the kubeconfig file exists and is valid.
ok, err := verifyBootstrapClientConfig(kubeconfigPath) ok, err := isClientConfigStillValid(kubeconfigPath)
if err != nil { if err != nil {
return err return err
} }
@ -117,8 +169,10 @@ func LoadClientCert(kubeconfigPath string, bootstrapPath string, certDir string,
klog.V(2).Infof("failed cleaning up private key file %q: %v", privKeyPath, err) klog.V(2).Infof("failed cleaning up private key file %q: %v", privKeyPath, err)
} }
pemPath := store.CurrentPath() return writeKubeconfigFromBootstrapping(bootstrapClientConfig, kubeconfigPath, store.CurrentPath())
}
func writeKubeconfigFromBootstrapping(bootstrapClientConfig *restclient.Config, kubeconfigPath, pemPath string) error {
// Get the CA data from the bootstrap client config. // Get the CA data from the bootstrap client config.
caFile, caData := bootstrapClientConfig.CAFile, []byte{} caFile, caData := bootstrapClientConfig.CAFile, []byte{}
if len(caFile) == 0 { if len(caFile) == 0 {
@ -168,10 +222,10 @@ func loadRESTClientConfig(kubeconfig string) (*restclient.Config, error) {
).ClientConfig() ).ClientConfig()
} }
// verifyBootstrapClientConfig checks the provided kubeconfig to see if it has a valid // isClientConfigStillValid checks the provided kubeconfig to see if it has a valid
// client certificate. It returns true if the kubeconfig is valid, or an error if bootstrapping // client certificate. It returns true if the kubeconfig is valid, or an error if bootstrapping
// should stop immediately. // should stop immediately.
func verifyBootstrapClientConfig(kubeconfigPath string) (bool, error) { func isClientConfigStillValid(kubeconfigPath string) (bool, error) {
_, err := os.Stat(kubeconfigPath) _, err := os.Stat(kubeconfigPath)
if os.IsNotExist(err) { if os.IsNotExist(err) {
return false, nil return false, nil

View File

@ -17,6 +17,7 @@ limitations under the License.
package certificate package certificate
import ( import (
"crypto/tls"
"crypto/x509" "crypto/x509"
"crypto/x509/pkix" "crypto/x509/pkix"
"fmt" "fmt"
@ -29,7 +30,7 @@ import (
"k8s.io/api/core/v1" "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/types" "k8s.io/apimachinery/pkg/types"
clientset "k8s.io/client-go/kubernetes" clientset "k8s.io/client-go/kubernetes"
clientcertificates "k8s.io/client-go/kubernetes/typed/certificates/v1beta1" certificatesclient "k8s.io/client-go/kubernetes/typed/certificates/v1beta1"
"k8s.io/client-go/util/certificate" "k8s.io/client-go/util/certificate"
kubeletconfig "k8s.io/kubernetes/pkg/kubelet/apis/config" kubeletconfig "k8s.io/kubernetes/pkg/kubelet/apis/config"
"k8s.io/kubernetes/pkg/kubelet/metrics" "k8s.io/kubernetes/pkg/kubelet/metrics"
@ -38,7 +39,7 @@ import (
// NewKubeletServerCertificateManager creates a certificate manager for the kubelet when retrieving a server certificate // NewKubeletServerCertificateManager creates a certificate manager for the kubelet when retrieving a server certificate
// or returns an error. // or returns an error.
func NewKubeletServerCertificateManager(kubeClient clientset.Interface, kubeCfg *kubeletconfig.KubeletConfiguration, nodeName types.NodeName, getAddresses func() []v1.NodeAddress, certDirectory string) (certificate.Manager, error) { func NewKubeletServerCertificateManager(kubeClient clientset.Interface, kubeCfg *kubeletconfig.KubeletConfiguration, nodeName types.NodeName, getAddresses func() []v1.NodeAddress, certDirectory string) (certificate.Manager, error) {
var certSigningRequestClient clientcertificates.CertificateSigningRequestInterface var certSigningRequestClient certificatesclient.CertificateSigningRequestInterface
if kubeClient != nil && kubeClient.CertificatesV1beta1() != nil { if kubeClient != nil && kubeClient.CertificatesV1beta1() != nil {
certSigningRequestClient = kubeClient.CertificatesV1beta1().CertificateSigningRequests() certSigningRequestClient = kubeClient.CertificatesV1beta1().CertificateSigningRequests()
} }
@ -78,8 +79,10 @@ func NewKubeletServerCertificateManager(kubeClient clientset.Interface, kubeCfg
} }
m, err := certificate.NewManager(&certificate.Config{ m, err := certificate.NewManager(&certificate.Config{
CertificateSigningRequestClient: certSigningRequestClient, ClientFn: func(current *tls.Certificate) (certificatesclient.CertificateSigningRequestInterface, error) {
GetTemplate: getTemplate, return certSigningRequestClient, nil
},
GetTemplate: getTemplate,
Usages: []certificates.KeyUsage{ Usages: []certificates.KeyUsage{
// https://tools.ietf.org/html/rfc5280#section-4.2.1.3 // https://tools.ietf.org/html/rfc5280#section-4.2.1.3
// //
@ -142,10 +145,18 @@ func addressesToHostnamesAndIPs(addresses []v1.NodeAddress) (dnsNames []string,
} }
// NewKubeletClientCertificateManager sets up a certificate manager without a // NewKubeletClientCertificateManager sets up a certificate manager without a
// client that can be used to sign new certificates (or rotate). It answers with // client that can be used to sign new certificates (or rotate). If a CSR
// whatever certificate it is initialized with. If a CSR client is set later, it // client is set later, it may begin rotating/renewing the client cert.
// may begin rotating/renewing the client cert func NewKubeletClientCertificateManager(
func NewKubeletClientCertificateManager(certDirectory string, nodeName types.NodeName, certData []byte, keyData []byte, certFile string, keyFile string) (certificate.Manager, error) { certDirectory string,
nodeName types.NodeName,
bootstrapCertData []byte,
bootstrapKeyData []byte,
certFile string,
keyFile string,
clientFn certificate.CSRClientFunc,
) (certificate.Manager, error) {
certificateStore, err := certificate.NewFileStore( certificateStore, err := certificate.NewFileStore(
"kubelet-client", "kubelet-client",
certDirectory, certDirectory,
@ -163,9 +174,10 @@ func NewKubeletClientCertificateManager(certDirectory string, nodeName types.Nod
Help: "Gauge of the lifetime of a certificate. The value is the date the certificate will expire in seconds since January 1, 1970 UTC.", Help: "Gauge of the lifetime of a certificate. The value is the date the certificate will expire in seconds since January 1, 1970 UTC.",
}, },
) )
prometheus.MustRegister(certificateExpiration) prometheus.Register(certificateExpiration)
m, err := certificate.NewManager(&certificate.Config{ m, err := certificate.NewManager(&certificate.Config{
ClientFn: clientFn,
Template: &x509.CertificateRequest{ Template: &x509.CertificateRequest{
Subject: pkix.Name{ Subject: pkix.Name{
CommonName: fmt.Sprintf("system:node:%s", nodeName), CommonName: fmt.Sprintf("system:node:%s", nodeName),
@ -187,10 +199,16 @@ func NewKubeletClientCertificateManager(certDirectory string, nodeName types.Nod
// authenticate itself to the TLS server. // authenticate itself to the TLS server.
certificates.UsageClientAuth, certificates.UsageClientAuth,
}, },
CertificateStore: certificateStore,
BootstrapCertificatePEM: certData, // For backwards compatibility, the kubelet supports the ability to
BootstrapKeyPEM: keyData, // provide a higher privileged certificate as initial data that will
CertificateExpiration: certificateExpiration, // then be rotated immediately. This code path is used by kubeadm on
// the masters.
BootstrapCertificatePEM: bootstrapCertData,
BootstrapKeyPEM: bootstrapKeyData,
CertificateStore: certificateStore,
CertificateExpiration: certificateExpiration,
}) })
if err != nil { if err != nil {
return nil, fmt.Errorf("failed to initialize client certificate manager: %v", err) return nil, fmt.Errorf("failed to initialize client certificate manager: %v", err)

View File

@ -124,7 +124,9 @@ func (f *fakeManager) SetCertificateSigningRequestClient(certificatesclient.Cert
func (f *fakeManager) ServerHealthy() bool { return f.healthy } func (f *fakeManager) ServerHealthy() bool { return f.healthy }
func (f *fakeManager) Start() {} func (f *fakeManager) Start() {}
func (f *fakeManager) Stop() {}
func (f *fakeManager) RotateCerts() (bool, error) { return false, nil }
func (f *fakeManager) Current() *tls.Certificate { func (f *fakeManager) Current() *tls.Certificate {
if val := f.cert.Load(); val != nil { if val := f.cert.Load(); val != nil {

View File

@ -48,11 +48,10 @@ var certificateWaitBackoff = wait.Backoff{Duration: 30 * time.Second, Steps: 4,
// manager. In the background it communicates with the API server to get new // manager. In the background it communicates with the API server to get new
// certificates for certificates about to expire. // certificates for certificates about to expire.
type Manager interface { type Manager interface {
// CertificateSigningRequestClient sets the client interface that is used for
// signing new certificates generated as part of rotation.
SetCertificateSigningRequestClient(certificatesclient.CertificateSigningRequestInterface) error
// Start the API server status sync loop. // Start the API server status sync loop.
Start() Start()
// Stop the cert manager loop.
Stop()
// Current returns the currently selected certificate from the // Current returns the currently selected certificate from the
// certificate manager, as well as the associated certificate and key data // certificate manager, as well as the associated certificate and key data
// in PEM format. // in PEM format.
@ -67,11 +66,11 @@ type Manager interface {
// Config is the set of configuration parameters available for a new Manager. // Config is the set of configuration parameters available for a new Manager.
type Config struct { type Config struct {
// CertificateSigningRequestClient will be used for signing new certificate // ClientFn will be used to create a client for
// requests generated when a key rotation occurs. It must be set either at // signing new certificate requests generated when a key rotation occurs.
// initialization or by using CertificateSigningRequestClient before // It must be set at initialization. The function will never be invoked
// Manager.Start() is called. // in parallel. It is passed the current client certificate if one exists.
CertificateSigningRequestClient certificatesclient.CertificateSigningRequestInterface ClientFn CSRClientFunc
// Template is the CertificateRequest that will be used as a template for // Template is the CertificateRequest that will be used as a template for
// generating certificate signing requests for all new keys generated as // generating certificate signing requests for all new keys generated as
// part of rotation. It follows the same rules as the template parameter of // part of rotation. It follows the same rules as the template parameter of
@ -141,21 +140,34 @@ type Gauge interface {
// NoCertKeyError indicates there is no cert/key currently available. // NoCertKeyError indicates there is no cert/key currently available.
type NoCertKeyError string type NoCertKeyError string
// CSRClientFunc returns a new client for requesting CSRs. It passes the
// current certificate if one is available and valid.
type CSRClientFunc func(current *tls.Certificate) (certificatesclient.CertificateSigningRequestInterface, error)
func (e *NoCertKeyError) Error() string { return string(*e) } func (e *NoCertKeyError) Error() string { return string(*e) }
type manager struct { type manager struct {
certSigningRequestClient certificatesclient.CertificateSigningRequestInterface getTemplate func() *x509.CertificateRequest
getTemplate func() *x509.CertificateRequest lastRequestLock sync.Mutex
lastRequestLock sync.Mutex lastRequest *x509.CertificateRequest
lastRequest *x509.CertificateRequest dynamicTemplate bool
dynamicTemplate bool usages []certificates.KeyUsage
usages []certificates.KeyUsage forceRotation bool
certStore Store
certAccessLock sync.RWMutex certStore Store
cert *tls.Certificate
forceRotation bool certificateExpiration Gauge
certificateExpiration Gauge
serverHealth bool // the following variables must only be accessed under certAccessLock
certAccessLock sync.RWMutex
cert *tls.Certificate
serverHealth bool
// the clientFn must only be accessed under the clientAccessLock
clientAccessLock sync.Mutex
clientFn CSRClientFunc
stopCh chan struct{}
stopped bool
} }
// NewManager returns a new certificate manager. A certificate manager is // NewManager returns a new certificate manager. A certificate manager is
@ -176,14 +188,15 @@ func NewManager(config *Config) (Manager, error) {
} }
m := manager{ m := manager{
certSigningRequestClient: config.CertificateSigningRequestClient, stopCh: make(chan struct{}),
getTemplate: getTemplate, clientFn: config.ClientFn,
dynamicTemplate: config.GetTemplate != nil, getTemplate: getTemplate,
usages: config.Usages, dynamicTemplate: config.GetTemplate != nil,
certStore: config.CertificateStore, usages: config.Usages,
cert: cert, certStore: config.CertificateStore,
forceRotation: forceRotation, cert: cert,
certificateExpiration: config.CertificateExpiration, forceRotation: forceRotation,
certificateExpiration: config.CertificateExpiration,
} }
return &m, nil return &m, nil
@ -192,10 +205,14 @@ func NewManager(config *Config) (Manager, error) {
// Current returns the currently selected certificate from the certificate // Current returns the currently selected certificate from the certificate
// manager. This can be nil if the manager was initialized without a // manager. This can be nil if the manager was initialized without a
// certificate and has not yet received one from the // certificate and has not yet received one from the
// CertificateSigningRequestClient. // CertificateSigningRequestClient, or if the current cert has expired.
func (m *manager) Current() *tls.Certificate { func (m *manager) Current() *tls.Certificate {
m.certAccessLock.RLock() m.certAccessLock.RLock()
defer m.certAccessLock.RUnlock() defer m.certAccessLock.RUnlock()
if m.cert != nil && m.cert.Leaf != nil && time.Now().After(m.cert.Leaf.NotAfter) {
klog.V(2).Infof("Current certificate is expired.")
return nil
}
return m.cert return m.cert
} }
@ -207,18 +224,15 @@ func (m *manager) ServerHealthy() bool {
return m.serverHealth return m.serverHealth
} }
// SetCertificateSigningRequestClient sets the client interface that is used // Stop terminates the manager.
// for signing new certificates generated as part of rotation. It must be func (m *manager) Stop() {
// called before Start() and can not be used to change the m.clientAccessLock.Lock()
// CertificateSigningRequestClient that has already been set. This method is to defer m.clientAccessLock.Unlock()
// support the one specific scenario where the CertificateSigningRequestClient if m.stopped {
// uses the CertificateManager. return
func (m *manager) SetCertificateSigningRequestClient(certSigningRequestClient certificatesclient.CertificateSigningRequestInterface) error {
if m.certSigningRequestClient == nil {
m.certSigningRequestClient = certSigningRequestClient
return nil
} }
return fmt.Errorf("property CertificateSigningRequestClient is already set") close(m.stopCh)
m.stopped = true
} }
// Start will start the background work of rotating the certificates. // Start will start the background work of rotating the certificates.
@ -226,7 +240,7 @@ func (m *manager) Start() {
// Certificate rotation depends on access to the API server certificate // Certificate rotation depends on access to the API server certificate
// signing API, so don't start the certificate manager if we don't have a // signing API, so don't start the certificate manager if we don't have a
// client. // client.
if m.certSigningRequestClient == nil { if m.clientFn == nil {
klog.V(2).Infof("Certificate rotation is not enabled, no connection to the apiserver.") klog.V(2).Infof("Certificate rotation is not enabled, no connection to the apiserver.")
return return
} }
@ -234,7 +248,7 @@ func (m *manager) Start() {
klog.V(2).Infof("Certificate rotation is enabled.") klog.V(2).Infof("Certificate rotation is enabled.")
templateChanged := make(chan struct{}) templateChanged := make(chan struct{})
go wait.Forever(func() { go wait.Until(func() {
deadline := m.nextRotationDeadline() deadline := m.nextRotationDeadline()
if sleepInterval := deadline.Sub(time.Now()); sleepInterval > 0 { if sleepInterval := deadline.Sub(time.Now()); sleepInterval > 0 {
klog.V(2).Infof("Waiting %v for next certificate rotation", sleepInterval) klog.V(2).Infof("Waiting %v for next certificate rotation", sleepInterval)
@ -269,17 +283,17 @@ func (m *manager) Start() {
utilruntime.HandleError(fmt.Errorf("Reached backoff limit, still unable to rotate certs: %v", err)) utilruntime.HandleError(fmt.Errorf("Reached backoff limit, still unable to rotate certs: %v", err))
wait.PollInfinite(32*time.Second, m.rotateCerts) wait.PollInfinite(32*time.Second, m.rotateCerts)
} }
}, time.Second) }, time.Second, m.stopCh)
if m.dynamicTemplate { if m.dynamicTemplate {
go wait.Forever(func() { go wait.Until(func() {
// check if the current template matches what we last requested // check if the current template matches what we last requested
if !m.certSatisfiesTemplate() && !reflect.DeepEqual(m.getLastRequest(), m.getTemplate()) { if !m.certSatisfiesTemplate() && !reflect.DeepEqual(m.getLastRequest(), m.getTemplate()) {
// if the template is different, queue up an interrupt of the rotation deadline loop. // if the template is different, queue up an interrupt of the rotation deadline loop.
// if we've requested a CSR that matches the new template by the time the interrupt is handled, the interrupt is disregarded. // if we've requested a CSR that matches the new template by the time the interrupt is handled, the interrupt is disregarded.
templateChanged <- struct{}{} templateChanged <- struct{}{}
} }
}, time.Second) }, time.Second, m.stopCh)
} }
} }
@ -327,11 +341,26 @@ func getCurrentCertificateOrBootstrap(
return &bootstrapCert, true, nil return &bootstrapCert, true, nil
} }
func (m *manager) getClient() (certificatesclient.CertificateSigningRequestInterface, error) {
current := m.Current()
m.clientAccessLock.Lock()
defer m.clientAccessLock.Unlock()
return m.clientFn(current)
}
// RotateCerts is exposed for testing only and is not a part of the public interface.
// Returns true if it changed the cert, false otherwise. Error is only returned in
// exceptional cases.
func (m *manager) RotateCerts() (bool, error) {
return m.rotateCerts()
}
// rotateCerts attempts to request a client cert from the server, wait a reasonable // rotateCerts attempts to request a client cert from the server, wait a reasonable
// period of time for it to be signed, and then update the cert on disk. If it cannot // period of time for it to be signed, and then update the cert on disk. If it cannot
// retrieve a cert, it will return false. It will only return error in exceptional cases. // retrieve a cert, it will return false. It will only return error in exceptional cases.
// This method also keeps track of "server health" by interpreting the responses it gets // This method also keeps track of "server health" by interpreting the responses it gets
// from the server on the various calls it makes. // from the server on the various calls it makes.
// TODO: return errors, have callers handle and log them correctly
func (m *manager) rotateCerts() (bool, error) { func (m *manager) rotateCerts() (bool, error) {
klog.V(2).Infof("Rotating certificates") klog.V(2).Infof("Rotating certificates")
@ -341,9 +370,16 @@ func (m *manager) rotateCerts() (bool, error) {
return false, nil return false, nil
} }
// request the client each time
client, err := m.getClient()
if err != nil {
utilruntime.HandleError(fmt.Errorf("Unable to load a client to request certificates: %v", err))
return false, nil
}
// Call the Certificate Signing Request API to get a certificate for the // Call the Certificate Signing Request API to get a certificate for the
// new private key. // new private key.
req, err := csr.RequestCertificate(m.certSigningRequestClient, csrPEM, "", m.usages, privateKey) req, err := csr.RequestCertificate(client, csrPEM, "", m.usages, privateKey)
if err != nil { if err != nil {
utilruntime.HandleError(fmt.Errorf("Failed while requesting a signed certificate from the master: %v", err)) utilruntime.HandleError(fmt.Errorf("Failed while requesting a signed certificate from the master: %v", err))
return false, m.updateServerError(err) return false, m.updateServerError(err)
@ -359,7 +395,7 @@ func (m *manager) rotateCerts() (bool, error) {
var crtPEM []byte var crtPEM []byte
watchDuration := time.Minute watchDuration := time.Minute
if err := wait.ExponentialBackoff(certificateWaitBackoff, func() (bool, error) { if err := wait.ExponentialBackoff(certificateWaitBackoff, func() (bool, error) {
data, err := csr.WaitForCertificate(m.certSigningRequestClient, req, watchDuration) data, err := csr.WaitForCertificate(client, req, watchDuration)
switch { switch {
case err == nil: case err == nil:
crtPEM = data crtPEM = data

View File

@ -60,6 +60,23 @@ iQIgZX08DA8VfvcA5/Xj1Zjdey9FVY6POLXen6RPiabE97UCICp6eUW7ht+2jjar
e35EltCRCjoejRHTuN9TC0uCoVipAiAXaJIx/Q47vGwiw6Y8KXsNU6y54gTbOSxX e35EltCRCjoejRHTuN9TC0uCoVipAiAXaJIx/Q47vGwiw6Y8KXsNU6y54gTbOSxX
54LzHNk/+Q== 54LzHNk/+Q==
-----END RSA PRIVATE KEY-----`) -----END RSA PRIVATE KEY-----`)
var expiredStoreCertData = newCertificateData(`-----BEGIN CERTIFICATE-----
MIIBFzCBwgIJALhygXnxXmN1MA0GCSqGSIb3DQEBCwUAMBMxETAPBgNVBAMMCGhv
c3QtMTIzMB4XDTE4MTEwNDIzNTc1NFoXDTE4MTEwNTIzNTc1NFowEzERMA8GA1UE
AwwIaG9zdC0xMjMwXDANBgkqhkiG9w0BAQEFAANLADBIAkEAtBMa7NWpv3BVlKTC
PGO/LEsguKqWHBtKzweMY2CVtAL1rQm913huhxF9w+ai76KQ3MHK5IVnLJjYYA5M
zP2H5QIDAQABMA0GCSqGSIb3DQEBCwUAA0EAN2DPFUtCzqnidL+5nh+46Sk6dkMI
T5DD11UuuIjZusKvThsHKVCIsyJ2bDo7cTbI+/nklLRP+FcC2wESFUgXbA==
-----END CERTIFICATE-----`, `-----BEGIN RSA PRIVATE KEY-----
MIIBUwIBADANBgkqhkiG9w0BAQEFAASCAT0wggE5AgEAAkEAtBMa7NWpv3BVlKTC
PGO/LEsguKqWHBtKzweMY2CVtAL1rQm913huhxF9w+ai76KQ3MHK5IVnLJjYYA5M
zP2H5QIDAQABAkAS9BfXab3OKpK3bIgNNyp+DQJKrZnTJ4Q+OjsqkpXvNltPJosf
G8GsiKu/vAt4HGqI3eU77NvRI+mL4MnHRmXBAiEA3qM4FAtKSRBbcJzPxxLEUSwg
XSCcosCktbkXvpYrS30CIQDPDxgqlwDEJQ0uKuHkZI38/SPWWqfUmkecwlbpXABK
iQIgZX08DA8VfvcA5/Xj1Zjdey9FVY6POLXen6RPiabE97UCICp6eUW7ht+2jjar
e35EltCRCjoejRHTuN9TC0uCoVipAiAXaJIx/Q47vGwiw6Y8KXsNU6y54gTbOSxX
54LzHNk/+Q==
-----END RSA PRIVATE KEY-----`)
var bootstrapCertData = newCertificateData( var bootstrapCertData = newCertificateData(
`-----BEGIN CERTIFICATE----- `-----BEGIN CERTIFICATE-----
MIICRzCCAfGgAwIBAgIJANXr+UzRFq4TMA0GCSqGSIb3DQEBCwUAMH4xCzAJBgNV MIICRzCCAfGgAwIBAgIJANXr+UzRFq4TMA0GCSqGSIb3DQEBCwUAMH4xCzAJBgNV
@ -388,8 +405,8 @@ func TestRotateCertCreateCSRError(t *testing.T) {
}, },
getTemplate: func() *x509.CertificateRequest { return &x509.CertificateRequest{} }, getTemplate: func() *x509.CertificateRequest { return &x509.CertificateRequest{} },
usages: []certificates.KeyUsage{}, usages: []certificates.KeyUsage{},
certSigningRequestClient: fakeClient{ clientFn: func(_ *tls.Certificate) (certificatesclient.CertificateSigningRequestInterface, error) {
failureType: createError, return fakeClient{failureType: createError}, nil
}, },
} }
@ -411,8 +428,8 @@ func TestRotateCertWaitingForResultError(t *testing.T) {
}, },
getTemplate: func() *x509.CertificateRequest { return &x509.CertificateRequest{} }, getTemplate: func() *x509.CertificateRequest { return &x509.CertificateRequest{} },
usages: []certificates.KeyUsage{}, usages: []certificates.KeyUsage{},
certSigningRequestClient: fakeClient{ clientFn: func(_ *tls.Certificate) (certificatesclient.CertificateSigningRequestInterface, error) {
failureType: watchError, return fakeClient{failureType: watchError}, nil
}, },
} }
@ -598,6 +615,14 @@ func TestInitializeCertificateSigningRequestClient(t *testing.T) {
expectedCertBeforeStart: storeCertData, expectedCertBeforeStart: storeCertData,
expectedCertAfterStart: storeCertData, expectedCertAfterStart: storeCertData,
}, },
{
description: "Current certificate expired, no bootstrap certificate",
storeCert: expiredStoreCertData,
bootstrapCert: nilCertificate,
apiCert: apiServerCertData,
expectedCertBeforeStart: nil,
expectedCertAfterStart: apiServerCertData,
},
} }
for _, tc := range testCases { for _, tc := range testCases {
@ -621,19 +646,25 @@ func TestInitializeCertificateSigningRequestClient(t *testing.T) {
CertificateStore: certificateStore, CertificateStore: certificateStore,
BootstrapCertificatePEM: tc.bootstrapCert.certificatePEM, BootstrapCertificatePEM: tc.bootstrapCert.certificatePEM,
BootstrapKeyPEM: tc.bootstrapCert.keyPEM, BootstrapKeyPEM: tc.bootstrapCert.keyPEM,
ClientFn: func(_ *tls.Certificate) (certificatesclient.CertificateSigningRequestInterface, error) {
return &fakeClient{
certificatePEM: tc.apiCert.certificatePEM,
}, nil
},
}) })
if err != nil { if err != nil {
t.Errorf("Got %v, wanted no error.", err) t.Errorf("Got %v, wanted no error.", err)
} }
certificate := certificateManager.Current() certificate := certificateManager.Current()
if !certificatesEqual(certificate, tc.expectedCertBeforeStart.certificate) { if tc.expectedCertBeforeStart == nil {
t.Errorf("Got %v, wanted %v", certificateString(certificate), certificateString(tc.expectedCertBeforeStart.certificate)) if certificate != nil {
} t.Errorf("Expected certificate to be nil, was %s", certificate.Leaf.NotAfter)
if err := certificateManager.SetCertificateSigningRequestClient(&fakeClient{ }
certificatePEM: tc.apiCert.certificatePEM, } else {
}); err != nil { if !certificatesEqual(certificate, tc.expectedCertBeforeStart.certificate) {
t.Errorf("Got error %v, expected none.", err) t.Errorf("Got %v, wanted %v", certificateString(certificate), certificateString(tc.expectedCertBeforeStart.certificate))
}
} }
if m, ok := certificateManager.(*manager); !ok { if m, ok := certificateManager.(*manager); !ok {
@ -649,6 +680,12 @@ func TestInitializeCertificateSigningRequestClient(t *testing.T) {
} }
certificate = certificateManager.Current() certificate = certificateManager.Current()
if tc.expectedCertAfterStart == nil {
if certificate != nil {
t.Errorf("Expected certificate to be nil, was %s", certificate.Leaf.NotAfter)
}
return
}
if !certificatesEqual(certificate, tc.expectedCertAfterStart.certificate) { if !certificatesEqual(certificate, tc.expectedCertAfterStart.certificate) {
t.Errorf("Got %v, wanted %v", certificateString(certificate), certificateString(tc.expectedCertAfterStart.certificate)) t.Errorf("Got %v, wanted %v", certificateString(certificate), certificateString(tc.expectedCertAfterStart.certificate))
} }
@ -721,8 +758,10 @@ func TestInitializeOtherRESTClients(t *testing.T) {
CertificateStore: certificateStore, CertificateStore: certificateStore,
BootstrapCertificatePEM: tc.bootstrapCert.certificatePEM, BootstrapCertificatePEM: tc.bootstrapCert.certificatePEM,
BootstrapKeyPEM: tc.bootstrapCert.keyPEM, BootstrapKeyPEM: tc.bootstrapCert.keyPEM,
CertificateSigningRequestClient: &fakeClient{ ClientFn: func(_ *tls.Certificate) (certificatesclient.CertificateSigningRequestInterface, error) {
certificatePEM: tc.apiCert.certificatePEM, return &fakeClient{
certificatePEM: tc.apiCert.certificatePEM,
}, nil
}, },
}) })
if err != nil { if err != nil {
@ -873,10 +912,12 @@ func TestServerHealth(t *testing.T) {
CertificateStore: certificateStore, CertificateStore: certificateStore,
BootstrapCertificatePEM: tc.bootstrapCert.certificatePEM, BootstrapCertificatePEM: tc.bootstrapCert.certificatePEM,
BootstrapKeyPEM: tc.bootstrapCert.keyPEM, BootstrapKeyPEM: tc.bootstrapCert.keyPEM,
CertificateSigningRequestClient: &fakeClient{ ClientFn: func(_ *tls.Certificate) (certificatesclient.CertificateSigningRequestInterface, error) {
certificatePEM: tc.apiCert.certificatePEM, return &fakeClient{
failureType: tc.failureType, certificatePEM: tc.apiCert.certificatePEM,
err: tc.clientErr, failureType: tc.failureType,
err: tc.clientErr,
}, nil
}, },
}) })
if err != nil { if err != nil {