diff --git a/Makefile b/Makefile index 76e2eb5a1..7f9a3514d 100644 --- a/Makefile +++ b/Makefile @@ -120,7 +120,7 @@ delete-gatekeeper: helm delete gatekeeper --namespace ${GATEKEEPER_NAMESPACE} .PHONY: test-e2e -test-e2e: +test-e2e: generate-rotation-certs bats -t ${BATS_TESTS_FILE} .PHONY: test-e2e-cli @@ -134,6 +134,13 @@ test-e2e-cli: e2e-dependencies e2e-create-local-registry e2e-notaryv2-setup e2e- generate-certs: ./scripts/generate-tls-certs.sh ${CERT_DIR} ${GATEKEEPER_NAMESPACE} +generate-rotation-certs: + mkdir -p .staging/rotation + mkdir -p .staging/rotation/gatekeeper + + ./scripts/generate-gk-tls-certs.sh .staging/rotation/gatekeeper ${GATEKEEPER_NAMESPACE} + ./scripts/generate-tls-certs.sh .staging/rotation ${GATEKEEPER_NAMESPACE} + install-bats: # Download and install bats curl -sSLO https://github.com/bats-core/bats-core/archive/v${BATS_VERSION}.tar.gz && tar -zxvf v${BATS_VERSION}.tar.gz && bash bats-core-${BATS_VERSION}/install.sh ${GITHUB_WORKSPACE} diff --git a/httpserver/server.go b/httpserver/server.go index 9b3c20239..c8ff507a1 100644 --- a/httpserver/server.go +++ b/httpserver/server.go @@ -18,7 +18,6 @@ package httpserver import ( "context" "crypto/tls" - "crypto/x509" "fmt" "net" "net/http" @@ -145,23 +144,20 @@ func (server *Server) Run() error { logrus.Info(fmt.Sprintf("%s: [%s:%s] [%s:%s]", "starting server using TLS", "certFile", certFile, "keyFile", keyFile)) - if server.CaCertFile != "" { - caCert, err := os.ReadFile(server.CaCertFile) - if err != nil { - panic(err) - } - - clientCAs := x509.NewCertPool() - clientCAs.AppendCertsFromPEM(caCert) - - config := &tls.Config{ - MinVersion: tls.VersionTLS13, - ClientCAs: clientCAs, - ClientAuth: tls.RequireAndVerifyClientCert, - } - svr.TLSConfig = config - logrus.Info(fmt.Sprintf("%s: [%s:%s] ", "loaded client CA certificate for mTLS", "CaFIle", server.CaCertFile)) + tlsCertWatcher, err := NewTLSCertWatcher(certFile, keyFile, server.CaCertFile) + if err != nil { + return err + } + if err = tlsCertWatcher.Start(); err != nil { + return err } + defer tlsCertWatcher.Stop() + + svr.TLSConfig = &tls.Config{ + GetConfigForClient: tlsCertWatcher.GetConfigForClient, + MinVersion: tls.VersionTLS13, + } + if err := svr.ServeTLS(lsnr, certFile, keyFile); err != nil { logrus.Errorf("failed to start server: %v", err) return err diff --git a/httpserver/tlsManager.go b/httpserver/tlsManager.go new file mode 100644 index 000000000..5b5f5d9d8 --- /dev/null +++ b/httpserver/tlsManager.go @@ -0,0 +1,204 @@ +/* +Copyright The Ratify 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 httpserver + +import ( + "context" + "crypto/tls" + "crypto/x509" + "fmt" + "os" + "sync" + "time" + + "github.com/fsnotify/fsnotify" + "github.com/sirupsen/logrus" + "k8s.io/apimachinery/pkg/util/wait" +) + +// This implementation is based on K8s certwatcher: https://github.com/kubernetes-sigs/controller-runtime/blob/main/pkg/certwatcher/certwatcher.go +type TLSCertWatcher struct { + sync.RWMutex + ratifyServerCert *tls.Certificate + clientCACert *x509.CertPool + watcher *fsnotify.Watcher + + ratifyServerCertPath string + ratifyServerKeyPath string + clientCACertPath string +} + +// NewTLSCertWatcher creates a new TLSCertWatcher for ratify tls cert/key paths and client CA cert path +func NewTLSCertWatcher(ratifyServerCertPath, ratifyServerKeyPath, clientCACertPath string) (*TLSCertWatcher, error) { + var err error + certWatcher := &TLSCertWatcher{ + ratifyServerCertPath: ratifyServerCertPath, + ratifyServerKeyPath: ratifyServerKeyPath, + clientCACertPath: clientCACertPath, + } + + if err = certWatcher.ReadCertificates(); err != nil { + return nil, err + } + + certWatcher.watcher, err = fsnotify.NewWatcher() + if err != nil { + return nil, err + } + + return certWatcher, nil +} + +// Start adds the files to watcher and starts the certificate watcher routine +func (t *TLSCertWatcher) Start() error { + files := map[string]struct{}{t.ratifyServerCertPath: {}, t.ratifyServerKeyPath: {}} + if t.clientCACertPath != "" { + files[t.clientCACertPath] = struct{}{} + } + + { + var watchErr error + deadlineCtx, cancel := context.WithTimeout(context.TODO(), 10*time.Second) + defer cancel() + if err := wait.PollUntilWithContext(deadlineCtx, 1*time.Second, func(ctx context.Context) (done bool, err error) { + for f := range files { + if err := t.watcher.Add(f); err != nil { + watchErr = err + return false, nil //nolint:nilerr // we want to keep trying. + } + // remove it from the set + delete(files, f) + } + return true, nil + }); err != nil { + return fmt.Errorf("failed to add watches: %w: %s", err, watchErr.Error()) + } + } + + logrus.Info("Starting TLS certificate watcher") + go t.Watch() + + return nil +} + +// Stop closes the watcher +func (t *TLSCertWatcher) Stop() { + if err := t.watcher.Close(); err != nil { + logrus.Errorf("error closing certificate watcher: %v", err) + } +} + +// ReadCertificates reads the certificates from the cert/key paths +func (t *TLSCertWatcher) ReadCertificates() error { + if t.ratifyServerCertPath == "" || t.ratifyServerKeyPath == "" { + return fmt.Errorf("ratify server cert or key path is empty") + } + + if t.clientCACertPath != "" { + caCert, err := os.ReadFile(t.clientCACertPath) + if err != nil { + return err + } + + clientCAs := x509.NewCertPool() + clientCAs.AppendCertsFromPEM(caCert) + t.Lock() + t.clientCACert = clientCAs + t.Unlock() + } + + ratifyServerCert, err := tls.LoadX509KeyPair(t.ratifyServerCertPath, t.ratifyServerKeyPath) + if err != nil { + return err + } + t.Lock() + t.ratifyServerCert = &ratifyServerCert + t.Unlock() + return nil +} + +// GetConfigForClient returns the tls config for the client use in the TLS Config +func (t *TLSCertWatcher) GetConfigForClient(*tls.ClientHelloInfo) (*tls.Config, error) { + t.RLock() + defer t.RUnlock() + + config := &tls.Config{ + MinVersion: tls.VersionTLS13, + Certificates: []tls.Certificate{*t.ratifyServerCert}, + GetConfigForClient: t.GetConfigForClient, + } + + if t.clientCACert != nil { + config.ClientCAs = t.clientCACert + config.ClientAuth = tls.RequireAndVerifyClientCert + } + return config, nil +} + +func (t *TLSCertWatcher) handleEvent(event fsnotify.Event) { + // Only care about events which may modify the contents of the file. + if !(isWrite(event) || isRemove(event) || isCreate(event)) { + return + } + + logrus.Infof("tls certificate rotation event: %v", event) + + // If the file was removed, re-add the watch. + if isRemove(event) { + if err := t.watcher.Add(event.Name); err != nil { + logrus.Errorf("error re-watching file: %v", err) + } + } + + if err := t.ReadCertificates(); err != nil { + logrus.Errorf("error re-reading certificates: %v", err) + } +} + +// Watch watches the certificate files for changes and terminates on error/stop +func (t *TLSCertWatcher) Watch() { + for { + select { + case event, ok := <-t.watcher.Events: + // Channel is closed. + if !ok { + return + } + + t.handleEvent(event) + + case err, ok := <-t.watcher.Errors: + // Channel is closed. + if !ok { + return + } + + logrus.Errorf("certificate watch error: %v", err) + } + } +} + +func isWrite(event fsnotify.Event) bool { + return event.Op == fsnotify.Write +} + +func isCreate(event fsnotify.Event) bool { + return event.Op == fsnotify.Create +} + +func isRemove(event fsnotify.Event) bool { + return event.Op == fsnotify.Remove +} diff --git a/httpserver/tlsManager_test.go b/httpserver/tlsManager_test.go new file mode 100644 index 000000000..82b4339b4 --- /dev/null +++ b/httpserver/tlsManager_test.go @@ -0,0 +1,378 @@ +/* +Copyright The Ratify 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 httpserver + +import ( + "bytes" + "crypto/tls" + "crypto/x509" + "os" + "path/filepath" + "testing" + "time" + + "github.com/fsnotify/fsnotify" +) + +const ( + firstCertFileName string = "firstCert.crt" + firstKeyFileName string = "firstKey.key" + firstCACertFileName string = "firstCACert.crt" + invalidPath string = "invalid" +) +const firstCertificate string = `-----BEGIN CERTIFICATE----- +MIIDAjCCAeqgAwIBAgIUAbsAmO7kx6m5NgjlMLTvdHNqZtAwDQYJKoZIhvcNAQEL +BQAwKjEPMA0GA1UECgwGUmF0aWZ5MRcwFQYDVQQDDA5SYXRpZnkgUm9vdCBDQTAe +Fw0yMzA1MTgxOTQ5MzVaFw0yNDA1MTcxOTQ5MzVaMCMxITAfBgNVBAMMGHJhdGlm +eS5nYXRla2VlcGVyLXN5c3RlbTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoC +ggEBANDh5yHzotQdPEZtI6S6Grq/85h29Q03iUbH5Bmc8MDKwl0QqX0UY1D1Ue4f +aFNJ+odD2yHdS6t1rCjEw/xdaaqVN82/d0/qBVoIfeUUBSKmrOQcaXIOzHidO2ci +++jPCRWApCay6k32t9G4NDZi99fwgybI19KNA1tmpQEU6pPRi37vljupplBSGFme +eSFPkmbXa/ECX0BjpAfs4if+8pzgIHqWvVT0Wl3SFLhXYfCQERupv/d+DGbTlTif +lWanDmZWLSSlPxgcgjjzQICU0Kj8WW7zxTSejwYCq7L+FL4XcWUurgqSxFNj41qT +cEeDFc1iWewLWCDA7D2wGjH43vUCAwEAAaMnMCUwIwYDVR0RBBwwGoIYcmF0aWZ5 +LmdhdGVrZWVwZXItc3lzdGVtMA0GCSqGSIb3DQEBCwUAA4IBAQBRffIo/nY0JfNr +4IA3AI9fHkGKmwt2/tqEUe8paL+h42MWxa7FrSBMwcqOyhD+mrPDqC4CDqVOq5YY +ymzCuCbHsJqTohioPakFl80XTi03VZgMiySrWOwLtfBKSYxVhHI7jrR4VSLipxMu +Zu5gYBGyM/LEl2Mqg4+US5IMt9DsL83J8nMweIFv8Zsckz/Y4Gm9VGOAfKb2GUan +8etfPff+KRO5Gg4Pl/kwmOU+4vDpn+1PVyPzn3CVIDplcqcpeqjdV9YDBrlz0Ge+ +Qh/RHtx6eyYD5PcLjlv9DO2SslR2kWYV0cJjTrlNGzscLrKSQj3vEJIJ5NKGvcvH +kxxpbB05 +-----END CERTIFICATE-----` +const firstKey string = `-----BEGIN PRIVATE KEY----- +MIIEvwIBADANBgkqhkiG9w0BAQEFAASCBKkwggSlAgEAAoIBAQDQ4ech86LUHTxG +bSOkuhq6v/OYdvUNN4lGx+QZnPDAysJdEKl9FGNQ9VHuH2hTSfqHQ9sh3Uurdawo +xMP8XWmqlTfNv3dP6gVaCH3lFAUipqzkHGlyDsx4nTtnIvvozwkVgKQmsupN9rfR +uDQ2YvfX8IMmyNfSjQNbZqUBFOqT0Yt+75Y7qaZQUhhZnnkhT5Jm12vxAl9AY6QH +7OIn/vKc4CB6lr1U9Fpd0hS4V2HwkBEbqb/3fgxm05U4n5Vmpw5mVi0kpT8YHII4 +80CAlNCo/Flu88U0no8GAquy/hS+F3FlLq4KksRTY+Nak3BHgxXNYlnsC1ggwOw9 +sBox+N71AgMBAAECggEBAJAhYF/wAgnExoN96VtPwwPbUVWBt6NQD9wUL5Nw1Drj +bWvUBG83MzR4ofjiGRVndYQCUWEzlnQP9SQIaYdoWXIIFoJUvBobS2gNdfkscEKx +qZiY9jVqerI7I/MNk67XtNfudNXzHHOBauM97GEetw98eLK5YRp6jLdzwyVU7mvh +qjlSO0PsYWhrRU4slxNsQMPy4aOetcavJFoidCZZbAt+LBVRrLDNRUf5rX99s+s4 +8yp+fGgj6NwxSoDaps6cAYWmLCYk3c7fPbeLokP3Qnmi5cn74qkJ5Y39XdnSSyep +sQl7vDTl67DC4ujc2aVGIB3iasiq7qCF98phesdaQEECgYEA928v0ii4csJN/gqE +d6Bks755BZPpQz//0aiSVJ8MC7zng8BDQ407dHgHWofNxLsdHNkW9FpGL+R/wLSu +fj+1r7kX6iOwWjsJbAETF+hJfWc63Vo/dyeQo+azO1EPtTLlcrpum6rR3HO7fvh2 +sTdPtsxdsE308fOqK4P46qRqs1ECgYEA2B0PttCaF6WP8z1Dm5e2ea92zqBwvaJ3 +oTRRucKu2nXqRPj5GK2yzmWl1NGCaS8O9FqRK9OQFuK5PSjmN6gXHI6kH3BiOhHw +AG+ENlef6pV+E2KqPF8xjqqEUfKN2hEe2FRBfduFam0AIppUB5Y2Ebl0EagQXk9v +3i2YsyyWIGUCgYEAyXHwUP1uDaA7txQA/RPMaLot9WiShHnaYGsJl3NVb0kAg7dI +C/sz6ILAGehukjh0X0Qu+Al3Ew7JI672UTq1RLdAzRL5RLzD0vadAN3Q1xPwTL5o +5S2FCKuOSECatT8Wpu05l+reqMhgYeMPXwBVGdIQhLUzMrjaVks/oGjzpcECgYBt +P7Oz7RwYnB97DRtiSn16YlMi/URA+SKUoYg26c3OrhExsNLrwNNFN2lvfkH4vktH +B4mfqCGNECwoWMaYmCamzwz0v7FIPc0fy0AA4Kb8xXmofxYj0tOQlW6ypnVDKah4 +H4/D+fcl59hLpcyY0TygFSoxys4LfwjEPjSVTxLNaQKBgQCnXKDWjqtgDwmvceQC +DH9dxDSdp/Jrz5GELXJuFzepxtGJS51CpPBV+2YOSyaSzEc9q6Usd4mSO6DCb2rM +A5p2U+ZMyVpdVxM+n2t8UxbqU8DK5Cko+6wR3q/HUXXNOtGYUrSQhQmW4y8ySyE6 +7PGdPM31LcIuIjfPTG15FbNBTg== +-----END PRIVATE KEY----- +` +const firstCACert string = `-----BEGIN CERTIFICATE----- +MIIDNTCCAh2gAwIBAgIUODI+rSGQO2RuEyMftSqojjxcaRQwDQYJKoZIhvcNAQEL +BQAwKjEPMA0GA1UECgwGUmF0aWZ5MRcwFQYDVQQDDA5SYXRpZnkgUm9vdCBDQTAe +Fw0yMzA1MTgxOTQ5MzVaFw0yMzA1MTkxOTQ5MzVaMCoxDzANBgNVBAoMBlJhdGlm +eTEXMBUGA1UEAwwOUmF0aWZ5IFJvb3QgQ0EwggEiMA0GCSqGSIb3DQEBAQUAA4IB +DwAwggEKAoIBAQDO43Y+WkYsH1jKoHtzdQMii2hdrXEzltXCLrM/P1OhERZyFSlZ +QubAwzNG41gT4/cBCRTafC45hw3aTKSCY7tphgGn6qXOelMLKu9yf5I/awukylHQ +F6+H+T40tWlRLT+bXFZtByIacZmoGoWzbZRmGBKoParNZCH3vfNvb7jYM4Ehb+JA +vPEKvz0Iu7l9jh0qgS6v9wx0GBz7vDFhTQHoVFXRcY9Vb9FdnNHEx+Lc7UoW0/kJ +LKTYqLsYJXZRqfmnAocpqxjaDODBzs9+k6ulrrqoZjyZmkbVbaFSLLvIVAeHzuwL +0gQbz8OEIP+uqH+7VhGb6vbKknAMwja+EbHDAgMBAAGjUzBRMB0GA1UdDgQWBBSD +TaR4b0ju72jRo1RMffIHt3fIcDAfBgNVHSMEGDAWgBSDTaR4b0ju72jRo1RMffIH +t3fIcDAPBgNVHRMBAf8EBTADAQH/MA0GCSqGSIb3DQEBCwUAA4IBAQBGVtPpetE8 +Fe9aAN/ZLmaIj14FfvAy7LOSv1AIDNFgNLTbo25z+IH7cQf1Ws967BqlaW79gg8y +k9en7wfLBy3G/teU00bOy2eYtPIsVwLgrpIbVU6VYeUg1i86/pk/tNzOiDRj06qO +Dztv/HJIGGcLwj1T2yiiFnizgRlwmUSVi1BDu4c34A0P9OGoZPSZ97/hhHauK81o +WqlYtBTNTFx4eisM277wPKV8iHNGqULVGtADGmU0WWsWPDmHytmBaE5tF/NifRrF +1oSTxUCtw319hyHUFX5xHq2REEN+tbxTUbVsgXVTrZmnuSxN2WFAfvuPVKxQ9RD9 +ve39ZodlqIAz +-----END CERTIFICATE----- +` +const secondCertificate string = `-----BEGIN CERTIFICATE----- +MIIDAjCCAeqgAwIBAgIUKM/3jH+txmBUCQAOTc93FjzJUP0wDQYJKoZIhvcNAQEL +BQAwKjEPMA0GA1UECgwGUmF0aWZ5MRcwFQYDVQQDDA5SYXRpZnkgUm9vdCBDQTAe +Fw0yMzA1MjMyMTUzMDNaFw0yNDA1MjIyMTUzMDNaMCMxITAfBgNVBAMMGHJhdGlm +eS5nYXRla2VlcGVyLXN5c3RlbTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoC +ggEBALcOqIaafGjT4aRc4OLQCK+2pSEHgZSKdv/pRBDLT6+Tbe8vrK9eLRhuoEIw +k2QPeO7gQnvT+R8zlqOsS0l8echaXjrZypF8D2FACgn2nUotJFMMJvV552vKhq01 +MxkD0nlhQE12TOWtR/fNavyf+GcyQW2rM72Nwj4RfdHQ56wpN2Q9WRRITWXpqy6M +RWnaVhLA9tRtSk0SRwPIk5xrLNDlVRQiw8GnAL7lK4d3n+dDdFoKHyn61pGVauav +Lf7Sd1yUoSqplEGWjA3R/T8j3sKQjOL/SeJqXI0vsniRTfIFMmlFhoTIxROVCIa4 +AVDCLdQaH+9wwp29NyjqJZ0ajFsCAwEAAaMnMCUwIwYDVR0RBBwwGoIYcmF0aWZ5 +LmdhdGVrZWVwZXItc3lzdGVtMA0GCSqGSIb3DQEBCwUAA4IBAQDDx3Z9Z5kwSa24 +3AJZpgNIRsrnoqumqPnYt555U8oZAJKl175tHlDbJAFaRXK4cq1Ua2Q+h0ytzulo +kTi+B45PJjSdi4P62SDxJoqNaiqFoKUA98z32os/FTcyJoXDC7y7DoVpqjJkuSjH +I130FBsc+IBUeHqjSbPRoaNXOYOA36YoMtV2wJ2uPmG1wLbfyk8vqWUv4u30g7z1 +36bQN1yigqNZEO0gM0pOBDodW1OhyNE83d3UHdmCcx3pejbWNWbAEbnuMluGqNLx +CGq38sYWjgy1eHctMF/MpMcCK3Nm64YYvmIwBa0wl7EJArZJLWMGpgB4MC8fwqam ++4RirFh7 +-----END CERTIFICATE----- +` +const secondKey string = `-----BEGIN PRIVATE KEY----- +MIIEwAIBADANBgkqhkiG9w0BAQEFAASCBKowggSmAgEAAoIBAQC3DqiGmnxo0+Gk +XODi0AivtqUhB4GUinb/6UQQy0+vk23vL6yvXi0YbqBCMJNkD3ju4EJ70/kfM5aj +rEtJfHnIWl462cqRfA9hQAoJ9p1KLSRTDCb1eedryoatNTMZA9J5YUBNdkzlrUf3 +zWr8n/hnMkFtqzO9jcI+EX3R0OesKTdkPVkUSE1l6asujEVp2lYSwPbUbUpNEkcD +yJOcayzQ5VUUIsPBpwC+5SuHd5/nQ3RaCh8p+taRlWrmry3+0ndclKEqqZRBlowN +0f0/I97CkIzi/0nialyNL7J4kU3yBTJpRYaEyMUTlQiGuAFQwi3UGh/vcMKdvTco +6iWdGoxbAgMBAAECggEBAKwQMiX7Vc8uwZw91QA8rL2E/yfRp2IY2IvpFZp3kBon +iKDXfgiEi/y4FxjAEfpudKyLzNIZx8MlOYX07/tN7iZ9kq7cggRHySkPCaCd1vCf +B9KrzH7WK8ls3zQ1mib8Kbz/xXJKLTOBsfDhe5ujPdi6KzfLQWH9ukOfK1Wpd+mg +aMAt2txGkQYgFFX+m00zAxCXPs9gqgsiQvsE3HSP8PSRnOaeYIz2me0y7UWkn+Rq +jbVvtbb1VDG6A2xe3bpFWzJuybHuE13nq7zK8MwLDaYSQL15JcQhnsBXl5SnHHJJ +LvRFXxLhCBpcMO2LfnlsGr2aMqbHvzgcNcLKjB9hc5ECgYEA46JVBcYNKAPhPbN/ +xjFQ4I/gmzyVPE/D5iGTnSwc2XEh5/f5S3BI7WzhSA0j0MtOo6LFC5x8JwYKCcno +geUM9GH4sI8UAmkZbquGcRU1yPGTj9LM4uBqvNL4nrSuiS+eY71MEU9a5Fw5JGYa +fdNLw7SkgluprJ9o6fNc/YxbzgMCgYEAzd5MGe4uUv9+r8o4g4KqmJmgw7FYkw7r +1hEp5eN9bvQvQRMONzhoQfDSC+DARtdTcir5JUMJH1CBsh1+alW+QgM3P8Bklqdq +yx9iYHyUqqnNx5mv46NtlhYObxy+9CVjuyVjZT29hfkl2BOlw0TmaGuUv+MvQj6p +wNKoxJFvRMkCgYEAmcwY89CvHOUaLqzzXH3/benn0BqrndcqvXbcHCosx8EHLoo9 +NfoEW93fi+XM2Ao09JxJ06GDxH3xFFIFtJWEHi1/cBMLauGFnF9pc0foUf7eOyMq +6PLFSxSjg98BuZChzDOejGd4OqgQt4YAyhiTrQOEzsqNpiMCKGcT4f8OG+8CgYEA +sSnf1eTaasTC8mcVkV9OjnqPFjm1nwCVRiiJJPRMCsMLM3ZBopXhavXi3SPydERz +5GlE9aMl45P1uSGWm83kKIz569wW9GtpBRqiH6S2j9QHagFBk6Yd9a5Ph6F2V0ch +93jqe8LRKc1KmxP1cAEIQ85pOWU6U0j37x+a62a5GbkCgYEA4QPZpUBU+tiLeQRn +8Jh6G6OELoMHf1Ik6dtDFGM2Cnjmu5iUyRDSsyEQEvRD81RgNbaeIbvagpCCafLJ +IaJp+XVCACjQmmJxJzMPXPjGIQnmk6VrDXttJKQA49DAEZ/aboXGSxjNZiNW3orB +US+cjMyfbSZlnfuxmH5YXqX10co= +-----END PRIVATE KEY----- +` +const secondCACert string = `-----BEGIN CERTIFICATE----- +MIIDNTCCAh2gAwIBAgIUdZS8ouQu+djZ+H+LqqOCVkw89N8wDQYJKoZIhvcNAQEL +BQAwKjEPMA0GA1UECgwGUmF0aWZ5MRcwFQYDVQQDDA5SYXRpZnkgUm9vdCBDQTAe +Fw0yMzA1MjMyMTUzMDNaFw0yMzA1MjQyMTUzMDNaMCoxDzANBgNVBAoMBlJhdGlm +eTEXMBUGA1UEAwwOUmF0aWZ5IFJvb3QgQ0EwggEiMA0GCSqGSIb3DQEBAQUAA4IB +DwAwggEKAoIBAQDaMktJqu7h89TLP/JGNtU0f1zCxQEdiVJZQpDxHHVbwp7fW+j7 +MF68N7AGIVgMXuAsu/ewtA0Y6M+zr8ZJ3sBxWWX//QHoNqOIsGDS8Jx0jsG8448q +0wnkrwWMMiz8Vw182byPKOPlrnrdZ65KjuXXBtWR0LOaZXqQ1e7UA+AloBY8OTk7 +T10ZuyolB/PL8nNIjCesVzTvzHRdR+WC446XeXED68IpV/7uInzzpV/+z1k3foR5 +/wFK/p5Qt5SGcQCacIWBWFTXNLOggZgEpQBZbdO0jQF0fqjAEfksjzgngLALQ+GF +Zp1N2DrmC6RXirvCtnK6Ctto6hgaetWFz7mFAgMBAAGjUzBRMB0GA1UdDgQWBBTP +fzcZabuyIPwAZxgqVQyhmt2J8jAfBgNVHSMEGDAWgBTPfzcZabuyIPwAZxgqVQyh +mt2J8jAPBgNVHRMBAf8EBTADAQH/MA0GCSqGSIb3DQEBCwUAA4IBAQBLB87eJAld +zp8is33YCyu1iQcfXhzMCI/vQVwieV7YPQPYSrn+QQete02n8jzibd2QhPrQbZa5 +mYejaR7fVlJ8KWkhOZAr9nKor7oC3rBywQ9lQhKIUOJr4REPcr11j+iZACcHq/xW +CQ6putzEi4jjDcRQ3bJEEGTcYSQt8KYLzuCYUyS0kjU5Qu9i6Obv4ZhT5/JBIYr9 ++cyT75CQ6D1qyQB0zjtJ1oPWBux5Poix46TGRXXeeR50qEb+bIdNa24aJ+F32iXV +A2lm1FHtdZBdx6x23Oz6FY6YM4jk6CAVwQCkfOsolEVtQqC7IytHrKc+EMVYTjdx +WbAZC9hFRlJ1 +-----END CERTIFICATE----- +` + +func TestNewTLSCertWatcher_Expected(t *testing.T) { + // setup temp dir with test certs/key + tmpDir, err := os.MkdirTemp("", "test-certs") + if err != nil { + t.Fatalf("temp dir creation failed %v", err) + } + defer os.RemoveAll(tmpDir) + + certFileName := filepath.Join(tmpDir, firstCertFileName) + if err = os.WriteFile(certFileName, []byte(firstCertificate), 0600); err != nil { + t.Fatalf("cert file creation failed %v", err) + } + keyFileName := filepath.Join(tmpDir, firstKeyFileName) + if err = os.WriteFile(keyFileName, []byte(firstKey), 0600); err != nil { + t.Fatalf("cert key creation failed %v", err) + } + caFileName := filepath.Join(tmpDir, firstCACertFileName) + if err = os.WriteFile(caFileName, []byte(firstCACert), 0600); err != nil { + t.Fatalf("ca cert file creation failed %v", err) + } + // initialize cert watcher with empty paths + var cw *TLSCertWatcher + _, err = NewTLSCertWatcher("", "", "") + if err == nil { + t.Errorf("Expected error, got %v", err) + } + // initialize cert watcher with valid paths + cw, err = NewTLSCertWatcher(certFileName, keyFileName, caFileName) + if err != nil { + t.Errorf("Expected no error, got %v", err) + } + + // check if cert watcher is initialized correctly + if cw.ratifyServerCertPath != certFileName { + t.Errorf("Expected %s, got %s", certFileName, cw.ratifyServerCertPath) + } + if cw.ratifyServerKeyPath != keyFileName { + t.Errorf("Expected %s, got %s", keyFileName, cw.ratifyServerKeyPath) + } + if cw.clientCACertPath != caFileName { + t.Errorf("Expected %s, got %s", caFileName, cw.clientCACertPath) + } + if cw.ratifyServerCert == nil { + t.Errorf("Expected ratifyServerCert to be set") + } + if cw.clientCACert == nil { + t.Errorf("Expected clientCACert to be set") + } + if cw.watcher == nil { + t.Errorf("Expected watcher to be set") + } +} + +func TestReadCertificates_Expected(t *testing.T) { + // setup temp dir with test certs/key + tmpDir, err := os.MkdirTemp("", "test-certs") + if err != nil { + t.Fatalf("temp dir creation failed %v", err) + } + defer os.RemoveAll(tmpDir) + + certFileName := filepath.Join(tmpDir, firstCertFileName) + if err = os.WriteFile(certFileName, []byte(firstCertificate), 0600); err != nil { + t.Fatalf("cert file creation failed %v", err) + } + keyFileName := filepath.Join(tmpDir, firstKeyFileName) + if err = os.WriteFile(keyFileName, []byte(firstKey), 0600); err != nil { + t.Fatalf("cert key creation failed %v", err) + } + caFileName := filepath.Join(tmpDir, firstCACertFileName) + if err = os.WriteFile(caFileName, []byte(firstCACert), 0600); err != nil { + t.Fatalf("ca cert file creation failed %v", err) + } + + // test with empty cert/key paths + cw := &TLSCertWatcher{} + if err := cw.ReadCertificates(); err == nil { + t.Errorf("Expected error, got %v", err) + } + + // test with invalid ca cert path + cw.ratifyServerCertPath = invalidPath + cw.clientCACertPath = invalidPath + cw.ratifyServerKeyPath = invalidPath + if err := cw.ReadCertificates(); err == nil { + t.Errorf("Expected error, got %v", err) + } + + // test with invalid cert/key paths + cw.clientCACertPath = caFileName + if err := cw.ReadCertificates(); err == nil { + t.Errorf("Expected error, got %v", err) + } + + // test with valid cert/key paths + cw.ratifyServerCertPath = certFileName + cw.ratifyServerKeyPath = keyFileName + if err := cw.ReadCertificates(); err != nil { + t.Errorf("Expected no error, got %v", err) + } +} + +func TestCertRotation(t *testing.T) { + // setup temp dir with test certs/key + tmpDir, err := os.MkdirTemp("", "test-certs") + if err != nil { + t.Fatalf("temp dir creation failed %v", err) + } + defer os.RemoveAll(tmpDir) + + certFileName := filepath.Join(tmpDir, firstCertFileName) + if err = os.WriteFile(certFileName, []byte(firstCertificate), 0600); err != nil { + t.Fatalf("cert file creation failed %v", err) + } + keyFileName := filepath.Join(tmpDir, firstKeyFileName) + if err = os.WriteFile(keyFileName, []byte(firstKey), 0600); err != nil { + t.Fatalf("cert key creation failed %v", err) + } + caFileName := filepath.Join(tmpDir, firstCACertFileName) + if err = os.WriteFile(caFileName, []byte(firstCACert), 0600); err != nil { + t.Fatalf("ca cert file creation failed %v", err) + } + + // set up new cert watcher + var cw *TLSCertWatcher + cw, err = NewTLSCertWatcher(certFileName, keyFileName, caFileName) + if err != nil { + t.Errorf("Expected no error, got %v", err) + } + + // start cert watcher + if err = cw.Start(); err != nil { + t.Errorf("Expected no error, got %v", err) + } + defer cw.Stop() + + // load actual cert bundle and ca cert + actualCertBundle, err := tls.LoadX509KeyPair(certFileName, keyFileName) + if err != nil { + t.Errorf("Expected no error, got %v", err) + } + actualCaKey := x509.NewCertPool() + actualCaKey.AppendCertsFromPEM([]byte(firstCACert)) + + // check if certs match + if !bytes.Equal(actualCertBundle.Certificate[0], cw.ratifyServerCert.Certificate[0]) { + t.Errorf("Expected ratify certs to match") + } + if !actualCaKey.Equal(cw.clientCACert) { + t.Errorf("Expected client CA certs to match") + } + + // update cert/key files for rotation + if err = os.WriteFile(certFileName, []byte(secondCertificate), 0600); err != nil { + t.Fatalf("second cert file creation failed %v", err) + } + if err = os.WriteFile(keyFileName, []byte(secondKey), 0600); err != nil { + t.Fatalf("cert key creation failed %v", err) + } + if err = os.WriteFile(caFileName, []byte(secondCACert), 0600); err != nil { + t.Fatalf("second ca cert file creation failed %v", err) + } + + // wait for cert rotation (watcher is not instant) + time.Sleep(1 * time.Second) + + // reload actual cert bundle and ca cert + actualCertBundle, err = tls.LoadX509KeyPair(certFileName, keyFileName) + if err != nil { + t.Errorf("Expected no error, got %v", err) + } + actualCaKey = x509.NewCertPool() + actualCaKey.AppendCertsFromPEM([]byte(secondCACert)) + + // check if updated certs match + if !bytes.Equal(actualCertBundle.Certificate[0], cw.ratifyServerCert.Certificate[0]) { + t.Errorf("Expected ratify certs to match") + } + if !actualCaKey.Equal(cw.clientCACert) { + t.Errorf("Expected client CA certs to match") + } +} + +func TestIsWrite_Expected(t *testing.T) { + actual := fsnotify.Event{Op: fsnotify.Write} + if !isWrite(actual) { + t.Errorf("Expected true, got false") + } +} + +func TestIsCreate_Expected(t *testing.T) { + actual := fsnotify.Event{Op: fsnotify.Create} + if !isCreate(actual) { + t.Errorf("Expected true, got false") + } +} + +func TestIsRemove_Expected(t *testing.T) { + actual := fsnotify.Event{Op: fsnotify.Remove} + if !isRemove(actual) { + t.Errorf("Expected true, got false") + } +} diff --git a/scripts/generate-gk-tls-certs.sh b/scripts/generate-gk-tls-certs.sh new file mode 100755 index 000000000..d53f70147 --- /dev/null +++ b/scripts/generate-gk-tls-certs.sh @@ -0,0 +1,42 @@ +#!/usr/bin/env bash +##-------------------------------------------------------------------- +# +# Copyright The Ratify 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. +# +##-------------------------------------------------------------------- + +set -o errexit +set -o nounset +set -o pipefail + +ns=${2:-gatekeeper-system} +CERT_DIR=$1 + +generate() { + # generate CA key and certificate + echo "Generating CA key and certificate for gatekeeper..." + openssl genrsa -out ca.key 2048 + openssl req -new -x509 -days 365 -key ca.key -subj "/O=gatekeeper/CN=gatekeeper-ca" -out ca.crt + + # generate server key and certificate + echo "Generating server key and certificate for gatekeeper..." + openssl genrsa -out server.key 2048 + openssl req -newkey rsa:2048 -nodes -keyout server.key -subj "/CN=gatekeeper-webhook-service.${ns}.svc" -out server.csr + openssl x509 -req -extfile <(printf "subjectAltName=DNS:gatekeeper-webhook-service.${ns}.svc") -days 365 -in server.csr -CA ca.crt -CAkey ca.key -CAcreateserial -out server.crt +} + +mkdir -p ${CERT_DIR} +pushd "${CERT_DIR}" +generate +popd diff --git a/scripts/generate-tls-certs.sh b/scripts/generate-tls-certs.sh index d52afecfd..92071517c 100755 --- a/scripts/generate-tls-certs.sh +++ b/scripts/generate-tls-certs.sh @@ -36,7 +36,6 @@ generate() { openssl x509 -req -extfile <(printf "subjectAltName=DNS:ratify.${ns}") -days 365 -in server.csr -CA ca.crt -CAkey ca.key -CAcreateserial -out server.crt } -rm -r ${CERT_DIR} || true mkdir -p ${CERT_DIR} pushd "${CERT_DIR}" generate diff --git a/test/bats/test.bats b/test/bats/test.bats index 61e602f2a..75c74510e 100644 --- a/test/bats/test.bats +++ b/test/bats/test.bats @@ -414,3 +414,32 @@ SLEEP_TIME=1 run kubectl run demo-leaf2 --namespace default --image=registry:5000/notation:leafSigned assert_failure } + +@test "validate ratify/gatekeeper tls cert rotation" { + teardown() { + wait_for_process ${WAIT_TIME} ${SLEEP_TIME} 'kubectl delete pod demo --namespace default --force --ignore-not-found=true' + } + + # update Providers to use the new CA + run kubectl get Provider ratify-mutation-provider -o json | jq --arg ca "$(cat .staging/rotation/ca.crt | base64)" '.spec.caBundle=$ca' | kubectl replace -f - + run kubectl get Provider ratify-provider -o json | jq --arg ca "$(cat .staging/rotation/ca.crt | base64)" '.spec.caBundle=$ca' | kubectl replace -f - + + # update the ratify tls secret to use the new tls cert and key + run kubectl get secret ratify-tls -n gatekeeper-system -o json | jq --arg cert "$(cat .staging/rotation/server.crt | base64)" --arg key "$(cat .staging/rotation/server.key | base64)" '.data["tls.key"]=$key | .data["tls.crt"]=$cert'| kubectl replace -f - + + # update the gatekeeper webhook server tls secret to use the new cert bundle + run kubectl get Secret gatekeeper-webhook-server-cert -n gatekeeper-system -o json | jq --arg caCert "$(cat .staging/rotation/gatekeeper/ca.crt | base64)" --arg caKey "$(cat .staging/rotation/gatekeeper/ca.key | base64)" --arg tlsCert "$(cat .staging/rotation/gatekeeper/server.crt | base64)" --arg tlsKey "$(cat .staging/rotation/gatekeeper/server.key | base64)" '.data["ca.crt"]=$caCert | .data["ca.key"]=$caKey | .data["tls.crt"]=$tlsCert | .data["tls.key"]=$tlsKey' | kubectl replace -f - + + # volume projection can take up to 90 seconds + sleep 100 + + # verify that the verification succeeds + run kubectl apply -f ./library/default/template.yaml + assert_success + sleep 5 + run kubectl apply -f ./library/default/samples/constraint.yaml + assert_success + sleep 5 + run kubectl run demo --namespace default --image=registry:5000/notation:signed + assert_success +}