diff --git a/cmd/gtoken-webhook/go.mod b/cmd/gtoken-webhook/go.mod index 9e0bcfd..bc17550 100644 --- a/cmd/gtoken-webhook/go.mod +++ b/cmd/gtoken-webhook/go.mod @@ -3,6 +3,7 @@ module github.com/doitintl/gtoken-webhook go 1.17 require ( + github.com/fsnotify/fsnotify v1.5.1 github.com/google/go-cmp v0.5.7 github.com/pkg/errors v0.9.1 github.com/prometheus/client_golang v1.12.1 diff --git a/cmd/gtoken-webhook/main.go b/cmd/gtoken-webhook/main.go index 5ff3f85..cce7718 100644 --- a/cmd/gtoken-webhook/main.go +++ b/cmd/gtoken-webhook/main.go @@ -2,14 +2,18 @@ package main import ( "context" + "crypto/tls" "fmt" "math/rand" "net/http" "os" + "path/filepath" "runtime" "strings" + "sync" "time" + "github.com/fsnotify/fsnotify" "github.com/pkg/errors" "github.com/prometheus/client_golang/prometheus" "github.com/prometheus/client_golang/prometheus/promhttp" @@ -67,6 +71,73 @@ const ( limitsMemory = "50Mi" ) +type certLoader struct { + certPath string + keyPath string + mu sync.RWMutex + cert *tls.Certificate +} + +func newCertLoader(certPath, keyPath string) (*certLoader, error) { + cl := &certLoader{certPath: certPath, keyPath: keyPath} + if err := cl.loadCert(); err != nil { + return nil, err + } + return cl, nil +} + +func (cl *certLoader) loadCert() error { + cert, err := tls.LoadX509KeyPair(cl.certPath, cl.keyPath) + if err != nil { + return err + } + cl.mu.Lock() + cl.cert = &cert + cl.mu.Unlock() + return nil +} + +func (cl *certLoader) getCertificate(*tls.ClientHelloInfo) (*tls.Certificate, error) { + cl.mu.RLock() + defer cl.mu.RUnlock() + return cl.cert, nil +} + +func (cl *certLoader) watch(stopCh <-chan struct{}) { + watcher, err := fsnotify.NewWatcher() + if err != nil { + log.Fatal(err) + } + defer watcher.Close() + + // Watch directory where secret is mounted + certDir := filepath.Dir(cl.certPath) + if err := watcher.Add(certDir); err != nil { + logger.WithError(err).Fatalf("failed to add watcher for cert at path: %s", certDir) + } + + logger.Infof("watching directory %s for TLS cert changes", certDir) + + for { + select { + case event := <-watcher.Events: + // Secret projected volumes often replace ..data directory, watch for creation of it + if event.Op&fsnotify.Create != 0 && filepath.Base(event.Name) == "..data" { + logger.Warnf("cert/key file changed (%s), reloading...", event) + if err := cl.loadCert(); err != nil { + logger.WithError(err).Error("failed to reload cert") + } + logger.Infof("reloaded TLS certificate") + } + case err := <-watcher.Errors: + logger.WithError(err).Error("watcher error") + case <-stopCh: + logger.Infof("stopping cert watcher") + return + } + } +} + type mutatingWebhook struct { k8sClient kubernetes.Interface image string @@ -351,6 +422,7 @@ func runWebhook(c *cli.Context) error { listenAddress := c.String("listen-address") tlsCertFile := c.String("tls-cert-file") tlsPrivateKeyFile := c.String("tls-private-key-file") + watchCert := c.Bool("watch-cert") if len(telemetryAddress) > 0 { // Serving metrics without TLS on separated address @@ -359,12 +431,33 @@ func runWebhook(c *cli.Context) error { mux.Handle("/metrics", promhttp.Handler()) } + srv := &http.Server{ + Addr: listenAddress, + Handler: mux, + } if tlsCertFile == "" && tlsPrivateKeyFile == "" { logger.Infof("listening on http://%s", listenAddress) - err = http.ListenAndServe(listenAddress, mux) + if err := srv.ListenAndServe(); err != nil { + logger.WithError(err).Fatal("error serving webhook") + } } else { + cl, err := newCertLoader(tlsCertFile, tlsPrivateKeyFile) + if err != nil { + logger.WithError(err).Fatal("failed to load TLS certs") + } + if watchCert { + stopCh := make(chan struct{}) + go cl.watch(stopCh) // background watcher + } + + srv.TLSConfig = &tls.Config{ + GetCertificate: cl.getCertificate, + MinVersion: tls.VersionTLS12, + } logger.Infof("listening on https://%s", listenAddress) - err = http.ListenAndServeTLS(listenAddress, tlsCertFile, tlsPrivateKeyFile, mux) + if err := srv.ListenAndServeTLS("", ""); err != nil { + logger.WithError(err).Fatal("error serving webhook") + } } if err != nil { @@ -450,6 +543,10 @@ func main() { Usage: "token file name", Value: tokenFileName, }, + cli.BoolFlag{ + Name: "watch-cert", + Usage: "watch certificate and reload then when changes", + }, }, Usage: "mutation admission webhook", Description: "run mutation admission webhook server",