diff --git a/admission-webhook/cert_reloader.go b/admission-webhook/cert_reloader.go new file mode 100644 index 00000000..6580fe7c --- /dev/null +++ b/admission-webhook/cert_reloader.go @@ -0,0 +1,101 @@ +package main + +import ( + "crypto/tls" + "sync" + + "github.com/fsnotify/fsnotify" + "github.com/sirupsen/logrus" +) + +type CertLoader interface { + CertPath() string + KeyPath() string + LoadCertificate() (*tls.Certificate, error) +} + +type CertReloader struct { + sync.Mutex + certPath string + keyPath string + certificate *tls.Certificate +} + +func NewCertReloader(certPath, keyPath string) *CertReloader { + return &CertReloader{ + certPath: certPath, + keyPath: keyPath, + } +} + +func (cr *CertReloader) CertPath() string { + return cr.certPath +} + +func (cr *CertReloader) KeyPath() string { + return cr.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(certLoader CertLoader) { + watcher, err := fsnotify.NewWatcher() + if err != nil { + logrus.Errorf("error creating watcher: %v", err) + } + + go func() { + defer watcher.Close() + + 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 := certLoader.LoadCertificate() + if err != nil { + logrus.Errorf("error reloading certificate: %v", err) + } else { + logrus.Infof("successfully reloaded certificate") + } + } + case err, ok := <-watcher.Errors: + if !ok { + logrus.Errorf("watcher error returned !ok: %v", err) + return + } + logrus.Errorf("watcher error: %v", err) + } + } + }() + + err = watcher.Add(certLoader.CertPath()) + if err != nil { + logrus.Fatalf("error watching certificate file: %v", err) + } + err = watcher.Add(certLoader.KeyPath()) + if err != nil { + logrus.Fatalf("error watching key file: %v", err) + } +} diff --git a/admission-webhook/cert_reloader_test.go b/admission-webhook/cert_reloader_test.go new file mode 100644 index 00000000..463a2c44 --- /dev/null +++ b/admission-webhook/cert_reloader_test.go @@ -0,0 +1,114 @@ +package main + +import ( + "crypto/tls" + "os" + "testing" + + "github.com/stretchr/testify/assert" +) + +// 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) + } +} + +type mockCertLoader struct { + certPath string + keyPath string + loadCertFunc func() (*tls.Certificate, error) +} + +func (m *mockCertLoader) CertPath() string { + return m.certPath +} + +func (m *mockCertLoader) KeyPath() string { + return m.keyPath +} + +func (m *mockCertLoader) LoadCertificate() (*tls.Certificate, error) { + return m.loadCertFunc() +} + +func TestWatchingCertFiles(t *testing.T) { + tmpCertFile, err := os.CreateTemp("", "cert*.pem") + if err != nil { + t.Fatalf("Failed to create temp cert file: %v", err) + } + defer os.Remove(tmpCertFile.Name()) + + tmpKeyFile, err := os.CreateTemp("", "key*.pem") + if err != nil { + t.Fatalf("Failed to create temp key file: %v", err) + } + defer os.Remove(tmpKeyFile.Name()) + + loadCertFuncChan := make(chan bool) + + cl := &mockCertLoader{ + certPath: tmpCertFile.Name(), + keyPath: tmpKeyFile.Name(), + loadCertFunc: func() (*tls.Certificate, error) { + loadCertFuncChan <- true + return &tls.Certificate{}, nil + }, + } + + go func() { + defer close(loadCertFuncChan) + + called := <-loadCertFuncChan + assert.True(t, called) + }() + + watchCertFiles(cl) + + 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) + } + + <-loadCertFuncChan +} 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/integration_tests/integration_test.go b/admission-webhook/integration_tests/integration_test.go index 43af37e4..6d5c8e3d 100644 --- a/admission-webhook/integration_tests/integration_test.go +++ b/admission-webhook/integration_tests/integration_test.go @@ -2,6 +2,7 @@ package integrationtests import ( "context" + "encoding/base64" "encoding/json" "fmt" "html/template" @@ -365,6 +366,42 @@ func TestDeployV1CredSpecGetAllVersions(t *testing.T) { assert.Equal(t, v1alpha1CredSpec.Object["credSpec"], v1CredSpec.Object["credSpec"]) } +func TestPossibleToUpdatePodWithNewCert(t *testing.T) { + /** TODO: + * - update the webhook pod to use the new flag + * - make a request to create a pod to make sure it works (already done) + * - get a blessed certificate from the API server + * (https://github.com/kubernetes-sigs/windows-gmsa/blob/141/admission-webhook/deploy/create-signed-cert.sh) + * - update existing secret in place and wait for the pod to get new secrets which can take time + * (https://kubernetes.io/docs/concepts/configuration/secret/#using-secrets-as-files-from-a-pod) - similar to what you are doing here + * - kubectl exec into the running pod to see that the secret changed + * (using utils like https://github.com/ycheng-kareo/windows-gmsa/blob/watch-reload-cert/admission-webhook/integration_tests/kube.go#L199) + * - make a request to create a pod to verify that it still works (pod := waitForPodToComeUp(t, testConfig.Namespace, "app="+testName)) + * - add a separate test to verify that requests to the webhook always return during this process + */ + testName := "possible-to-update-pod-with-new-cert" + credSpecTemplates := []string{"credspec-0"} + newSecretTemplate := "new-secret" + templates := []string{"credspecs-users-rbac-role", "service-account", "sa-rbac-binding", "single-pod-with-container-level-gmsa"} + + testConfig, tearDownFunc := integrationTestSetup(t, testName, credSpecTemplates, templates) + defer tearDownFunc() + + pod := waitForPodToComeUp(t, testConfig.Namespace, "app="+testName) + assert.Equal(t, expectedCredSpec0, extractContainerCredSpecContents(t, pod, testName)) + + // read test cert & key + newCert, _ := os.ReadFile("../testdata/cert.pem") + newKey, _ := os.ReadFile("../testdata/key.pem") + testConfig.Cert = base64.StdEncoding.EncodeToString(newCert) + testConfig.Key = base64.StdEncoding.EncodeToString(newKey) + + // apply the new cert & key pair + renderedTemplate := renderTemplate(t, testConfig, newSecretTemplate) + success, _, _ := applyManifest(t, renderedTemplate) + assert.True(t, success) +} + /* Helpers */ type testConfig struct { @@ -378,6 +415,8 @@ type testConfig struct { RoleBindingName string Image string ExtraSpecLines []string + Cert string + Key string } // integrationTestSetup creates a new namespace to play in, and returns a function to diff --git a/admission-webhook/integration_tests/templates/new-secret.yml b/admission-webhook/integration_tests/templates/new-secret.yml new file mode 100644 index 00000000..03d226fe --- /dev/null +++ b/admission-webhook/integration_tests/templates/new-secret.yml @@ -0,0 +1,8 @@ +apiVersion: v1 +kind: Secret +metadata: + name: {{ .TestName }} + namespace: {{ .Namespace }} +data: + tls_private_key: {{ .Key }} + tls_certificate: {{ .Cert }} diff --git a/admission-webhook/main.go b/admission-webhook/main.go index a0447c49..c04a4dd0 100644 --- a/admission-webhook/main.go +++ b/admission-webhook/main.go @@ -1,6 +1,7 @@ package main import ( + "flag" "fmt" "os" "strconv" @@ -13,12 +14,15 @@ import ( func main() { initLogrus() + enableCertReload := flag.Bool("cert-reload", false, "enable certificate reload") + flag.Parse() + kubeClient, err := createKubeClient() if err != nil { panic(err) } - webhook := newWebhook(kubeClient) + webhook := newWebhookWithOptions(kubeClient, WithCertReload(*enableCertReload)) tlsConfig := &tlsConfig{ crtPath: env("TLS_CRT"), 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..06f772d4 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 ( @@ -38,6 +38,7 @@ const ( type webhook struct { server *http.Server client kubeClientInterface + config *WebhookConfig } type podAdmissionError struct { @@ -46,8 +47,33 @@ type podAdmissionError struct { pod *corev1.Pod } +type WebhookConfig struct { + EnableCertReload bool +} + +type WebhookOption func(*WebhookConfig) + +func WithCertReload(enabled bool) WebhookOption { + return func(cfg *WebhookConfig) { + cfg.EnableCertReload = enabled + } +} + func newWebhook(client kubeClientInterface) *webhook { - return &webhook{client: client} + return newWebhookWithOptions(client) +} + +func newWebhookWithOptions(client kubeClientInterface, options ...WebhookOption) *webhook { + config := &WebhookConfig{EnableCertReload: false} + + for _, option := range options { + option(config) + } + + return &webhook{ + client: client, + config: config, + } } // start is a blocking call. @@ -77,7 +103,23 @@ 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) + if webhook.config.EnableCertReload { + 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, "", "") + } else { + err = webhook.server.ServeTLS(keepAliveListener, tlsConfig.crtPath, tlsConfig.keyPath) + } } if err != nil { diff --git a/admission-webhook/webhook_http_test.go b/admission-webhook/webhook_http_test.go index 7c50a114..98228bc2 100644 --- a/admission-webhook/webhook_http_test.go +++ b/admission-webhook/webhook_http_test.go @@ -170,6 +170,52 @@ func TestHTTPWebhook(t *testing.T) { }) } +func TestStartHTTPWebhookWithCertReload(t *testing.T) { + authorizedToUseCredSpec := true + + kubeClient := &dummyKubeClient{ + isAuthorizedToUseCredSpecFunc: func(ctx context.Context, serviceAccountName, namespace, credSpecName string) (authorized bool, reason string) { + assert.Equal(t, dummyServiceAccoutName, serviceAccountName) + assert.Equal(t, dummyNamespace, namespace) + assert.Equal(t, dummyCredSpecName, credSpecName) + + return authorizedToUseCredSpec, "bogus reason" + }, + retrieveCredSpecContentsFunc: func(ctx context.Context, credSpecName string) (contents string, httpCode int, err error) { + assert.Equal(t, dummyCredSpecName, credSpecName) + + contents = dummyCredSpecContents + return + }, + } + + webhook := newWebhookWithOptions(kubeClient, WithCertReload(true)) + port := getAvailablePort(t) + tlsConfig := &tlsConfig{ + crtPath: "testdata/cert.pem", + keyPath: "testdata/key.pem", + } + + listeningChan := make(chan interface{}) + go func() { + assert.Nil(t, webhook.start(port, tlsConfig, listeningChan)) + }() + defer webhook.stop() + + select { + case <-listeningChan: + for { + if webhook.server.TLSConfig != nil { + break + } + time.Sleep(10 * time.Millisecond) + } + assert.NotNil(t, webhook.server.TLSConfig.GetCertificate) + case <-time.After(5 * time.Second): + t.Fatalf("Timed out waiting for HTTP server to start listening on %d", port) + } +} + func startHTTPServer(t *testing.T, kubeClient *dummyKubeClient) (int, func()) { webhook := newWebhook(kubeClient) port := getAvailablePort(t) diff --git a/admission-webhook/webhook_test.go b/admission-webhook/webhook_test.go index 37769d06..5c6c354a 100644 --- a/admission-webhook/webhook_test.go +++ b/admission-webhook/webhook_test.go @@ -413,6 +413,18 @@ func TestValidateUpdateRequest(t *testing.T) { }) } +func TestDefaultWebhookConfig(t *testing.T) { + expectedCertReload := false + webhook := newWebhookWithOptions(nil, WithCertReload(expectedCertReload)) + assert.Equal(t, expectedCertReload, webhook.config.EnableCertReload) +} + +func TestSetWebhookConfig(t *testing.T) { + expectedCertReload := true + webhook := newWebhookWithOptions(nil, WithCertReload(expectedCertReload)) + assert.Equal(t, expectedCertReload, webhook.config.EnableCertReload) +} + func TestEqualStringPointers(t *testing.T) { ptrToString := func(s *string) string { if s == nil { diff --git a/charts/README.md b/charts/README.md index 6fa3d032..1c617adf 100644 --- a/charts/README.md +++ b/charts/README.md @@ -35,6 +35,7 @@ The following table lists the configurable parameters of the latest GMSA chart a | `certificates.certManager.version` | version of cert manager | | | `certificates.caBundle` | cert-manager disabled, add self-signed ca.crt in base64 format | | | `certificates.secretName` | cert-manager disabled, upload certs data as k8s secretName | `gmsa-server-cert` | +| `certificates.certReload.enabled` | enable cert reload on changes | `false` | | `credential.enabled` | enable creation of GMSA Credential | `true` | | `credential.domainJoinConfig.dnsName` | DNS Domain Name | | | `credential.domainJoinConfig.dnsTreeName` | DNS Domain Name Root | | diff --git a/charts/gmsa/templates/deployment.yaml b/charts/gmsa/templates/deployment.yaml index e1f4ef04..f171181c 100644 --- a/charts/gmsa/templates/deployment.yaml +++ b/charts/gmsa/templates/deployment.yaml @@ -56,6 +56,8 @@ spec: {{- if .Values.securityContext }} securityContext: {{ toYaml .Values.securityContext | nindent 12 }} {{- end }} + args: + - --cert-reload={{ .Values.certificates.certReload.enabled }} volumes: - name: tls secret: diff --git a/charts/gmsa/values.yaml b/charts/gmsa/values.yaml index 0c60a7a8..55c381ae 100644 --- a/charts/gmsa/values.yaml +++ b/charts/gmsa/values.yaml @@ -7,6 +7,9 @@ certificates: caBundle: "" # If cert-manager integration is disabled, upload certs data (ca.crt, tls.crt and tls.key) as k8s secretName in the namespace secretName: gmsa-server-cert + certReload: + # Enable cert reload when the certs change + enabled: false credential: enabled: false