From 91a292df7860cb4375d31995a4ae3daa82473f9f Mon Sep 17 00:00:00 2001 From: Ye Cheng Date: Thu, 8 Feb 2024 00:30:57 -0800 Subject: [PATCH] Watch cert files and reload on change The current webhook does not watch for cert renewal changes. When the cert is renewed, the pod must be restarted to reload the new cert. This commit watches the cert files and reloads the cert when a change is detected. See: #135 --- admission-webhook/cert_reloader.go | 88 +++++++++++++++++++++++++ admission-webhook/cert_reloader_test.go | 52 +++++++++++++++ admission-webhook/go.mod | 1 + admission-webhook/go.sum | 2 + admission-webhook/testdata/cert.pem | 29 ++++++++ admission-webhook/testdata/key.pem | 52 +++++++++++++++ admission-webhook/webhook.go | 16 ++++- 7 files changed, 238 insertions(+), 2 deletions(-) create mode 100644 admission-webhook/cert_reloader.go create mode 100644 admission-webhook/cert_reloader_test.go create mode 100644 admission-webhook/testdata/cert.pem create mode 100644 admission-webhook/testdata/key.pem diff --git a/admission-webhook/cert_reloader.go b/admission-webhook/cert_reloader.go new file mode 100644 index 00000000..011f883f --- /dev/null +++ b/admission-webhook/cert_reloader.go @@ -0,0 +1,88 @@ +package main + +import ( + "crypto/tls" + "sync" + + "github.com/fsnotify/fsnotify" + "github.com/sirupsen/logrus" +) + +type CertReloader struct { + sync.Mutex + certPath string + keyPath string + certificate *tls.Certificate +} + +func NewCertReloader(certPath, keyPath string) *CertReloader { + return &CertReloader{ + certPath: certPath, + keyPath: keyPath, + } +} + +// LoadCertificate loads or reloads the certificate from disk. +func (cr *CertReloader) LoadCertificate() (*tls.Certificate, error) { + cr.Lock() + defer cr.Unlock() + + cert, err := tls.LoadX509KeyPair(cr.certPath, cr.keyPath) + if err != nil { + return nil, err + } + cr.certificate = &cert + return cr.certificate, nil +} + +// GetCertificateFunc returns a function that can be assigned to tls.Config.GetCertificate +func (cr *CertReloader) GetCertificateFunc() func(*tls.ClientHelloInfo) (*tls.Certificate, error) { + return func(chi *tls.ClientHelloInfo) (*tls.Certificate, error) { + return cr.certificate, nil + } +} + +func watchCertFiles(certReloader *CertReloader) { + watcher, err := fsnotify.NewWatcher() + if err != nil { + logrus.Errorf("error creating watcher: %v", err) + } + defer watcher.Close() + + done := make(chan bool) + go func() { + for { + select { + case event, ok := <-watcher.Events: + if !ok { + return + } + if event.Op&fsnotify.Write == fsnotify.Write || event.Op&fsnotify.Rename == fsnotify.Rename { + logrus.Infof("detected change in certificate file: %v", event.Name) + _, err := certReloader.LoadCertificate() + if err != nil { + logrus.Errorf("error reloading certificate: %v", err) + } else { + logrus.Infof("successfully reloaded certificate") + } + } + case err, ok := <-watcher.Errors: + if !ok { + return + } + logrus.Errorf("watcher error: %v", err) + } + } + }() + + err = watcher.Add(certReloader.certPath) + if err != nil { + logrus.Errorf("error watching certificate file: %v", err) + } + err = watcher.Add(certReloader.keyPath) + if err != nil { + logrus.Errorf("error watching key file: %v", err) + } + + <-done +} diff --git a/admission-webhook/cert_reloader_test.go b/admission-webhook/cert_reloader_test.go new file mode 100644 index 00000000..1484f88f --- /dev/null +++ b/admission-webhook/cert_reloader_test.go @@ -0,0 +1,52 @@ +package main + +import ( + "os" + "testing" +) + +// TestCertReloader tests the reloading functionality of the certificate. +func TestCertReloader(t *testing.T) { + // Create temporary cert and key files + tmpCertFile, err := os.CreateTemp("", "cert*.pem") + if err != nil { + t.Fatalf("Failed to create temp cert file: %v", err) + } + defer os.Remove(tmpCertFile.Name()) // clean up + + tmpKeyFile, err := os.CreateTemp("", "key*.pem") + if err != nil { + t.Fatalf("Failed to create temp key file: %v", err) + } + defer os.Remove(tmpKeyFile.Name()) // clean up + + // Write initial cert and key to temp files + initialCertData, _ := os.ReadFile("testdata/cert.pem") + if err := os.WriteFile(tmpCertFile.Name(), initialCertData, 0644); err != nil { + t.Fatalf("Failed to write to temp cert file: %v", err) + } + + initialKeyData, _ := os.ReadFile("testdata/key.pem") + if err := os.WriteFile(tmpKeyFile.Name(), initialKeyData, 0644); err != nil { + t.Fatalf("Failed to write to temp key file: %v", err) + } + + // Setup CertReloader with temp files + certReloader := NewCertReloader(tmpCertFile.Name(), tmpKeyFile.Name()) + _, err = certReloader.LoadCertificate() + if err != nil { + t.Fatalf("Failed to load initial certificate: %v", err) + } + + // Mocking a certificate change by writing new data to the files + newCertData, _ := os.ReadFile("testdata/cert.pem") + if err := os.WriteFile(tmpCertFile.Name(), newCertData, 0644); err != nil { + t.Fatalf("Failed to write new data to cert file: %v", err) + } + + // Simulate reloading + _, err = certReloader.LoadCertificate() + if err != nil { + t.Fatalf("Failed to reload certificate: %v", err) + } +} diff --git a/admission-webhook/go.mod b/admission-webhook/go.mod index 2cc424f7..162c3feb 100644 --- a/admission-webhook/go.mod +++ b/admission-webhook/go.mod @@ -3,6 +3,7 @@ module github.com/kubernetes-sigs/windows-gmsa/admission-webhook go 1.21 require ( + github.com/fsnotify/fsnotify v1.7.0 github.com/mitchellh/go-homedir v1.1.0 github.com/sirupsen/logrus v1.9.3 github.com/stretchr/testify v1.8.4 diff --git a/admission-webhook/go.sum b/admission-webhook/go.sum index 41b5bf59..978362e5 100644 --- a/admission-webhook/go.sum +++ b/admission-webhook/go.sum @@ -4,6 +4,8 @@ github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/emicklei/go-restful/v3 v3.11.0 h1:rAQeMHw1c7zTmncogyy8VvRZwtkmkZ4FxERmMY4rD+g= github.com/emicklei/go-restful/v3 v3.11.0/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc= +github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA= +github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM= github.com/go-logr/logr v1.3.0 h1:2y3SDp0ZXuc6/cjLSZ+Q3ir+QB9T/iG5yYRXqsagWSY= github.com/go-logr/logr v1.3.0/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= github.com/go-openapi/jsonpointer v0.19.6/go.mod h1:osyAmYz/mB/C3I+WsTTSgw1ONzaLJoLCyoi6/zppojs= diff --git a/admission-webhook/testdata/cert.pem b/admission-webhook/testdata/cert.pem new file mode 100644 index 00000000..300db313 --- /dev/null +++ b/admission-webhook/testdata/cert.pem @@ -0,0 +1,29 @@ +-----BEGIN CERTIFICATE----- +MIIFCTCCAvGgAwIBAgIUDlTAXZUZ0DGcutGxcspszdn4lAowDQYJKoZIhvcNAQEL +BQAwFDESMBAGA1UEAwwJbG9jYWxob3N0MB4XDTI0MDIwODA2MDAyN1oXDTM0MDIw +NTA2MDAyN1owFDESMBAGA1UEAwwJbG9jYWxob3N0MIICIjANBgkqhkiG9w0BAQEF +AAOCAg8AMIICCgKCAgEAg1kVSkS0tl8+VMBH8nVwfXy3yNgDS5a8uWAFv62O1vI4 +KEOClNhPaFvihL7ubi3rhCmHoUfbIhe1MMqwV7mEXI+AVObs7Sf8feX5WkW6zJyF +AByH8O37fGFfEsISJBcMwmS5/gCShPfUuG2vr7Zg28brxU+Ixp1DhA87X+A+CEG0 +JTH2LDiKMv86At/olH/IeDOH7j2tD25MThDN+xyKa9u2cpy73GcF822haUtFzmVX +mA8Mw4Qs5B2lMPY3k/C2UaqRDnNFu+0U011hvAGFA4+Jw4Cvpy13/4kQQZ0JHSOD +oy+jtbpqMQJn2oCMQ9DX0WQTS7E4W03y5gKS4v8xkUneAWuWoSwTm8TXRoAXbT3n +ZqDXmdy69ckLiLgn/w5uAeKjeHdk522QiJ2MHqYRLJbzUQ6LsrYdcR3nhh1pgh5K +tdnuz7HQtg77KR9g1X1aAT20SqV9rV85FwWI50dTfg8ehWXOSuXlZMlRUuMOn0a0 +iAyw+rCbaLvmuXwPZxuk/PPW+4lWwE1OqHSjs3iHl/fZM5AfmVS9Av3n3JRD2hrA +2aoOnjiFSjI/9qzcjUl8LTvrzGFt3QWcVRDzMqNQW8qPvBFvxrRcZlPTElRM023p +TO8P3k4n08EGgY+dV9s3xC6dnIkyVp7b2UtEDAC6E53mI2e+wG5uYrKu4QSaM9MC +AwEAAaNTMFEwHQYDVR0OBBYEFCEw8jWYUa8ed+R5O9dxhUSVqBJwMB8GA1UdIwQY +MBaAFCEw8jWYUa8ed+R5O9dxhUSVqBJwMA8GA1UdEwEB/wQFMAMBAf8wDQYJKoZI +hvcNAQELBQADggIBAAE7F7qZ4B5PzqydYRJ0Er39kdfEugs0L3/LYwHfQ4eJKedI +CNXnSW+2kh5kQW9YnXy77VewZ/wtaZyGndNNocRKlN6X2yr97HOtuysdSvHuNmUl +Dyk9brgPcRJK4YizO44DHuQmn0LUhxbeph2VjYxs6B5XEdD6aGFpljWNCHzuWqao +so3uF588lhudSPVkx9VEWHF/N5BKQeJr6gPy1BB8rlSkD8ImxHmq7ledV7ri0mCS +o5WO+17kaLBvyj5H8sN/M+zWPKMHLohe5BXFWwlgl8nGnaXNW0HaKhgyjGTZBZJe +u5kTVQnhTDUo706K4BC8Zz78L6Xcb44FMUIRF5yZ8iKN2M6mPmEQmrE6aKEmiNxc +j0Yfz5MGumog8goYEgOkxp49aI6zojOq7GskDVKo9NxPsfotASriDOhpe106F/yK +cboL1oeL3pAjgICgwdy2pNawjDVNt8cadU4RAxF+m0gpa/xAsGhlz5YKsdOv/7V8 +Lb7KguUyYHmyBFwzJluhBrUWrGpPEKxdjrMbn/9G8b7AbXyV2w/9bwqkLvFU6qyJ +vuv4HpHIywsm9tST8p62RDVRbWlYfBwWIsnz4sraOPJXt8SU9QE3XI3MLw3iiEbs +1oToxKEj+6KbAgaC81cIkohZ+6RYnX+huhhe89Mgg0r7wWnt+huHiKLhGnm/ +-----END CERTIFICATE----- diff --git a/admission-webhook/testdata/key.pem b/admission-webhook/testdata/key.pem new file mode 100644 index 00000000..6f335be9 --- /dev/null +++ b/admission-webhook/testdata/key.pem @@ -0,0 +1,52 @@ +-----BEGIN PRIVATE KEY----- +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 +Qq94HQjGtc+tQXYMH1mJfTWqcfyT +-----END PRIVATE KEY----- diff --git a/admission-webhook/webhook.go b/admission-webhook/webhook.go index 2582c1da..0ad3d0d3 100644 --- a/admission-webhook/webhook.go +++ b/admission-webhook/webhook.go @@ -2,6 +2,7 @@ package main import ( "context" + "crypto/tls" "encoding/json" "fmt" "io/ioutil" @@ -21,7 +22,6 @@ import ( type webhookOperation string -// type gmsaResourceKind string const ( @@ -77,7 +77,19 @@ func (webhook *webhook) start(port int, tlsConfig *tlsConfig, listeningChan chan if tlsConfig == nil { err = webhook.server.Serve(keepAliveListener) } else { - err = webhook.server.ServeTLS(keepAliveListener, tlsConfig.crtPath, tlsConfig.keyPath) + certReloader := NewCertReloader(tlsConfig.crtPath, tlsConfig.keyPath) + _, err = certReloader.LoadCertificate() + if err != nil { + return err + } + + go watchCertFiles(certReloader) + + webhook.server.TLSConfig = &tls.Config{ + GetCertificate: certReloader.GetCertificateFunc(), + } + + err = webhook.server.ServeTLS(keepAliveListener, "", "") } if err != nil {