Merge pull request #41912 from jcbsmpsn/rotate-client-certificate

Automatic merge from submit-queue (batch tested with PRs 46726, 41912, 46695, 46034, 46551)

Rotate kubelet client certificate.

Changes the kubelet so it bootstraps off the cert/key specified in the
config file and uses those to request new cert/key pairs from the
Certificate Signing Request API, as well as rotating client certificates
when they approach expiration.

Default behavior is for client certificate rotation to be disabled. If enabled
using a command line flag, the kubelet exits each time the certificate is
rotated. I tried to use `GetCertificate` in [tls.Config](https://golang.org/pkg/crypto/tls/#Config) but it is only called
on the server side of connections. Then I tried `GetClientCertificate`,
but it is new in 1.8.

**Release note**
```release-note
With --feature-gates=RotateKubeletClientCertificate=true set, the kubelet will
request a client certificate from the API server during the boot cycle and pause
waiting for the request to be satisfied. It will continually refresh the certificate
as the certificates expiration approaches.
```
This commit is contained in:
Kubernetes Submit Queue 2017-06-02 21:42:37 -07:00 committed by GitHub
commit 24d09977fb
5 changed files with 130 additions and 8 deletions

View File

@ -37,6 +37,7 @@ go_library(
"//cmd/kubelet/app/options:go_default_library", "//cmd/kubelet/app/options:go_default_library",
"//pkg/api:go_default_library", "//pkg/api:go_default_library",
"//pkg/api/v1:go_default_library", "//pkg/api/v1:go_default_library",
"//pkg/apis/certificates/v1beta1:go_default_library",
"//pkg/apis/componentconfig:go_default_library", "//pkg/apis/componentconfig:go_default_library",
"//pkg/apis/componentconfig/v1alpha1:go_default_library", "//pkg/apis/componentconfig/v1alpha1:go_default_library",
"//pkg/capabilities:go_default_library", "//pkg/capabilities:go_default_library",
@ -52,6 +53,7 @@ go_library(
"//pkg/features:go_default_library", "//pkg/features:go_default_library",
"//pkg/kubelet:go_default_library", "//pkg/kubelet:go_default_library",
"//pkg/kubelet/cadvisor:go_default_library", "//pkg/kubelet/cadvisor:go_default_library",
"//pkg/kubelet/certificate:go_default_library",
"//pkg/kubelet/cm:go_default_library", "//pkg/kubelet/cm:go_default_library",
"//pkg/kubelet/config:go_default_library", "//pkg/kubelet/config:go_default_library",
"//pkg/kubelet/container:go_default_library", "//pkg/kubelet/container:go_default_library",
@ -110,6 +112,7 @@ go_library(
"//vendor/k8s.io/apimachinery/pkg/apis/meta/v1:go_default_library", "//vendor/k8s.io/apimachinery/pkg/apis/meta/v1:go_default_library",
"//vendor/k8s.io/apimachinery/pkg/runtime:go_default_library", "//vendor/k8s.io/apimachinery/pkg/runtime:go_default_library",
"//vendor/k8s.io/apimachinery/pkg/types:go_default_library", "//vendor/k8s.io/apimachinery/pkg/types:go_default_library",
"//vendor/k8s.io/apimachinery/pkg/util/net:go_default_library",
"//vendor/k8s.io/apimachinery/pkg/util/runtime:go_default_library", "//vendor/k8s.io/apimachinery/pkg/util/runtime:go_default_library",
"//vendor/k8s.io/apimachinery/pkg/util/sets:go_default_library", "//vendor/k8s.io/apimachinery/pkg/util/sets:go_default_library",
"//vendor/k8s.io/apimachinery/pkg/util/wait:go_default_library", "//vendor/k8s.io/apimachinery/pkg/util/wait:go_default_library",

View File

@ -19,6 +19,8 @@ package app
import ( import (
"crypto/tls" "crypto/tls"
"crypto/x509"
"crypto/x509/pkix"
"errors" "errors"
"fmt" "fmt"
"io/ioutil" "io/ioutil"
@ -41,6 +43,7 @@ import (
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/types" "k8s.io/apimachinery/pkg/types"
utilnet "k8s.io/apimachinery/pkg/util/net"
utilruntime "k8s.io/apimachinery/pkg/util/runtime" utilruntime "k8s.io/apimachinery/pkg/util/runtime"
"k8s.io/apimachinery/pkg/util/sets" "k8s.io/apimachinery/pkg/util/sets"
"k8s.io/apimachinery/pkg/util/wait" "k8s.io/apimachinery/pkg/util/wait"
@ -58,6 +61,7 @@ import (
"k8s.io/kubernetes/cmd/kubelet/app/options" "k8s.io/kubernetes/cmd/kubelet/app/options"
"k8s.io/kubernetes/pkg/api" "k8s.io/kubernetes/pkg/api"
"k8s.io/kubernetes/pkg/api/v1" "k8s.io/kubernetes/pkg/api/v1"
certificates "k8s.io/kubernetes/pkg/apis/certificates/v1beta1"
"k8s.io/kubernetes/pkg/apis/componentconfig" "k8s.io/kubernetes/pkg/apis/componentconfig"
componentconfigv1alpha1 "k8s.io/kubernetes/pkg/apis/componentconfig/v1alpha1" componentconfigv1alpha1 "k8s.io/kubernetes/pkg/apis/componentconfig/v1alpha1"
"k8s.io/kubernetes/pkg/capabilities" "k8s.io/kubernetes/pkg/capabilities"
@ -68,6 +72,7 @@ import (
"k8s.io/kubernetes/pkg/features" "k8s.io/kubernetes/pkg/features"
"k8s.io/kubernetes/pkg/kubelet" "k8s.io/kubernetes/pkg/kubelet"
"k8s.io/kubernetes/pkg/kubelet/cadvisor" "k8s.io/kubernetes/pkg/kubelet/cadvisor"
"k8s.io/kubernetes/pkg/kubelet/certificate"
"k8s.io/kubernetes/pkg/kubelet/cm" "k8s.io/kubernetes/pkg/kubelet/cm"
"k8s.io/kubernetes/pkg/kubelet/config" "k8s.io/kubernetes/pkg/kubelet/config"
kubecontainer "k8s.io/kubernetes/pkg/kubelet/container" kubecontainer "k8s.io/kubernetes/pkg/kubelet/container"
@ -446,10 +451,30 @@ func run(s *options.KubeletServer, kubeDeps *kubelet.KubeletDeps) (err error) {
} }
clientConfig, err := CreateAPIServerClientConfig(s) clientConfig, err := CreateAPIServerClientConfig(s)
var clientCertificateManager certificate.Manager
if err == nil { if err == nil {
if utilfeature.DefaultFeatureGate.Enabled(features.RotateKubeletClientCertificate) {
nodeName, err := getNodeName(cloud, nodeutil.GetHostname(s.HostnameOverride))
if err != nil {
return err
}
clientCertificateManager, err = initializeClientCertificateManager(s.CertDirectory, nodeName, clientConfig.CertData, clientConfig.KeyData)
if err != nil {
return err
}
if err := updateTransport(clientConfig, clientCertificateManager); err != nil {
return err
}
}
kubeClient, err = clientset.NewForConfig(clientConfig) kubeClient, err = clientset.NewForConfig(clientConfig)
if err != nil { if err != nil {
glog.Warningf("New kubeClient from clientConfig error: %v", err) glog.Warningf("New kubeClient from clientConfig error: %v", err)
} else if kubeClient.Certificates() != nil && clientCertificateManager != nil {
glog.V(2).Info("Starting client certificate rotation.")
clientCertificateManager.SetCertificateSigningRequestClient(kubeClient.Certificates().CertificateSigningRequests())
clientCertificateManager.Start()
} }
externalKubeClient, err = clientgoclientset.NewForConfig(clientConfig) externalKubeClient, err = clientgoclientset.NewForConfig(clientConfig)
if err != nil { if err != nil {
@ -599,6 +624,90 @@ func run(s *options.KubeletServer, kubeDeps *kubelet.KubeletDeps) (err error) {
return nil return nil
} }
func updateTransport(clientConfig *restclient.Config, clientCertificateManager certificate.Manager) error {
if clientConfig.Transport != nil {
return fmt.Errorf("there is already a transport configured")
}
tlsConfig, err := restclient.TLSConfigFor(clientConfig)
if err != nil {
return fmt.Errorf("unable to configure TLS for the rest client: %v", err)
}
if tlsConfig == nil {
tlsConfig = &tls.Config{}
}
tlsConfig.Certificates = nil
tlsConfig.GetClientCertificate = func(requestInfo *tls.CertificateRequestInfo) (*tls.Certificate, error) {
cert := clientCertificateManager.Current()
if cert == nil {
return &tls.Certificate{Certificate: nil}, nil
}
return cert, nil
}
clientConfig.Transport = utilnet.SetTransportDefaults(&http.Transport{
Proxy: http.ProxyFromEnvironment,
TLSHandshakeTimeout: 10 * time.Second,
TLSClientConfig: tlsConfig,
MaxIdleConnsPerHost: 25,
Dial: (&net.Dialer{
Timeout: 30 * time.Second,
KeepAlive: 30 * time.Second,
}).Dial,
})
clientConfig.CertData = nil
clientConfig.KeyData = nil
clientConfig.CertFile = ""
clientConfig.KeyFile = ""
clientConfig.CAData = nil
clientConfig.CAFile = ""
return nil
}
// initializeClientCertificateManager sets up a certificate manager without a
// client that can be used to sign new certificates (or rotate). It answers with
// whatever certificate it is initialized with. If a CSR client is set later, it
// may begin rotating/renewing the client cert
func initializeClientCertificateManager(certDirectory string, nodeName types.NodeName, certData []byte, keyData []byte) (certificate.Manager, error) {
certificateStore, err := certificate.NewFileStore(
"kubelet-client",
certDirectory,
certDirectory,
"",
"")
if err != nil {
return nil, fmt.Errorf("failed to initialize certificate store: %v", err)
}
clientCertificateManager, err := certificate.NewManager(&certificate.Config{
Template: &x509.CertificateRequest{
Subject: pkix.Name{
Organization: []string{"system:nodes"},
CommonName: fmt.Sprintf("system:node:%s", nodeName),
},
},
Usages: []certificates.KeyUsage{
// https://tools.ietf.org/html/rfc5280#section-4.2.1.3
//
// DigitalSignature allows the certificate to be used to verify
// digital signatures including signatures used during TLS
// negotiation.
certificates.UsageDigitalSignature,
// KeyEncipherment allows the cert/key pair to be used to encrypt
// keys, including the symetric keys negotiated during TLS setup
// and used for data transfer..
certificates.UsageKeyEncipherment,
// ClientAuth allows the cert to be used by a TLS client to
// authenticate itself to the TLS server.
certificates.UsageClientAuth,
},
CertificateStore: certificateStore,
BootstrapCertificatePEM: certData,
BootstrapKeyPEM: keyData,
})
if err != nil {
return nil, fmt.Errorf("failed to initialize certificate manager: %v", err)
}
return clientCertificateManager, nil
}
// 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) {

View File

@ -97,6 +97,13 @@ const (
// certificate as expiration approaches. // certificate as expiration approaches.
RotateKubeletServerCertificate utilfeature.Feature = "RotateKubeletServerCertificate" RotateKubeletServerCertificate utilfeature.Feature = "RotateKubeletServerCertificate"
// owner: @jcbsmpsn
// alpha: v1.7
//
// Automatically renews the client certificate used for communicating with
// the API server as the certificate approaches expiration.
RotateKubeletClientCertificate utilfeature.Feature = "RotateKubeletClientCertificate"
// owner: @msau // owner: @msau
// alpha: v1.7 // alpha: v1.7
// //
@ -128,6 +135,7 @@ var defaultKubernetesFeatureGates = map[utilfeature.Feature]utilfeature.FeatureS
Accelerators: {Default: false, PreRelease: utilfeature.Alpha}, Accelerators: {Default: false, PreRelease: utilfeature.Alpha},
TaintBasedEvictions: {Default: false, PreRelease: utilfeature.Alpha}, TaintBasedEvictions: {Default: false, PreRelease: utilfeature.Alpha},
RotateKubeletServerCertificate: {Default: false, PreRelease: utilfeature.Alpha}, RotateKubeletServerCertificate: {Default: false, PreRelease: utilfeature.Alpha},
RotateKubeletClientCertificate: {Default: false, PreRelease: utilfeature.Alpha},
PersistentLocalVolumes: {Default: false, PreRelease: utilfeature.Alpha}, PersistentLocalVolumes: {Default: false, PreRelease: utilfeature.Alpha},
LocalStorageCapacityIsolation: {Default: false, PreRelease: utilfeature.Alpha}, LocalStorageCapacityIsolation: {Default: false, PreRelease: utilfeature.Alpha},

View File

@ -52,14 +52,17 @@ type Manager interface {
// Start the API server status sync loop. // Start the API server status sync loop.
Start() Start()
// Current returns the currently selected certificate from the // Current returns the currently selected certificate from the
// certificate manager. // certificate manager, as well as the associated certificate and key data
// in PEM format.
Current() *tls.Certificate Current() *tls.Certificate
} }
// 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 // CertificateSigningRequestClient will be used for signing new certificate
// requests generated when a key rotation occurs. // requests generated when a key rotation occurs. It must be set either at
// initialization or by using CertificateSigningRequestClient before
// Manager.Start() is called.
CertificateSigningRequestClient certificatesclient.CertificateSigningRequestInterface CertificateSigningRequestClient certificatesclient.CertificateSigningRequestInterface
// 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
@ -99,7 +102,8 @@ type Config struct {
// Depending on the concrete implementation, the backing store for this // Depending on the concrete implementation, the backing store for this
// behavior may vary. // behavior may vary.
type Store interface { type Store interface {
// Current returns the currently selected certificate. If the Store doesn't // Current returns the currently selected certificate, as well as the
// associated certificate and key data in PEM format. If the Store doesn't
// have a cert/key pair currently, it should return a NoCertKeyError so // have a cert/key pair currently, it should return a NoCertKeyError so
// that the Manager can recover by using bootstrap certificates to request // that the Manager can recover by using bootstrap certificates to request
// a new cert/key pair. // a new cert/key pair.

View File

@ -173,7 +173,7 @@ func TestShouldRotate(t *testing.T) {
} }
m.setRotationDeadline() m.setRotationDeadline()
if m.shouldRotate() != test.shouldRotate { if m.shouldRotate() != test.shouldRotate {
t.Errorf("For time %v, a certificate issued for (%v, %v) should rotate should be %t.", t.Errorf("Time %v, a certificate issued for (%v, %v) should rotate should be %t.",
now, now,
m.cert.Leaf.NotBefore, m.cert.Leaf.NotBefore,
m.cert.Leaf.NotAfter, m.cert.Leaf.NotAfter,
@ -296,7 +296,6 @@ func TestNewManagerBootstrap(t *testing.T) {
if cert == nil { if cert == nil {
t.Errorf("Certificate was nil, expected something.") t.Errorf("Certificate was nil, expected something.")
} }
if m, ok := cm.(*manager); !ok { if m, ok := cm.(*manager); !ok {
t.Errorf("Expected a '*manager' from 'NewManager'") t.Errorf("Expected a '*manager' from 'NewManager'")
} else if !m.shouldRotate() { } else if !m.shouldRotate() {
@ -335,7 +334,6 @@ func TestNewManagerNoBootstrap(t *testing.T) {
if currentCert == nil { if currentCert == nil {
t.Errorf("Certificate was nil, expected something.") t.Errorf("Certificate was nil, expected something.")
} }
if m, ok := cm.(*manager); !ok { if m, ok := cm.(*manager); !ok {
t.Errorf("Expected a '*manager' from 'NewManager'") t.Errorf("Expected a '*manager' from 'NewManager'")
} else { } else {
@ -386,8 +384,8 @@ func TestGetCurrentCertificateOrBootstrap(t *testing.T) {
store, store,
tc.bootstrapCertData, tc.bootstrapCertData,
tc.bootstrapKeyData) tc.bootstrapKeyData)
if certResult == nil || tc.expectedCert == nil { if certResult == nil || certResult.Certificate == nil || tc.expectedCert == nil {
if certResult != tc.expectedCert { if certResult != nil && tc.expectedCert != nil {
t.Errorf("Got certificate %v, wanted %v", certResult, tc.expectedCert) t.Errorf("Got certificate %v, wanted %v", certResult, tc.expectedCert)
} }
} else { } else {