diff --git a/util/certificate/certificate_manager.go b/util/certificate/certificate_manager.go index e189c847..7b07b26a 100644 --- a/util/certificate/certificate_manager.go +++ b/util/certificate/certificate_manager.go @@ -24,6 +24,7 @@ import ( "crypto/x509" "encoding/pem" "fmt" + "reflect" "sync" "time" @@ -32,6 +33,7 @@ import ( certificates "k8s.io/api/certificates/v1beta1" "k8s.io/apimachinery/pkg/api/errors" utilruntime "k8s.io/apimachinery/pkg/util/runtime" + "k8s.io/apimachinery/pkg/util/sets" "k8s.io/apimachinery/pkg/util/wait" certificatesclient "k8s.io/client-go/kubernetes/typed/certificates/v1beta1" "k8s.io/client-go/util/cert" @@ -75,6 +77,13 @@ type Config struct { // part of rotation. It follows the same rules as the template parameter of // crypto.x509.CreateCertificateRequest in the Go standard libraries. Template *x509.CertificateRequest + // GetTemplate returns 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. + // If no template is available, nil may be returned, and no certificate will be requested. + // If specified, takes precedence over Template. + GetTemplate func() *x509.CertificateRequest // Usages is the types of usages that certificates generated by the manager // can be used for. Usages []certificates.KeyUsage @@ -136,7 +145,10 @@ func (e *NoCertKeyError) Error() string { return string(*e) } type manager struct { certSigningRequestClient certificatesclient.CertificateSigningRequestInterface - template *x509.CertificateRequest + getTemplate func() *x509.CertificateRequest + lastRequestLock sync.Mutex + lastRequest *x509.CertificateRequest + dynamicTemplate bool usages []certificates.KeyUsage certStore Store certAccessLock sync.RWMutex @@ -158,9 +170,15 @@ func NewManager(config *Config) (Manager, error) { return nil, err } + getTemplate := config.GetTemplate + if getTemplate == nil { + getTemplate = func() *x509.CertificateRequest { return config.Template } + } + m := manager{ certSigningRequestClient: config.CertificateSigningRequestClient, - template: config.Template, + getTemplate: getTemplate, + dynamicTemplate: config.GetTemplate != nil, usages: config.Usages, certStore: config.CertificateStore, cert: cert, @@ -215,12 +233,32 @@ func (m *manager) Start() { glog.V(2).Infof("Certificate rotation is enabled.") + templateChanged := make(chan struct{}) go wait.Forever(func() { deadline := m.nextRotationDeadline() if sleepInterval := deadline.Sub(time.Now()); sleepInterval > 0 { glog.V(2).Infof("Waiting %v for next certificate rotation", sleepInterval) - time.Sleep(sleepInterval) + + timer := time.NewTimer(sleepInterval) + defer timer.Stop() + + select { + case <-timer.C: + // unblock when deadline expires + case <-templateChanged: + if reflect.DeepEqual(m.getLastRequest(), m.getTemplate()) { + // if the template now matches what we last requested, restart the rotation deadline loop + return + } + glog.V(2).Infof("Certificate template changed, rotating") + } } + + // Don't enter rotateCerts and trigger backoff if we don't even have a template to request yet + if m.getTemplate() == nil { + return + } + backoff := wait.Backoff{ Duration: 2 * time.Second, Factor: 2, @@ -231,7 +269,18 @@ func (m *manager) Start() { utilruntime.HandleError(fmt.Errorf("Reached backoff limit, still unable to rotate certs: %v", err)) wait.PollInfinite(32*time.Second, m.rotateCerts) } - }, 0) + }, time.Second) + + if m.dynamicTemplate { + go wait.Forever(func() { + // check if the current template matches what we last requested + if !reflect.DeepEqual(m.getLastRequest(), m.getTemplate()) { + // 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. + templateChanged <- struct{}{} + } + }, time.Second) + } } func getCurrentCertificateOrBootstrap( @@ -286,7 +335,7 @@ func getCurrentCertificateOrBootstrap( func (m *manager) rotateCerts() (bool, error) { glog.V(2).Infof("Rotating certificates") - csrPEM, keyPEM, privateKey, err := m.generateCSR() + template, csrPEM, keyPEM, privateKey, err := m.generateCSR() if err != nil { utilruntime.HandleError(fmt.Errorf("Unable to generate a certificate signing request: %v", err)) return false, nil @@ -300,6 +349,9 @@ func (m *manager) rotateCerts() (bool, error) { return false, m.updateServerError(err) } + // Once we've successfully submitted a CSR for this template, record that we did so + m.setLastRequest(template) + // Wait for the certificate to be signed. Instead of one long watch, we retry with slightly longer // intervals each time in order to tolerate failures from the server AND to preserve the liveliness // of the cert manager loop. This creates slightly more traffic against the API server in return @@ -353,6 +405,36 @@ func (m *manager) nextRotationDeadline() time.Time { return time.Now() } + // Ensure the currently held certificate satisfies the requested subject CN and SANs + if template := m.getTemplate(); template != nil { + if template.Subject.CommonName != m.cert.Leaf.Subject.CommonName { + glog.V(2).Infof("Current certificate CN (%s) does not match requested CN (%s), rotating now", m.cert.Leaf.Subject.CommonName, template.Subject.CommonName) + return time.Now() + } + + currentDNSNames := sets.NewString(m.cert.Leaf.DNSNames...) + desiredDNSNames := sets.NewString(template.DNSNames...) + missingDNSNames := desiredDNSNames.Difference(currentDNSNames) + if len(missingDNSNames) > 0 { + glog.V(2).Infof("Current certificate is missing requested DNS names %v, rotating now", missingDNSNames.List()) + return time.Now() + } + + currentIPs := sets.NewString() + for _, ip := range m.cert.Leaf.IPAddresses { + currentIPs.Insert(ip.String()) + } + desiredIPs := sets.NewString() + for _, ip := range template.IPAddresses { + desiredIPs.Insert(ip.String()) + } + missingIPs := desiredIPs.Difference(currentIPs) + if len(missingIPs) > 0 { + glog.V(2).Infof("Current certificate is missing requested IP addresses %v, rotating now", missingIPs.List()) + return time.Now() + } + } + notAfter := m.cert.Leaf.NotAfter totalDuration := float64(notAfter.Sub(m.cert.Leaf.NotBefore)) deadline := m.cert.Leaf.NotBefore.Add(jitteryDuration(totalDuration)) @@ -408,22 +490,38 @@ func (m *manager) updateServerError(err error) error { return nil } -func (m *manager) generateCSR() (csrPEM []byte, keyPEM []byte, key interface{}, err error) { +func (m *manager) generateCSR() (template *x509.CertificateRequest, csrPEM []byte, keyPEM []byte, key interface{}, err error) { // Generate a new private key. privateKey, err := ecdsa.GenerateKey(elliptic.P256(), cryptorand.Reader) if err != nil { - return nil, nil, nil, fmt.Errorf("unable to generate a new private key: %v", err) + return nil, nil, nil, nil, fmt.Errorf("unable to generate a new private key: %v", err) } der, err := x509.MarshalECPrivateKey(privateKey) if err != nil { - return nil, nil, nil, fmt.Errorf("unable to marshal the new key to DER: %v", err) + return nil, nil, nil, nil, fmt.Errorf("unable to marshal the new key to DER: %v", err) } keyPEM = pem.EncodeToMemory(&pem.Block{Type: cert.ECPrivateKeyBlockType, Bytes: der}) - csrPEM, err = cert.MakeCSRFromTemplate(privateKey, m.template) - if err != nil { - return nil, nil, nil, fmt.Errorf("unable to create a csr from the private key: %v", err) + template = m.getTemplate() + if template == nil { + return nil, nil, nil, nil, fmt.Errorf("unable to create a csr, no template available") } - return csrPEM, keyPEM, privateKey, nil + csrPEM, err = cert.MakeCSRFromTemplate(privateKey, template) + if err != nil { + return nil, nil, nil, nil, fmt.Errorf("unable to create a csr from the private key: %v", err) + } + return template, csrPEM, keyPEM, privateKey, nil +} + +func (m *manager) getLastRequest() *x509.CertificateRequest { + m.lastRequestLock.Lock() + defer m.lastRequestLock.Unlock() + return m.lastRequest +} + +func (m *manager) setLastRequest(r *x509.CertificateRequest) { + m.lastRequestLock.Lock() + defer m.lastRequestLock.Unlock() + m.lastRequest = r } diff --git a/util/certificate/certificate_manager_test.go b/util/certificate/certificate_manager_test.go index 6a23f042..299a1d53 100644 --- a/util/certificate/certificate_manager_test.go +++ b/util/certificate/certificate_manager_test.go @@ -186,7 +186,7 @@ func TestSetRotationDeadline(t *testing.T) { NotAfter: tc.notAfter, }, }, - template: &x509.CertificateRequest{}, + getTemplate: func() *x509.CertificateRequest { return &x509.CertificateRequest{} }, usages: []certificates.KeyUsage{}, certificateExpiration: &g, } @@ -221,8 +221,8 @@ func TestRotateCertCreateCSRError(t *testing.T) { NotAfter: now.Add(-1 * time.Hour), }, }, - template: &x509.CertificateRequest{}, - usages: []certificates.KeyUsage{}, + getTemplate: func() *x509.CertificateRequest { return &x509.CertificateRequest{} }, + usages: []certificates.KeyUsage{}, certSigningRequestClient: fakeClient{ failureType: createError, }, @@ -244,8 +244,8 @@ func TestRotateCertWaitingForResultError(t *testing.T) { NotAfter: now.Add(-1 * time.Hour), }, }, - template: &x509.CertificateRequest{}, - usages: []certificates.KeyUsage{}, + getTemplate: func() *x509.CertificateRequest { return &x509.CertificateRequest{} }, + usages: []certificates.KeyUsage{}, certSigningRequestClient: fakeClient{ failureType: watchError, },