291 lines
8.3 KiB
Go
291 lines
8.3 KiB
Go
/*
|
|
Copyright 2020 The cert-manager 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 tls
|
|
|
|
import (
|
|
"context"
|
|
"crypto"
|
|
"crypto/tls"
|
|
"crypto/x509"
|
|
"errors"
|
|
"fmt"
|
|
"sync"
|
|
"time"
|
|
|
|
"github.com/go-logr/logr"
|
|
"golang.org/x/sync/errgroup"
|
|
"k8s.io/apimachinery/pkg/util/wait"
|
|
|
|
cmapi "github.com/cert-manager/cert-manager/pkg/apis/certmanager/v1"
|
|
logf "github.com/cert-manager/cert-manager/pkg/logs"
|
|
"github.com/cert-manager/cert-manager/pkg/util/pki"
|
|
)
|
|
|
|
type Authority interface {
|
|
// Run starts the authority and blocks until it is stopped or an error occurs.
|
|
Run(ctx context.Context) error
|
|
|
|
// WatchRotation adds a watcher to the authority that will notify the given
|
|
// channel when the root CA has been rotated. It is guaranteed to post a message
|
|
// to the channel when the root CA has been rotated and the channel is not full.
|
|
WatchRotation(ch chan<- struct{})
|
|
|
|
// StopWatchingRotation removes the watcher from the authority.
|
|
StopWatchingRotation(ch chan<- struct{})
|
|
|
|
// Sign signs the given certificate template and returns the signed certificate.
|
|
// WARNING: The WatchRotation method should be called before Sign to ensure that
|
|
// the rotation of the CA used to sign the certificate in this call is detected.
|
|
Sign(template *x509.Certificate) (*x509.Certificate, error)
|
|
}
|
|
|
|
// DynamicSource provides certificate data for a golang HTTP server by
|
|
// automatically generating certificates using an authority.SignFunc.
|
|
type DynamicSource struct {
|
|
// DNSNames that will be set on certificates this source produces.
|
|
DNSNames []string
|
|
|
|
// The authority used to sign certificate templates.
|
|
Authority Authority
|
|
|
|
RetryInterval time.Duration
|
|
|
|
log logr.Logger
|
|
|
|
cachedCertificate *tls.Certificate
|
|
lock sync.Mutex
|
|
}
|
|
|
|
var _ CertificateSource = &DynamicSource{}
|
|
|
|
// Implements Runnable (https://github.com/kubernetes-sigs/controller-runtime/blob/56159419231e985c091ef3e7a8a3dee40ddf1d73/pkg/manager/manager.go#L287)
|
|
func (f *DynamicSource) Start(ctx context.Context) error {
|
|
f.log = logf.FromContext(ctx)
|
|
|
|
if f.RetryInterval == 0 {
|
|
f.RetryInterval = 1 * time.Second
|
|
}
|
|
|
|
group, ctx := errgroup.WithContext(ctx)
|
|
group.Go(func() error {
|
|
if err := f.Authority.Run(ctx); err != nil {
|
|
return fmt.Errorf("failed to run certificate authority: %w", err)
|
|
}
|
|
|
|
if ctx.Err() == nil {
|
|
return fmt.Errorf("certificate authority stopped unexpectedly")
|
|
}
|
|
|
|
// Context was cancelled, return nil
|
|
return nil
|
|
})
|
|
|
|
// channel which will be notified when the authority has rotated its root CA
|
|
// We start watching the rotation of the root CA before we start generating
|
|
// certificates to ensure we don't miss any rotations.
|
|
rotationChan := make(chan struct{}, 1)
|
|
f.Authority.WatchRotation(rotationChan)
|
|
defer f.Authority.StopWatchingRotation(rotationChan)
|
|
|
|
nextRenewCh := make(chan time.Time, 1)
|
|
|
|
// initially fetch a certificate from the signing CA
|
|
if err := f.tryRegenerateCertificate(ctx, nextRenewCh); err != nil {
|
|
if err := group.Wait(); err != nil {
|
|
return err
|
|
}
|
|
|
|
if errors.Is(err, context.Canceled) {
|
|
return nil
|
|
}
|
|
|
|
return err
|
|
}
|
|
|
|
// channel which will be notified when the leaf certificate reaches 2/3 of its lifetime
|
|
// and needs to be renewed
|
|
renewalChan := make(chan struct{})
|
|
group.Go(func() error {
|
|
// At this point, we expect to have one renewal moment
|
|
// in the channel, so we can start the timer with that value
|
|
var renewMoment time.Time
|
|
select {
|
|
case renewMoment = <-nextRenewCh:
|
|
// We recevieved a renew moment
|
|
default:
|
|
// This should never happen
|
|
panic("Unreacheable")
|
|
}
|
|
|
|
for {
|
|
if done := func() bool {
|
|
timer := time.NewTimer(time.Until(renewMoment))
|
|
defer timer.Stop()
|
|
|
|
// Wait for the timer to expire, or for a new renewal moment to be received
|
|
select {
|
|
case <-ctx.Done():
|
|
// context was cancelled, return nil
|
|
return true
|
|
case <-timer.C:
|
|
// Continue to the next select to try to send a message on renewalChan
|
|
case renewMoment = <-nextRenewCh:
|
|
// We recevieved a renew moment, next loop iteration will update the timer
|
|
return false
|
|
}
|
|
|
|
// Try to send a message on renewalChan, but also allow for the context to be
|
|
// cancelled.
|
|
select {
|
|
case <-ctx.Done():
|
|
// context was cancelled, return nil
|
|
return true
|
|
case renewalChan <- struct{}{}:
|
|
// Message was sent on channel
|
|
}
|
|
|
|
return false
|
|
}(); done {
|
|
return nil
|
|
}
|
|
}
|
|
})
|
|
|
|
// check the current certificate in case it needs updating
|
|
if err := func() error {
|
|
for {
|
|
// regenerate the serving certificate if the root CA has been rotated
|
|
select {
|
|
// check if the context has been cancelled
|
|
case <-ctx.Done():
|
|
return ctx.Err()
|
|
|
|
// trigger regeneration if the root CA has been rotated
|
|
case <-rotationChan:
|
|
f.log.V(logf.InfoLevel).Info("Detected root CA rotation - regenerating serving certificates")
|
|
|
|
// trigger regeneration if a renewal is required
|
|
case <-renewalChan:
|
|
f.log.V(logf.InfoLevel).Info("cert-manager webhook certificate requires renewal, regenerating", "DNSNames", f.DNSNames)
|
|
}
|
|
|
|
if err := f.tryRegenerateCertificate(ctx, nextRenewCh); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
}(); err != nil {
|
|
if err := group.Wait(); err != nil {
|
|
return err
|
|
}
|
|
|
|
if errors.Is(err, context.Canceled) {
|
|
return nil
|
|
}
|
|
|
|
return err
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// Implements LeaderElectionRunnable (https://github.com/kubernetes-sigs/controller-runtime/blob/56159419231e985c091ef3e7a8a3dee40ddf1d73/pkg/manager/manager.go#L305)
|
|
func (f *DynamicSource) NeedLeaderElection() bool {
|
|
return false
|
|
}
|
|
|
|
func (f *DynamicSource) GetCertificate(*tls.ClientHelloInfo) (*tls.Certificate, error) {
|
|
f.lock.Lock()
|
|
defer f.lock.Unlock()
|
|
if f.cachedCertificate == nil {
|
|
return nil, ErrNotAvailable
|
|
}
|
|
return f.cachedCertificate, nil
|
|
}
|
|
|
|
func (f *DynamicSource) Healthy() bool {
|
|
return f.cachedCertificate != nil
|
|
}
|
|
|
|
func (f *DynamicSource) tryRegenerateCertificate(ctx context.Context, nextRenewCh chan<- time.Time) error {
|
|
return wait.PollUntilContextCancel(ctx, f.RetryInterval, true, func(ctx context.Context) (done bool, err error) {
|
|
if err := f.regenerateCertificate(nextRenewCh); err != nil {
|
|
f.log.Error(err, "Failed to generate serving certificate, retrying...", "interval", f.RetryInterval)
|
|
return false, nil
|
|
}
|
|
|
|
return true, nil
|
|
})
|
|
}
|
|
|
|
// regenerateCertificate will trigger the cached certificate and private key to
|
|
// be regenerated by requesting a new certificate from the authority.
|
|
func (f *DynamicSource) regenerateCertificate(nextRenew chan<- time.Time) error {
|
|
f.log.V(logf.DebugLevel).Info("Generating new ECDSA private key")
|
|
pk, err := pki.GenerateECPrivateKey(384)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// create the certificate template to be signed
|
|
template := &x509.Certificate{
|
|
Version: 3,
|
|
PublicKeyAlgorithm: x509.ECDSA,
|
|
PublicKey: pk.Public(),
|
|
DNSNames: f.DNSNames,
|
|
KeyUsage: x509.KeyUsageDigitalSignature | x509.KeyUsageKeyEncipherment,
|
|
ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth},
|
|
}
|
|
|
|
f.log.V(logf.DebugLevel).Info("Signing new serving certificate")
|
|
cert, err := f.Authority.Sign(template)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
f.log.V(logf.DebugLevel).Info("Signed new serving certificate")
|
|
|
|
return f.updateCertificate(pk, cert, nextRenew)
|
|
}
|
|
|
|
func (f *DynamicSource) updateCertificate(pk crypto.Signer, cert *x509.Certificate, nextRenewCh chan<- time.Time) error {
|
|
f.lock.Lock()
|
|
defer f.lock.Unlock()
|
|
|
|
pkData, err := pki.EncodePrivateKey(pk, cmapi.PKCS8)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
certData, err := pki.EncodeX509(cert)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
bundle, err := tls.X509KeyPair(certData, pkData)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
f.cachedCertificate = &bundle
|
|
certDuration := cert.NotAfter.Sub(cert.NotBefore)
|
|
// renew the certificate 1/3 of the time before its expiry
|
|
nextRenewCh <- cert.NotAfter.Add(certDuration / -3)
|
|
f.log.V(logf.InfoLevel).Info("Updated cert-manager TLS certificate", "DNSNames", f.DNSNames)
|
|
|
|
return nil
|
|
}
|