From e19256a2b8ac9812a4e33857f902cefedff21f80 Mon Sep 17 00:00:00 2001 From: Alex Kalenyuk Date: Wed, 11 May 2022 17:30:55 +0300 Subject: [PATCH] Allow TLS ciphers to be configured on webhook server Some operators might want to respect cluster-wide TLS ciphers, which means that these will eventually have to be passed down to the webhook server. Signed-off-by: Alex Kalenyuk --- pkg/webhook/server.go | 56 ++++++++++++++++++++++++++++++++++++++ pkg/webhook/server_test.go | 42 +++++++++++++++++++++++++++- 2 files changed, 97 insertions(+), 1 deletion(-) diff --git a/pkg/webhook/server.go b/pkg/webhook/server.go index 5bdc847e17..4b1e4d9929 100644 --- a/pkg/webhook/server.go +++ b/pkg/webhook/server.go @@ -41,6 +41,9 @@ import ( // DefaultPort is the default port that the webhook server serves. var DefaultPort = 9443 +// ExposedTLSConfig is used in unit tests to ensure propagation of tls related configurables to server. +var ExposedTLSConfig *tls.Config + // Server is an admission webhook server that can serve traffic and // generates related k8s resources for deploying. // @@ -76,6 +79,10 @@ type Server struct { // "", "1.0", "1.1", "1.2" and "1.3" only ("" is equivalent to "1.0" for backwards compatibility) TLSMinVersion string + // TLSCiphers is used to specify the cipher algorithms that are negotiated + // during the TLS handshake, refer to https://pkg.go.dev/crypto/tls#CipherSuites + TLSCiphers []string + // WebhookMux is the multiplexer that handles different webhooks. WebhookMux *http.ServeMux @@ -204,6 +211,48 @@ func tlsVersion(version string) (uint16, error) { } } +// tlsCipherSuitesIDs converts cipher suite names +// to the values accepted by tls.Config (for example 0xc02b). +func tlsCipherSuitesIDs(names []string) []uint16 { + // ref: https://www.iana.org/assignments/tls-parameters/tls-parameters.xml + var idByName = map[string]uint16{ + // TLS 1.2 + "ECDHE-ECDSA-AES128-GCM-SHA256": tls.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256, + "ECDHE-RSA-AES128-GCM-SHA256": tls.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256, + "ECDHE-ECDSA-AES256-GCM-SHA384": tls.TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384, + "ECDHE-RSA-AES256-GCM-SHA384": tls.TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384, + "ECDHE-ECDSA-CHACHA20-POLY1305": tls.TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256, + "ECDHE-RSA-CHACHA20-POLY1305": tls.TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256, + "ECDHE-ECDSA-AES128-SHA256": tls.TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA256, + "ECDHE-RSA-AES128-SHA256": tls.TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA256, + "AES128-GCM-SHA256": tls.TLS_RSA_WITH_AES_128_GCM_SHA256, + "AES256-GCM-SHA384": tls.TLS_RSA_WITH_AES_256_GCM_SHA384, + "AES128-SHA256": tls.TLS_RSA_WITH_AES_128_CBC_SHA256, + + // TLS 1 + "ECDHE-ECDSA-AES128-SHA": tls.TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA, + "ECDHE-RSA-AES128-SHA": tls.TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA, + "ECDHE-ECDSA-AES256-SHA": tls.TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA, + "ECDHE-RSA-AES256-SHA": tls.TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA, + + // SSL 3 + "AES128-SHA": tls.TLS_RSA_WITH_AES_128_CBC_SHA, + "AES256-SHA": tls.TLS_RSA_WITH_AES_256_CBC_SHA, + "DES-CBC3-SHA": tls.TLS_RSA_WITH_3DES_EDE_CBC_SHA, + } + for _, cipherSuite := range tls.CipherSuites() { + idByName[cipherSuite.Name] = cipherSuite.ID + } + + ids := []uint16{} + for _, name := range names { + if id, ok := idByName[name]; ok { + ids = append(ids, id) + } + } + return ids +} + // Start runs the server. // It will install the webhook related resources depend on the server configuration. func (s *Server) Start(ctx context.Context) error { @@ -254,6 +303,13 @@ func (s *Server) Start(ctx context.Context) error { cfg.ClientAuth = tls.RequireAndVerifyClientCert } + // load passed in TLS ciphers + if s.TLSCiphers != nil { + cipherSuites := tlsCipherSuitesIDs(s.TLSCiphers) + cfg.CipherSuites = cipherSuites + } + ExposedTLSConfig = cfg + listener, err := tls.Listen("tcp", net.JoinHostPort(s.Host, strconv.Itoa(s.Port)), cfg) if err != nil { return err diff --git a/pkg/webhook/server_test.go b/pkg/webhook/server_test.go index 2ed986e405..3ab1d70bd1 100644 --- a/pkg/webhook/server_test.go +++ b/pkg/webhook/server_test.go @@ -18,6 +18,7 @@ package webhook_test import ( "context" + "crypto/tls" "fmt" "io" "net" @@ -186,7 +187,7 @@ var _ = Describe("Webhook Server", func() { }) }) - It("should serve be able to serve in unmanaged mode", func() { + It("should be able to serve in unmanaged mode", func() { server = &webhook.Server{ Host: servingOpts.LocalServingHost, Port: servingOpts.LocalServingPort, @@ -207,6 +208,45 @@ var _ = Describe("Webhook Server", func() { ctxCancel() Eventually(doneCh, "4s").Should(BeClosed()) }) + + It("should respect passed in TLS configurations", func() { + ciphers := []string{ + "TLS_AES_128_GCM_SHA256", + "TLS_AES_256_GCM_SHA384", + "TLS_CHACHA20_POLY1305_SHA256", + "ECDHE-ECDSA-AES128-GCM-SHA256", + "ECDHE-RSA-AES128-GCM-SHA256", + "ECDHE-ECDSA-AES256-GCM-SHA384", + "ECDHE-RSA-AES256-GCM-SHA384", + "ECDHE-ECDSA-CHACHA20-POLY1305", + "ECDHE-RSA-CHACHA20-POLY1305", + "DHE-RSA-AES128-GCM-SHA256", + "DHE-RSA-AES256-GCM-SHA384", + } + server = &webhook.Server{ + Host: servingOpts.LocalServingHost, + Port: servingOpts.LocalServingPort, + CertDir: servingOpts.LocalServingCertDir, + TLSMinVersion: "1.2", + TLSCiphers: ciphers, + } + server.Register("/somepath", &testHandler{}) + doneCh := genericStartServer(func(ctx context.Context) { + Expect(server.StartStandalone(ctx, scheme.Scheme)) + }) + + Eventually(func() ([]byte, error) { + resp, err := client.Get(fmt.Sprintf("https://%s/somepath", testHostPort)) + Expect(err).NotTo(HaveOccurred()) + defer resp.Body.Close() + return io.ReadAll(resp.Body) + }).Should(Equal([]byte("gadzooks!"))) + Expect(webhook.ExposedTLSConfig.MinVersion).To(Equal(uint16(tls.VersionTLS12))) + Expect(webhook.ExposedTLSConfig.CipherSuites).To(ContainElement(tls.TLS_AES_128_GCM_SHA256)) + + ctxCancel() + Eventually(doneCh, "4s").Should(BeClosed()) + }) }) type testHandler struct {