Add bootstrap support to certificate manager.

This commit is contained in:
Jacob Simpson 2017-03-02 13:56:11 -08:00
parent db53e80445
commit e992eaec8f
4 changed files with 323 additions and 52 deletions

View File

@ -48,19 +48,56 @@ const (
type Manager interface { type Manager interface {
// Start the API server status sync loop. // Start the API server status sync loop.
Start() Start()
// GetCertificate gets the current certificate from the certificate // Current returns the currently selected certificate from the
// manager. This function matches the signature required by // certificate manager.
// tls.Config.GetCertificate so it can be passed as TLS configuration. A Current() *tls.Certificate
// TLS server will automatically call back here to get the correct }
// certificate when establishing each new connection.
GetCertificate(clientHello *tls.ClientHelloInfo) (*tls.Certificate, error) // Config is the set of configuration parameters available for a new Manager.
type Config struct {
// CertificateSigningRequestClient will be used for signing new certificate
// requests generated when a key rotation occurs.
CertificateSigningRequestClient certificatesclient.CertificateSigningRequestInterface
// Template is the CertificateRequest that will be used as a template for
// generating certificate signing requests for all new keys generated as
// part of rotation. It follows the same rules as the template parameter of
// crypto.x509.CreateCertificateRequest in the Go standard libraries.
Template *x509.CertificateRequest
// Usages is the types of usages that certificates generated by the manager
// can be used for.
Usages []certificates.KeyUsage
// CertificateStore is a persistent store where the current cert/key is
// kept and future cert/key pairs will be persisted after they are
// generated.
CertificateStore Store
// BootstrapCertificatePEM is the certificate data that will be returned
// from the Manager if the CertificateStore doesn't have any cert/key pairs
// currently available. If the CertificateStore does have a cert/key pair,
// this will be ignored. If the bootstrap cert/key pair are used, they will
// be rotated at the first opportunity, possibly well in advance of
// expiring. This is intended to allow the first boot of a component to be
// initialized using a generic, multi-use cert/key pair which will be
// quickly replaced with a unique cert/key pair.
BootstrapCertificatePEM []byte
// BootstrapKeyPEM is the key data that will be returned from the Manager
// if the CertificateStore doesn't have any cert/key pairs currently
// available. If the CertificateStore does have a cert/key pair, this will
// be ignored. If the bootstrap cert/key pair are used, they will be
// rotated at the first opportunity, possibly well in advance of expiring.
// This is intended to allow the first boot of a component to be
// initialized using a generic, multi-use cert/key pair which will be
// quickly replaced with a unique cert/key pair.
BootstrapKeyPEM []byte
} }
// Store is responsible for getting and updating the current certificate. // Store is responsible for getting and updating the current certificate.
// 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. // Current returns the currently selected certificate. If the Store doesn't
// have a cert/key pair currently, it should return a NoCertKeyError so
// that the Manager can recover by using bootstrap certificates to request
// a new cert/key pair.
Current() (*tls.Certificate, error) Current() (*tls.Certificate, error)
// Update accepts the PEM data for the cert/key pair and makes the new // Update accepts the PEM data for the cert/key pair and makes the new
// cert/key pair the 'current' pair, that will be returned by future calls // cert/key pair the 'current' pair, that will be returned by future calls
@ -68,6 +105,11 @@ type Store interface {
Update(cert, key []byte) (*tls.Certificate, error) Update(cert, key []byte) (*tls.Certificate, error)
} }
// NoCertKeyError indicates there is no cert/key currently available.
type NoCertKeyError string
func (e *NoCertKeyError) Error() string { return string(*e) }
type manager struct { type manager struct {
certSigningRequestClient certificatesclient.CertificateSigningRequestInterface certSigningRequestClient certificatesclient.CertificateSigningRequestInterface
template *x509.CertificateRequest template *x509.CertificateRequest
@ -75,49 +117,39 @@ type manager struct {
certStore Store certStore Store
certAccessLock sync.RWMutex certAccessLock sync.RWMutex
cert *tls.Certificate cert *tls.Certificate
forceRotation bool
} }
// NewManager returns a new certificate manager. A certificate manager is // NewManager returns a new certificate manager. A certificate manager is
// responsible for being the authoritative source of certificates in the // responsible for being the authoritative source of certificates in the
// Kubelet and handling updates due to rotation. // Kubelet and handling updates due to rotation.
func NewManager( func NewManager(config *Config) (Manager, error) {
certSigningRequestClient certificatesclient.CertificateSigningRequestInterface, cert, forceRotation, err := getCurrentCertificateOrBootstrap(
template *x509.CertificateRequest, config.CertificateStore,
usages []certificates.KeyUsage, config.BootstrapCertificatePEM,
certificateStore Store) (Manager, error) { config.BootstrapKeyPEM)
cert, err := certificateStore.Current()
if err != nil { if err != nil {
return nil, err return nil, err
} }
m := manager{ m := manager{
certSigningRequestClient: certSigningRequestClient, certSigningRequestClient: config.CertificateSigningRequestClient,
template: template, template: config.Template,
usages: usages, usages: config.Usages,
certStore: certificateStore, certStore: config.CertificateStore,
cert: cert, cert: cert,
forceRotation: forceRotation,
} }
return &m, nil return &m, nil
} }
// GetCertificate returns the certificate that should be used with TLS // Current returns the currently selected certificate from the
// connections. The value returned by this function will change over time as // certificate manager.
// the certificate is rotated. If a reference to this method is passed directly func (m *manager) Current() *tls.Certificate {
// into the TLS options for a connection, certificate rotation will be handled
// correctly by the underlying go libraries.
//
// tlsOptions := &server.TLSOptions{
// ...
// GetCertificate: certificateManager.GetCertificate
// ...
// }
//
func (m *manager) GetCertificate(clientHello *tls.ClientHelloInfo) (*tls.Certificate, error) {
m.certAccessLock.RLock() m.certAccessLock.RLock()
defer m.certAccessLock.RUnlock() defer m.certAccessLock.RUnlock()
return m.cert, nil return m.cert
} }
// Start will start the background work of rotating the certificates. // Start will start the background work of rotating the certificates.
@ -142,6 +174,40 @@ func (m *manager) Start() {
}, 0) }, 0)
} }
func getCurrentCertificateOrBootstrap(
store Store,
bootstrapCertificatePEM []byte,
bootstrapKeyPEM []byte) (cert *tls.Certificate, shouldRotate bool, errResult error) {
currentCert, err := store.Current()
if err == nil {
return currentCert, false, nil
}
if _, ok := err.(*NoCertKeyError); !ok {
return nil, false, err
}
if bootstrapCertificatePEM == nil || bootstrapKeyPEM == nil {
return nil, false, fmt.Errorf("no cert/key available and no bootstrap cert/key to fall back to")
}
bootstrapCert, err := tls.X509KeyPair(bootstrapCertificatePEM, bootstrapKeyPEM)
if err != nil {
return nil, false, err
}
if len(bootstrapCert.Certificate) < 1 {
return nil, false, fmt.Errorf("no cert/key data found")
}
certs, err := x509.ParseCertificates(bootstrapCert.Certificate[0])
if err != nil {
return nil, false, fmt.Errorf("unable to parse certificate data: %v", err)
}
bootstrapCert.Leaf = certs[0]
return &bootstrapCert, true, nil
}
// shouldRotate looks at how close the current certificate is to expiring and // shouldRotate looks at how close the current certificate is to expiring and
// decides if it is time to rotate or not. // decides if it is time to rotate or not.
func (m *manager) shouldRotate() bool { func (m *manager) shouldRotate() bool {
@ -158,7 +224,8 @@ func (m *manager) shouldRotate() bool {
jitteryDuration := wait.Jitter(time.Duration(totalDuration), 0.2) - time.Duration(totalDuration*0.3) jitteryDuration := wait.Jitter(time.Duration(totalDuration), 0.2) - time.Duration(totalDuration*0.3)
rotationThreshold := m.cert.Leaf.NotBefore.Add(jitteryDuration) rotationThreshold := m.cert.Leaf.NotBefore.Add(jitteryDuration)
return time.Now().After(rotationThreshold) passedThreshold := time.Now().After(rotationThreshold)
return m.forceRotation || passedThreshold
} }
func (m *manager) rotateCerts() error { func (m *manager) rotateCerts() error {
@ -183,10 +250,15 @@ func (m *manager) rotateCerts() error {
return fmt.Errorf("unable to store the new cert/key pair: %v", err) return fmt.Errorf("unable to store the new cert/key pair: %v", err)
} }
m.updateCached(cert)
m.forceRotation = false
return nil
}
func (m *manager) updateCached(cert *tls.Certificate) {
m.certAccessLock.Lock() m.certAccessLock.Lock()
defer m.certAccessLock.Unlock() defer m.certAccessLock.Unlock()
m.cert = cert m.cert = cert
return nil
} }
func (m *manager) generateCSR() (csrPEM []byte, keyPEM []byte, err error) { func (m *manager) generateCSR() (csrPEM []byte, keyPEM []byte, err error) {

View File

@ -17,9 +17,11 @@ limitations under the License.
package certificate package certificate
import ( import (
"bytes"
"crypto/tls" "crypto/tls"
"crypto/x509" "crypto/x509"
"fmt" "fmt"
"strings"
"testing" "testing"
"time" "time"
@ -78,6 +80,24 @@ O1eRCsCGPAnUCviFgNeH15ug+6N54DTTR6ZV/TTV64FDOcsox9nrhYcmH9sYuITi
-----END CERTIFICATE-----` -----END CERTIFICATE-----`
) )
func TestNewManagerNoRotation(t *testing.T) {
cert, err := tls.X509KeyPair([]byte(certificateData), []byte(privateKeyData))
if err != nil {
t.Fatalf("Unable to initialize a certificate: %v", err)
}
store := &fakeStore{
cert: &cert,
}
if _, err := NewManager(&Config{
Template: &x509.CertificateRequest{},
Usages: []certificates.KeyUsage{},
CertificateStore: store,
}); err != nil {
t.Fatalf("Failed to initialize the certificate manager: %v", err)
}
}
func TestShouldRotate(t *testing.T) { func TestShouldRotate(t *testing.T) {
now := time.Now() now := time.Now()
tests := []struct { tests := []struct {
@ -122,8 +142,8 @@ func TestRotateCertCreateCSRError(t *testing.T) {
m := manager{ m := manager{
cert: &tls.Certificate{ cert: &tls.Certificate{
Leaf: &x509.Certificate{ Leaf: &x509.Certificate{
NotAfter: now.Add(-1 * time.Hour),
NotBefore: now.Add(-2 * time.Hour), NotBefore: now.Add(-2 * time.Hour),
NotAfter: now.Add(-1 * time.Hour),
}, },
}, },
template: &x509.CertificateRequest{}, template: &x509.CertificateRequest{},
@ -143,8 +163,8 @@ func TestRotateCertWaitingForResultError(t *testing.T) {
m := manager{ m := manager{
cert: &tls.Certificate{ cert: &tls.Certificate{
Leaf: &x509.Certificate{ Leaf: &x509.Certificate{
NotAfter: now.Add(-1 * time.Hour),
NotBefore: now.Add(-2 * time.Hour), NotBefore: now.Add(-2 * time.Hour),
NotAfter: now.Add(-1 * time.Hour),
}, },
}, },
template: &x509.CertificateRequest{}, template: &x509.CertificateRequest{},
@ -159,6 +179,146 @@ func TestRotateCertWaitingForResultError(t *testing.T) {
} }
} }
func TestNewManagerBootstrap(t *testing.T) {
store := &fakeStore{}
cm, err := NewManager(&Config{
Template: &x509.CertificateRequest{},
Usages: []certificates.KeyUsage{},
CertificateStore: store,
BootstrapCertificatePEM: []byte(certificateData),
BootstrapKeyPEM: []byte(privateKeyData),
})
if err != nil {
t.Fatalf("Failed to initialize the certificate manager: %v", err)
}
cert := cm.Current()
if cert == nil {
t.Errorf("Certificate was nil, expected something.")
}
if m, ok := cm.(*manager); !ok {
t.Errorf("Expected a '*manager' from 'NewManager'")
} else if !m.shouldRotate() {
t.Errorf("Expected rotation should happen during bootstrap, but it won't.")
}
}
func TestNewManagerNoBootstrap(t *testing.T) {
now := time.Now()
cert, err := tls.X509KeyPair([]byte(certificateData), []byte(privateKeyData))
if err != nil {
t.Fatalf("Unable to initialize a certificate: %v", err)
}
cert.Leaf = &x509.Certificate{
NotBefore: now.Add(-24 * time.Hour),
NotAfter: now.Add(24 * time.Hour),
}
store := &fakeStore{
cert: &cert,
}
cm, err := NewManager(&Config{
Template: &x509.CertificateRequest{},
Usages: []certificates.KeyUsage{},
CertificateStore: store,
BootstrapCertificatePEM: []byte(certificateData),
BootstrapKeyPEM: []byte(privateKeyData),
})
if err != nil {
t.Fatalf("Failed to initialize the certificate manager: %v", err)
}
currentCert := cm.Current()
if currentCert == nil {
t.Errorf("Certificate was nil, expected something.")
}
if m, ok := cm.(*manager); !ok {
t.Errorf("Expected a '*manager' from 'NewManager'")
} else if m.shouldRotate() {
t.Errorf("Expected rotation should happen during bootstrap, but it won't.")
}
}
func TestGetCurrentCertificateOrBootstrap(t *testing.T) {
cert, err := tls.X509KeyPair([]byte(certificateData), []byte(privateKeyData))
if err != nil {
t.Fatalf("Unable to initialize a certificate: %v", err)
}
testCases := []struct {
description string
storeCert *tls.Certificate
bootstrapCertData []byte
bootstrapKeyData []byte
expectedCert *tls.Certificate
expectedShouldRotate bool
expectedErrMsg string
}{
{
"return cert from store",
&cert,
nil,
nil,
&cert,
false,
"",
},
{
"no cert in store and no bootstrap cert",
nil,
nil,
nil,
nil,
false,
"no cert/key available and no bootstrap cert/key to fall back to",
},
}
for _, tc := range testCases {
t.Run(tc.description, func(t *testing.T) {
store := &fakeStore{
cert: tc.storeCert,
}
certResult, shouldRotate, err := getCurrentCertificateOrBootstrap(
store,
tc.bootstrapCertData,
tc.bootstrapKeyData)
if certResult == nil || tc.expectedCert == nil {
if certResult != tc.expectedCert {
t.Errorf("Got certificate %v, wanted %v", certResult, tc.expectedCert)
}
} else {
if len(certResult.Certificate) != len(tc.expectedCert.Certificate) {
t.Errorf("Got %d certificates, wanted %d", len(certResult.Certificate), len(tc.expectedCert.Certificate))
}
if !bytes.Equal(certResult.Certificate[0], tc.expectedCert.Certificate[0]) {
t.Errorf("Got certificate %v, wanted %v", certResult, tc.expectedCert)
}
}
if shouldRotate != tc.expectedShouldRotate {
t.Errorf("Got shouldRotate %t, wanted %t", shouldRotate, tc.expectedShouldRotate)
}
if err == nil {
if tc.expectedErrMsg != "" {
t.Errorf("Got err %v, wanted %q", err, tc.expectedErrMsg)
}
} else {
if tc.expectedErrMsg == "" || !strings.Contains(err.Error(), tc.expectedErrMsg) {
t.Errorf("Got err %v, wanted %q", err, tc.expectedErrMsg)
}
}
})
}
}
type fakeClientFailureType int type fakeClientFailureType int
const ( const (
@ -233,6 +393,10 @@ type fakeStore struct {
} }
func (s *fakeStore) Current() (*tls.Certificate, error) { func (s *fakeStore) Current() (*tls.Certificate, error) {
if s.cert == nil {
noKeyErr := NoCertKeyError("")
return nil, &noKeyErr
}
return s.cert, nil return s.cert, nil
} }
@ -244,6 +408,11 @@ func (s *fakeStore) Update(certPEM, keyPEM []byte) (*tls.Certificate, error) {
if err != nil { if err != nil {
return nil, err return nil, err
} }
now := time.Now()
s.cert = &cert s.cert = &cert
s.cert.Leaf = &x509.Certificate{
NotBefore: now.Add(-24 * time.Hour),
NotAfter: now.Add(24 * time.Hour),
}
return s.cert, nil return s.cert, nil
} }

View File

@ -148,12 +148,14 @@ func (s *fileStore) Current() (*tls.Certificate, error) {
return loadX509KeyPair(c, k) return loadX509KeyPair(c, k)
} }
return nil, fmt.Errorf("no cert/key files read at %q, (%q, %q) or (%q, %q)", noKeyErr := NoCertKeyError(
pairFile, fmt.Sprintf("no cert/key files read at %q, (%q, %q) or (%q, %q)",
s.certFile, pairFile,
s.keyFile, s.certFile,
s.certDirectory, s.keyFile,
s.keyDirectory) s.certDirectory,
s.keyDirectory))
return nil, &noKeyErr
} }
func loadFile(pairFile string) (*tls.Certificate, error) { func loadFile(pairFile string) (*tls.Certificate, error) {

View File

@ -181,7 +181,7 @@ func TestUpdateSymlinkReplaceExistingSymlink(t *testing.T) {
func TestLoadCertKeyBlocksNoFile(t *testing.T) { func TestLoadCertKeyBlocksNoFile(t *testing.T) {
dir, err := ioutil.TempDir("", "k8s-test-load-cert-key-blocks") dir, err := ioutil.TempDir("", "k8s-test-load-cert-key-blocks")
if err != nil { if err != nil {
t.Fatalf("Unable to created the test directory %q: %v", dir, err) t.Fatalf("Unable to create the test directory %q: %v", dir, err)
} }
defer func() { defer func() {
if err := os.RemoveAll(dir); err != nil { if err := os.RemoveAll(dir); err != nil {
@ -199,7 +199,7 @@ func TestLoadCertKeyBlocksNoFile(t *testing.T) {
func TestLoadCertKeyBlocksEmptyFile(t *testing.T) { func TestLoadCertKeyBlocksEmptyFile(t *testing.T) {
dir, err := ioutil.TempDir("", "k8s-test-load-cert-key-blocks") dir, err := ioutil.TempDir("", "k8s-test-load-cert-key-blocks")
if err != nil { if err != nil {
t.Fatalf("Unable to created the test directory %q: %v", dir, err) t.Fatalf("Unable to create the test directory %q: %v", dir, err)
} }
defer func() { defer func() {
if err := os.RemoveAll(dir); err != nil { if err := os.RemoveAll(dir); err != nil {
@ -220,7 +220,7 @@ func TestLoadCertKeyBlocksEmptyFile(t *testing.T) {
func TestLoadCertKeyBlocksPartialFile(t *testing.T) { func TestLoadCertKeyBlocksPartialFile(t *testing.T) {
dir, err := ioutil.TempDir("", "k8s-test-load-cert-key-blocks") dir, err := ioutil.TempDir("", "k8s-test-load-cert-key-blocks")
if err != nil { if err != nil {
t.Fatalf("Unable to created the test directory %q: %v", dir, err) t.Fatalf("Unable to create the test directory %q: %v", dir, err)
} }
defer func() { defer func() {
if err := os.RemoveAll(dir); err != nil { if err := os.RemoveAll(dir); err != nil {
@ -241,7 +241,7 @@ func TestLoadCertKeyBlocksPartialFile(t *testing.T) {
func TestLoadCertKeyBlocks(t *testing.T) { func TestLoadCertKeyBlocks(t *testing.T) {
dir, err := ioutil.TempDir("", "k8s-test-load-cert-key-blocks") dir, err := ioutil.TempDir("", "k8s-test-load-cert-key-blocks")
if err != nil { if err != nil {
t.Fatalf("Unable to created the test directory %q: %v", dir, err) t.Fatalf("Unable to create the test directory %q: %v", dir, err)
} }
defer func() { defer func() {
if err := os.RemoveAll(dir); err != nil { if err := os.RemoveAll(dir); err != nil {
@ -269,7 +269,7 @@ func TestLoadCertKeyBlocks(t *testing.T) {
func TestLoadFile(t *testing.T) { func TestLoadFile(t *testing.T) {
dir, err := ioutil.TempDir("", "k8s-test-load-cert-key-blocks") dir, err := ioutil.TempDir("", "k8s-test-load-cert-key-blocks")
if err != nil { if err != nil {
t.Fatalf("Unable to created the test directory %q: %v", dir, err) t.Fatalf("Unable to create the test directory %q: %v", dir, err)
} }
defer func() { defer func() {
if err := os.RemoveAll(dir); err != nil { if err := os.RemoveAll(dir); err != nil {
@ -298,7 +298,7 @@ func TestUpdateNoRotation(t *testing.T) {
prefix := "kubelet-server" prefix := "kubelet-server"
dir, err := ioutil.TempDir("", "k8s-test-certstore-current") dir, err := ioutil.TempDir("", "k8s-test-certstore-current")
if err != nil { if err != nil {
t.Fatalf("Unable to created the test directory %q: %v", dir, err) t.Fatalf("Unable to create the test directory %q: %v", dir, err)
} }
defer func() { defer func() {
if err := os.RemoveAll(dir); err != nil { if err := os.RemoveAll(dir); err != nil {
@ -332,7 +332,7 @@ func TestUpdateRotation(t *testing.T) {
prefix := "kubelet-server" prefix := "kubelet-server"
dir, err := ioutil.TempDir("", "k8s-test-certstore-current") dir, err := ioutil.TempDir("", "k8s-test-certstore-current")
if err != nil { if err != nil {
t.Fatalf("Unable to created the test directory %q: %v", dir, err) t.Fatalf("Unable to create the test directory %q: %v", dir, err)
} }
defer func() { defer func() {
if err := os.RemoveAll(dir); err != nil { if err := os.RemoveAll(dir); err != nil {
@ -366,7 +366,7 @@ func TestUpdateWithBadCertKeyData(t *testing.T) {
prefix := "kubelet-server" prefix := "kubelet-server"
dir, err := ioutil.TempDir("", "k8s-test-certstore-current") dir, err := ioutil.TempDir("", "k8s-test-certstore-current")
if err != nil { if err != nil {
t.Fatalf("Unable to created the test directory %q: %v", dir, err) t.Fatalf("Unable to create the test directory %q: %v", dir, err)
} }
defer func() { defer func() {
if err := os.RemoveAll(dir); err != nil { if err := os.RemoveAll(dir); err != nil {
@ -400,7 +400,7 @@ func TestCurrentPairFile(t *testing.T) {
prefix := "kubelet-server" prefix := "kubelet-server"
dir, err := ioutil.TempDir("", "k8s-test-certstore-current") dir, err := ioutil.TempDir("", "k8s-test-certstore-current")
if err != nil { if err != nil {
t.Fatalf("Unable to created the test directory %q: %v", dir, err) t.Fatalf("Unable to create the test directory %q: %v", dir, err)
} }
defer func() { defer func() {
if err := os.RemoveAll(dir); err != nil { if err := os.RemoveAll(dir); err != nil {
@ -437,7 +437,7 @@ func TestCurrentCertKeyFiles(t *testing.T) {
prefix := "kubelet-server" prefix := "kubelet-server"
dir, err := ioutil.TempDir("", "k8s-test-certstore-current") dir, err := ioutil.TempDir("", "k8s-test-certstore-current")
if err != nil { if err != nil {
t.Fatalf("Unable to created the test directory %q: %v", dir, err) t.Fatalf("Unable to create the test directory %q: %v", dir, err)
} }
defer func() { defer func() {
if err := os.RemoveAll(dir); err != nil { if err := os.RemoveAll(dir); err != nil {
@ -469,3 +469,31 @@ func TestCurrentCertKeyFiles(t *testing.T) {
t.Fatalf("Got an empty leaf, expected private data.") t.Fatalf("Got an empty leaf, expected private data.")
} }
} }
func TestCurrentNoFiles(t *testing.T) {
dir, err := ioutil.TempDir("", "k8s-test-certstore-current")
if err != nil {
t.Fatalf("Unable to create the test directory %q: %v", dir, err)
}
defer func() {
if err := os.RemoveAll(dir); err != nil {
t.Errorf("Unable to clean up test directory %q: %v", dir, err)
}
}()
store, err := NewFileStore("kubelet-server", dir, dir, "", "")
if err != nil {
t.Fatalf("Failed to initialize certificate store: %v", err)
}
cert, err := store.Current()
if err == nil {
t.Fatalf("Got no error, expected an error because the cert/key files don't exist.")
}
if _, ok := err.(*NoCertKeyError); !ok {
t.Fatalf("Got error %v, expected NoCertKeyError.", err)
}
if cert != nil {
t.Fatalf("Got certificate, expected no certificate because the cert/key files don't exist.")
}
}