diff --git a/pkg/tls/tls.go b/pkg/tls/tls.go index ce1e8c7d..53c159c7 100644 --- a/pkg/tls/tls.go +++ b/pkg/tls/tls.go @@ -41,6 +41,16 @@ var ( DefaultTLSCiphers = configv1.TLSProfiles[configv1.TLSProfileIntermediateType].Ciphers //nolint:gochecknoglobals // DefaultMinTLSVersion is the default minimum TLS version for API servers. DefaultMinTLSVersion = configv1.TLSProfiles[configv1.TLSProfileIntermediateType].MinTLSVersion //nolint:gochecknoglobals + + // HTTP2NextProtos are the ALPN protocols advertised when HTTP/2 is enabled, + // with HTTP/1.1 fallback. + HTTP2NextProtos = []string{"h2", "http/1.1"} //nolint:gochecknoglobals + + // HTTP1NextProtos are the ALPN protocols advertised when HTTP/2 is disabled, + // restricting negotiation to HTTP/1.1 only. This provides defense-in-depth + // against HTTP/2 Rapid Reset (CVE-2023-44487, CVE-2023-39325) alongside + // the primary fixes in Go 1.21.3+ and golang.org/x/net v0.17.0+. + HTTP1NextProtos = []string{"http/1.1"} //nolint:gochecknoglobals ) // FetchAPIServerTLSProfile fetches the TLS profile spec configured in APIServer. @@ -131,6 +141,34 @@ func NewTLSConfigFromProfile(profile configv1.TLSProfileSpec) (tlsConfig func(*t }, unsupportedCiphers } +// SetNextProtos returns a TLS configuration function that sets the ALPN +// protocol negotiation list on a tls.Config. Empty strings are silently +// ignored, which allows conditional protocol inclusion. +// The returned function is intended to be used with controller-runtime's TLSOpts. +// +// Example: +// +// // Disable HTTP/2: +// openshifttls.SetNextProtos("http/1.1") +// +// // Enable HTTP/2 with fallback: +// openshifttls.SetNextProtos("h2", "http/1.1") +// +// // Using the well-known protocol lists: +// openshifttls.SetNextProtos(openshifttls.HTTP1NextProtos...) +// openshifttls.SetNextProtos(openshifttls.HTTP2NextProtos...) +func SetNextProtos(protos ...string) func(*tls.Config) { + var p []string + for _, proto := range protos { + if proto != "" { + p = append(p, proto) + } + } + return func(c *tls.Config) { + c.NextProtos = p + } +} + // cipherCode returns the TLS cipher code for an OpenSSL or IANA cipher name. // Returns 0 if the cipher is not supported. func cipherCode(cipher string) uint16 { diff --git a/pkg/tls/tls_test.go b/pkg/tls/tls_test.go index 973350e9..5e63375b 100644 --- a/pkg/tls/tls_test.go +++ b/pkg/tls/tls_test.go @@ -190,6 +190,94 @@ var _ = Describe("cipherCodes", func() { }) }) +var _ = Describe("SetNextProtos", func() { + It("should set NextProtos on tls.Config", func() { + fn := SetNextProtos("h2", "http/1.1") + c := &tls.Config{} + fn(c) + Expect(c.NextProtos).To(Equal([]string{"h2", "http/1.1"})) + }) + + It("should handle a single protocol", func() { + fn := SetNextProtos("http/1.1") + c := &tls.Config{} + fn(c) + Expect(c.NextProtos).To(Equal([]string{"http/1.1"})) + }) + + It("should handle no protocols by preserving nil semantics", func() { + fn := SetNextProtos() + c := &tls.Config{} + fn(c) + Expect(c.NextProtos).To(BeNil()) + }) + + It("should ignore empty strings", func() { + fn := SetNextProtos("", "http/1.1") + c := &tls.Config{} + fn(c) + Expect(c.NextProtos).To(Equal([]string{"http/1.1"})) + }) + + It("should return nil when all protocols are empty strings", func() { + fn := SetNextProtos("", "") + c := &tls.Config{} + fn(c) + Expect(c.NextProtos).To(BeNil()) + }) + + It("should not be affected by modification of the original slice", func() { + protos := []string{"h2", "http/1.1"} + fn := SetNextProtos(protos...) + protos[0] = "modified" + c := &tls.Config{} + fn(c) + Expect(c.NextProtos).To(Equal([]string{"h2", "http/1.1"})) + }) + + It("should override any existing NextProtos", func() { + fn := SetNextProtos("h2", "http/1.1") + c := &tls.Config{NextProtos: []string{"old-proto"}} + fn(c) + Expect(c.NextProtos).To(Equal([]string{"h2", "http/1.1"})) + }) + + It("should compose with NewTLSConfigFromProfile", func() { + profile := *configv1.TLSProfiles[configv1.TLSProfileIntermediateType] + tlsConfigFn, _ := NewTLSConfigFromProfile(profile) + + c := &tls.Config{} + tlsConfigFn(c) + SetNextProtos("http/1.1")(c) + + Expect(c.MinVersion).To(Equal(uint16(tls.VersionTLS12))) + Expect(c.CipherSuites).NotTo(BeEmpty()) + Expect(c.NextProtos).To(Equal([]string{"http/1.1"})) + }) + + It("should work with the well-known protocol lists", func() { + fn := SetNextProtos(HTTP2NextProtos...) + c := &tls.Config{} + fn(c) + Expect(c.NextProtos).To(Equal([]string{"h2", "http/1.1"})) + + fn = SetNextProtos(HTTP1NextProtos...) + c = &tls.Config{} + fn(c) + Expect(c.NextProtos).To(Equal([]string{"http/1.1"})) + }) +}) + +var _ = Describe("ALPN protocol variables", func() { + It("HTTP2NextProtos should contain h2 and http/1.1", func() { + Expect(HTTP2NextProtos).To(Equal([]string{"h2", "http/1.1"})) + }) + + It("HTTP1NextProtos should contain only http/1.1", func() { + Expect(HTTP1NextProtos).To(Equal([]string{"http/1.1"})) + }) +}) + var _ = Describe("NewTLSConfigFromProfile", func() { Context("when MinTLSVersion is TLS 1.2", func() { It("should set MinVersion and CipherSuites", func() {