cert-manager/pkg/server/tls/dynamic_source.go
Tim Ramlot 085136068a
fix misspell linter
Signed-off-by: Tim Ramlot <42113979+inteon@users.noreply.github.com>
2024-04-29 15:21:07 +02:00

294 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{}, 1)
group.Go(func() error {
var renewMoment time.Time
for {
if done := func() bool {
var timerChannel <-chan time.Time
if !renewMoment.IsZero() {
timer := time.NewTimer(time.Until(renewMoment))
defer timer.Stop()
renewMoment = time.Time{}
timerChannel = timer.C
}
// 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 <-timerChannel:
// 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
}
// the renewal channel has a buffer of 1 - drop event if we are already issuing
select {
case renewalChan <- struct{}{}:
default:
}
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 {
f.lock.Lock()
defer f.lock.Unlock()
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(ctx, 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(ctx context.Context, 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(ctx, pk, cert, nextRenew)
}
func (f *DynamicSource) updateCertificate(ctx context.Context, 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
renewMoment := cert.NotAfter.Add(certDuration / -3)
select {
case <-ctx.Done():
return nil
case nextRenewCh <- renewMoment:
}
f.log.V(logf.InfoLevel).Info("Updated cert-manager TLS certificate", "DNSNames", f.DNSNames)
return nil
}