From ca940b6abf360ab94c3ecb93ce5622f553681809 Mon Sep 17 00:00:00 2001 From: Arron Francis Date: Tue, 29 Apr 2025 18:12:07 +0100 Subject: [PATCH] feat: add support for reloading certs when renewed --- cmd/root.go | 12 +++++- pkg/utils/tlsutil.go | 91 ++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 102 insertions(+), 1 deletion(-) create mode 100644 pkg/utils/tlsutil.go diff --git a/cmd/root.go b/cmd/root.go index 7d2121b4..ad92a3f4 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -23,6 +23,7 @@ package cmd import ( "context" + "crypto/tls" "fmt" "net/http" "os" @@ -34,6 +35,7 @@ import ( "github.com/estahn/k8s-image-swapper/pkg/registry" "github.com/estahn/k8s-image-swapper/pkg/secrets" "github.com/estahn/k8s-image-swapper/pkg/types" + "github.com/estahn/k8s-image-swapper/pkg/utils" "github.com/estahn/k8s-image-swapper/pkg/webhook" homedir "github.com/mitchellh/go-homedir" "github.com/prometheus/client_golang/prometheus/promhttp" @@ -151,7 +153,15 @@ A mutating webhook for Kubernetes, pointing the images to a new location.`, log.Info().Msgf("Listening on %v", cfg.ListenAddress) //err = http.ListenAndServeTLS(":8080", cfg.certFile, cfg.keyFile, whHandler) if cfg.TLSCertFile != "" && cfg.TLSKeyFile != "" { - if err := srv.ListenAndServeTLS(cfg.TLSCertFile, cfg.TLSKeyFile); err != nil { + kpr, err := utils.NewKeypairReloader(cfg.TLSCertFile, cfg.TLSKeyFile) + if err != nil { + log.Err(err).Msg("Failed to load key pair") + os.Exit(1) + } + + // this will check if there are new certs before every tls handshake + srv.TLSConfig = &tls.Config{GetCertificate: kpr.GetCertificateFunc()} + if err := srv.ListenAndServeTLS("", ""); err != nil { log.Err(err).Msg("error serving webhook") os.Exit(1) } diff --git a/pkg/utils/tlsutil.go b/pkg/utils/tlsutil.go new file mode 100644 index 00000000..2d61540e --- /dev/null +++ b/pkg/utils/tlsutil.go @@ -0,0 +1,91 @@ +package utils + +import ( + "crypto/tls" + "path" + "sync" + + "github.com/fsnotify/fsnotify" + "github.com/rs/zerolog/log" +) + +// KeypairReloader structs holds cert path and certs +type KeypairReloader struct { + certMu sync.RWMutex + cert *tls.Certificate + tlsCertFile string + tlsKeyFile string +} + +// NewKeypairReloader will load certs on first run and trigger a goroutine for fsnotify watcher +func NewKeypairReloader(tlsCertFile, tlsKeyFile string) (*KeypairReloader, error) { + result := &KeypairReloader{ + tlsCertFile: tlsCertFile, + tlsKeyFile: tlsKeyFile, + } + cert, err := tls.LoadX509KeyPair(tlsCertFile, tlsKeyFile) + if err != nil { + return nil, err + } + result.cert = &cert + + // creates a new file watcher + watcher, err := fsnotify.NewWatcher() + if err != nil { + return nil, err + } + + defer func() { + if err != nil { + watcher.Close() + } + }() + + // Notify on changes to the cert directory + if err := watcher.Add(path.Dir(tlsCertFile)); err != nil { + return nil, err + } + + go func() { + for { + select { + // watch for events + case event := <-watcher.Events: + // Watch for changes to the tlsCertFile + if event.Name == tlsCertFile { + log.Info().Msg("Reloading certs") + if err := result.reload(); err != nil { + log.Err(err).Msg("Could not load new certs") + } + } + + // watch for errors + case err := <-watcher.Errors: + log.Err(err).Msg("Watcher error") + } + } + }() + + return result, nil +} + +// reload loads updated cert and key whenever they are updated +func (kpr *KeypairReloader) reload() error { + newCert, err := tls.LoadX509KeyPair(kpr.tlsCertFile, kpr.tlsKeyFile) + if err != nil { + return err + } + kpr.certMu.Lock() + defer kpr.certMu.Unlock() + kpr.cert = &newCert + return nil +} + +// GetCertificateFunc will return function which will be used as tls.Config.GetCertificate +func (kpr *KeypairReloader) GetCertificateFunc() func(*tls.ClientHelloInfo) (*tls.Certificate, error) { + return func(clientHello *tls.ClientHelloInfo) (*tls.Certificate, error) { + kpr.certMu.RLock() + defer kpr.certMu.RUnlock() + return kpr.cert, nil + } +}