From 3b4e4602d7422d3dcba7531d0d3d5e78c91a472f Mon Sep 17 00:00:00 2001 From: Todd Short Date: Wed, 21 Oct 2026 17:40:42 -0400 Subject: [PATCH 1/3] feat: configure OLMv0 TLS curves from cluster profile Signed-off-by: Todd Short --- pkg/lib/apiserver/querier.go | 3 ++- pkg/lib/apiserver/syncer.go | 27 +++++++++++++++------- pkg/lib/apiserver/syncer_test.go | 6 ++--- pkg/lib/apiserver/tlsconfig.go | 29 ++++++++++++++++++++---- pkg/lib/apiserver/tlsconfig_test.go | 12 +++++----- pkg/package-server/server/server.go | 12 +++++++--- pkg/package-server/server/server_test.go | 2 +- 7 files changed, 65 insertions(+), 26 deletions(-) diff --git a/pkg/lib/apiserver/querier.go b/pkg/lib/apiserver/querier.go index 94963890e1..153c4a303c 100644 --- a/pkg/lib/apiserver/querier.go +++ b/pkg/lib/apiserver/querier.go @@ -14,7 +14,8 @@ func NoopQuerier() Querier { // Querier is an interface that wraps the QueryTLSConfig method. // // QueryTLSConfig updates the provided TLS configuration with cluster-wide -// TLS security profile settings (MinVersion, CipherSuites, PreferServerCipherSuites). +// TLS security profile settings (MinVersion, CipherSuites, CurvePreferences, +// PreferServerCipherSuites). type Querier interface { QueryTLSConfig(config *tls.Config) error } diff --git a/pkg/lib/apiserver/syncer.go b/pkg/lib/apiserver/syncer.go index 0d9e68a5d1..1d28c5295d 100644 --- a/pkg/lib/apiserver/syncer.go +++ b/pkg/lib/apiserver/syncer.go @@ -131,7 +131,7 @@ func newTLSConfigHolder() *tlsConfigHolder { } // update atomically updates the stored TLS configuration. -func (h *tlsConfigHolder) update(minVersion uint16, cipherSuites []uint16) { +func (h *tlsConfigHolder) update(minVersion uint16, cipherSuites []uint16, curvePreferences []tls.CurveID) { h.mu.Lock() defer h.mu.Unlock() @@ -139,6 +139,7 @@ func (h *tlsConfigHolder) update(minVersion uint16, cipherSuites []uint16) { // Make a defensive copy of the slice h.config.CipherSuites = make([]uint16, len(cipherSuites)) copy(h.config.CipherSuites, cipherSuites) + h.config.CurvePreferences = append([]tls.CurveID(nil), curvePreferences...) h.config.PreferServerCipherSuites = true } @@ -152,6 +153,7 @@ func (h *tlsConfigHolder) copyTo(config *tls.Config) { config.MinVersion = h.config.MinVersion config.CipherSuites = make([]uint16, len(h.config.CipherSuites)) copy(config.CipherSuites, h.config.CipherSuites) + config.CurvePreferences = append([]tls.CurveID(nil), h.config.CurvePreferences...) config.PreferServerCipherSuites = h.config.PreferServerCipherSuites } @@ -177,21 +179,21 @@ func (w *Syncer) SyncAPIServer(object interface{}) error { } // Convert the TLS security profile to get new settings - minVersion, cipherSuites := GetSecurityProfileConfig(apiserver.Spec.TLSSecurityProfile) + minVersion, cipherSuites, curvePreferences := GetSecurityProfileConfig(apiserver.Spec.TLSSecurityProfile) // Check if configuration changed (before updating) - changed := w.hasConfigChanged(minVersion, cipherSuites) + changed := w.hasConfigChanged(minVersion, cipherSuites, curvePreferences) // Update the stored configuration atomically - w.currentConfig.update(minVersion, cipherSuites) + w.currentConfig.update(minVersion, cipherSuites, curvePreferences) // Log if configuration changed if changed { profileName := getProfileName(apiserver.Spec.TLSSecurityProfile) - w.logger.Infof("APIServer TLS configuration changed: profile=%s, minVersion=%s, cipherCount=%d", + w.logger.Infof("APIServer TLS configuration changed: profile=%s, minVersion=%s, cipherCount=%d, curvePreferences=%v", profileName, tlsVersionToString(minVersion), - len(cipherSuites)) + len(cipherSuites), curvePreferences) } return nil @@ -206,14 +208,15 @@ func (w *Syncer) HandleAPIServerDelete(object interface{}) { } // Reset to secure defaults (Intermediate profile) - w.currentConfig.update(GetSecurityProfileConfig(nil)) + minVersion, cipherSuites, curvePreferences := GetSecurityProfileConfig(nil) + w.currentConfig.update(minVersion, cipherSuites, curvePreferences) w.logger.Info("APIServer TLS configuration deleted, reverted to secure defaults") return } // hasConfigChanged checks if the new TLS settings differ from the current cached settings. -func (w *Syncer) hasConfigChanged(minVersion uint16, cipherSuites []uint16) bool { +func (w *Syncer) hasConfigChanged(minVersion uint16, cipherSuites []uint16, curvePreferences []tls.CurveID) bool { w.currentConfig.mu.RLock() defer w.currentConfig.mu.RUnlock() @@ -228,6 +231,14 @@ func (w *Syncer) hasConfigChanged(minVersion uint16, cipherSuites []uint16) bool return true } } + if len(w.currentConfig.config.CurvePreferences) != len(curvePreferences) { + return true + } + for i := range curvePreferences { + if w.currentConfig.config.CurvePreferences[i] != curvePreferences[i] { + return true + } + } return false } diff --git a/pkg/lib/apiserver/syncer_test.go b/pkg/lib/apiserver/syncer_test.go index 282eaad3e8..73a4625826 100644 --- a/pkg/lib/apiserver/syncer_test.go +++ b/pkg/lib/apiserver/syncer_test.go @@ -58,7 +58,7 @@ func TestSyncer_SyncAPIServer_IntermediateProfile(t *testing.T) { } // Test that GetSecurityProfileConfig returns expected values for Intermediate - minVersion, cipherSuites := apiserver.GetSecurityProfileConfig(server.Spec.TLSSecurityProfile) + minVersion, cipherSuites, _ := apiserver.GetSecurityProfileConfig(server.Spec.TLSSecurityProfile) assert.Equal(t, uint16(tls.VersionTLS12), minVersion, "Intermediate should use TLS 1.2") assert.NotEmpty(t, cipherSuites, "Should have cipher suites") @@ -79,7 +79,7 @@ func TestSyncer_SyncAPIServer_ModernProfile(t *testing.T) { } // Test that GetSecurityProfileConfig returns expected values for Modern - minVersion, cipherSuites := apiserver.GetSecurityProfileConfig(server.Spec.TLSSecurityProfile) + minVersion, cipherSuites, _ := apiserver.GetSecurityProfileConfig(server.Spec.TLSSecurityProfile) assert.Equal(t, uint16(tls.VersionTLS13), minVersion, "Modern should use TLS 1.3") assert.NotEmpty(t, cipherSuites, "Should have cipher suites") @@ -108,7 +108,7 @@ func TestSyncer_SyncAPIServer_CustomProfile(t *testing.T) { } // Test that GetSecurityProfileConfig returns expected values for Custom - minVersion, cipherSuites := apiserver.GetSecurityProfileConfig(server.Spec.TLSSecurityProfile) + minVersion, cipherSuites, _ := apiserver.GetSecurityProfileConfig(server.Spec.TLSSecurityProfile) assert.Equal(t, uint16(tls.VersionTLS13), minVersion, "Custom should respect MinTLSVersion") assert.NotEmpty(t, cipherSuites, "Should have cipher suites") diff --git a/pkg/lib/apiserver/tlsconfig.go b/pkg/lib/apiserver/tlsconfig.go index 29a240b179..6fead1242d 100644 --- a/pkg/lib/apiserver/tlsconfig.go +++ b/pkg/lib/apiserver/tlsconfig.go @@ -1,16 +1,19 @@ package apiserver import ( + "crypto/fips140" "crypto/tls" apiconfigv1 "github.com/openshift/api/config/v1" "github.com/openshift/library-go/pkg/crypto" + "k8s.io/klog/v2" ) -// GetSecurityProfileConfig extracts the minimum TLS version and cipher suites -// from a TLSSecurityProfile object. Converts OpenSSL cipher names to Go TLS cipher IDs. +// GetSecurityProfileConfig extracts the minimum TLS version, cipher suites, and +// curve preferences from a TLSSecurityProfile object. Converts OpenSSL cipher +// names and TLS groups to Go TLS identifiers. // If profile is nil, returns config defined by the Intermediate TLS Profile. -func GetSecurityProfileConfig(profile *apiconfigv1.TLSSecurityProfile) (uint16, []uint16) { +func GetSecurityProfileConfig(profile *apiconfigv1.TLSSecurityProfile) (uint16, []uint16, []tls.CurveID) { var profileType apiconfigv1.TLSProfileType if profile == nil { profileType = apiconfigv1.TLSProfileIntermediateType @@ -42,8 +45,26 @@ func GetSecurityProfileConfig(profile *apiconfigv1.TLSSecurityProfile) (uint16, // Convert OpenSSL cipher names to IANA names, then to Go cipher suite IDs ianaCipherNames := crypto.OpenSSLToIANACipherSuites(profileSpec.Ciphers) cipherSuites := CipherNamesToIDs(ianaCipherNames) + curvePreferences, unrecognizedGroups := crypto.TLSGroupsToCurveIDs(profileSpec.Groups) + if len(unrecognizedGroups) > 0 { + klog.Warningf("Dropping TLS groups not supported by this Go version: %v", unrecognizedGroups) + } + if fips140.Enabled() { + curvePreferences = filterFIPSCurvePreferences(curvePreferences) + } + + return minTLSVersion, cipherSuites, curvePreferences +} - return minTLSVersion, cipherSuites +func filterFIPSCurvePreferences(preferences []tls.CurveID) []tls.CurveID { + approved := make([]tls.CurveID, 0, len(preferences)) + for _, preference := range preferences { + switch preference { + case tls.CurveP256, tls.CurveP384, tls.CurveP521: + approved = append(approved, preference) + } + } + return approved } // CipherNamesToIDs converts IANA cipher suite names to Go TLS cipher suite IDs diff --git a/pkg/lib/apiserver/tlsconfig_test.go b/pkg/lib/apiserver/tlsconfig_test.go index 1af7cbbf97..50013c462b 100644 --- a/pkg/lib/apiserver/tlsconfig_test.go +++ b/pkg/lib/apiserver/tlsconfig_test.go @@ -12,7 +12,7 @@ import ( func TestGetSecurityProfileConfig_NilProfile(t *testing.T) { // When profile is nil, should use Intermediate defaults - minVersion, cipherSuites := apiserver.GetSecurityProfileConfig(nil) + minVersion, cipherSuites, _ := apiserver.GetSecurityProfileConfig(nil) assert.Equal(t, uint16(tls.VersionTLS12), minVersion, "Intermediate profile should use TLS 1.2") assert.NotEmpty(t, cipherSuites, "Should have cipher suites") @@ -23,7 +23,7 @@ func TestGetSecurityProfileConfig_IntermediateProfile(t *testing.T) { Type: apiconfigv1.TLSProfileIntermediateType, } - minVersion, cipherSuites := apiserver.GetSecurityProfileConfig(profile) + minVersion, cipherSuites, _ := apiserver.GetSecurityProfileConfig(profile) assert.Equal(t, uint16(tls.VersionTLS12), minVersion, "Intermediate profile should use TLS 1.2") assert.NotEmpty(t, cipherSuites, "Should have cipher suites") @@ -35,7 +35,7 @@ func TestGetSecurityProfileConfig_ModernProfile(t *testing.T) { Type: apiconfigv1.TLSProfileModernType, } - minVersion, cipherSuites := apiserver.GetSecurityProfileConfig(profile) + minVersion, cipherSuites, _ := apiserver.GetSecurityProfileConfig(profile) assert.Equal(t, uint16(tls.VersionTLS13), minVersion, "Modern profile should use TLS 1.3") assert.NotEmpty(t, cipherSuites, "Should have cipher suites") @@ -46,7 +46,7 @@ func TestGetSecurityProfileConfig_OldProfile(t *testing.T) { Type: apiconfigv1.TLSProfileOldType, } - minVersion, cipherSuites := apiserver.GetSecurityProfileConfig(profile) + minVersion, cipherSuites, _ := apiserver.GetSecurityProfileConfig(profile) assert.Equal(t, uint16(tls.VersionTLS10), minVersion, "Old profile should use TLS 1.0") assert.NotEmpty(t, cipherSuites, "Should have cipher suites") @@ -67,7 +67,7 @@ func TestGetSecurityProfileConfig_CustomProfile(t *testing.T) { }, } - minVersion, cipherSuites := apiserver.GetSecurityProfileConfig(profile) + minVersion, cipherSuites, _ := apiserver.GetSecurityProfileConfig(profile) assert.Equal(t, uint16(tls.VersionTLS13), minVersion, "Custom profile should respect MinTLSVersion") assert.NotEmpty(t, cipherSuites, "Should have cipher suites") @@ -80,7 +80,7 @@ func TestGetSecurityProfileConfig_CustomProfileWithoutSpec(t *testing.T) { Custom: nil, } - minVersion, cipherSuites := apiserver.GetSecurityProfileConfig(profile) + minVersion, cipherSuites, _ := apiserver.GetSecurityProfileConfig(profile) assert.Equal(t, uint16(tls.VersionTLS12), minVersion, "Should fall back to Intermediate") assert.NotEmpty(t, cipherSuites, "Should have cipher suites") diff --git a/pkg/package-server/server/server.go b/pkg/package-server/server/server.go index e2ef2a86f8..8eb6a0efc2 100644 --- a/pkg/package-server/server/server.go +++ b/pkg/package-server/server/server.go @@ -374,7 +374,7 @@ func applyClusterTLSProfile(ctx context.Context, config *rest.Config, serving *g // applyClusterTLSProfileWithClients is the testable core of applyClusterTLSProfile. // It applies the cluster-wide APIServer TLS security profile to the SecureServingOptions, -// but only for fields not already set by explicit flags (--tls-min-version / --tls-cipher-suites). +// but only for fields not already set by explicit TLS flags. // It is a no-op on non-OpenShift clusters. func applyClusterTLSProfileWithClients(ctx context.Context, discovery apidiscovery.DiscoveryInterface, cfgClient configv1client.Interface, serving *genericoptions.SecureServingOptionsWithLoopback) error { available, err := openshiftconfig.IsAPIAvailable(discovery) @@ -390,7 +390,7 @@ func applyClusterTLSProfileWithClients(ctx context.Context, discovery apidiscove return fmt.Errorf("failed to get APIServer config: %w", err) } - minVersion, cipherSuites := olmapiserver.GetSecurityProfileConfig(apiServer.Spec.TLSSecurityProfile) + minVersion, cipherSuites, curvePreferences := olmapiserver.GetSecurityProfileConfig(apiServer.Spec.TLSSecurityProfile) // Only override fields not already set by explicit flags. if serving.MinTLSVersion == "" { serving.MinTLSVersion = libcrypto.TLSVersionToNameOrDie(minVersion) @@ -398,6 +398,12 @@ func applyClusterTLSProfileWithClients(ctx context.Context, discovery apidiscove if len(serving.CipherSuites) == 0 { serving.CipherSuites = libcrypto.CipherSuitesToNamesOrDie(cipherSuites) } - log.Infof("Applying cluster TLS security profile: minVersion=%s cipherSuites=%v", serving.MinTLSVersion, serving.CipherSuites) + if len(serving.CurvePreferences) == 0 { + serving.CurvePreferences = make([]int32, len(curvePreferences)) + for i, curvePreference := range curvePreferences { + serving.CurvePreferences[i] = int32(curvePreference) + } + } + log.Infof("Applying cluster TLS security profile: minVersion=%s cipherSuites=%v curvePreferences=%v", serving.MinTLSVersion, serving.CipherSuites, serving.CurvePreferences) return nil } diff --git a/pkg/package-server/server/server_test.go b/pkg/package-server/server/server_test.go index 529c1257e5..112acfe02f 100644 --- a/pkg/package-server/server/server_test.go +++ b/pkg/package-server/server/server_test.go @@ -56,7 +56,7 @@ func TestApplyClusterTLSProfileWithClients_NonOpenShift(t *testing.T) { } // TestApplyClusterTLSProfileWithClients_IntermediateProfile verifies that the -// Intermediate TLS profile populates MinTLSVersion and CipherSuites. +// Intermediate TLS profile populates MinTLSVersion, CipherSuites, and CurvePreferences. func TestApplyClusterTLSProfileWithClients_IntermediateProfile(t *testing.T) { apiServer := clusterAPIServer(&apiconfigv1.TLSSecurityProfile{ Type: apiconfigv1.TLSProfileIntermediateType, From deff04a196b95995a2e007515b4758085b191d80 Mon Sep 17 00:00:00 2001 From: Todd Short Date: Thu, 22 Oct 2026 09:29:55 -0400 Subject: [PATCH 2/3] fix: preserve TLS curve restrictions in FIPS mode Signed-off-by: Todd Short --- pkg/lib/apiserver/tlsconfig.go | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/pkg/lib/apiserver/tlsconfig.go b/pkg/lib/apiserver/tlsconfig.go index 6fead1242d..ce3434b722 100644 --- a/pkg/lib/apiserver/tlsconfig.go +++ b/pkg/lib/apiserver/tlsconfig.go @@ -51,6 +51,10 @@ func GetSecurityProfileConfig(profile *apiconfigv1.TLSSecurityProfile) (uint16, } if fips140.Enabled() { curvePreferences = filterFIPSCurvePreferences(curvePreferences) + if len(profileSpec.Groups) > 0 && len(curvePreferences) == 0 { + klog.Warning("No configured TLS groups are FIPS-approved; using the FIPS-safe P-curve fallback") + curvePreferences = fipsSafeCurvePreferences() + } } return minTLSVersion, cipherSuites, curvePreferences @@ -67,6 +71,13 @@ func filterFIPSCurvePreferences(preferences []tls.CurveID) []tls.CurveID { return approved } +// fipsSafeCurvePreferences returns the NIST P-curves used when FIPS filtering +// removes every configured TLS group. Returning a non-empty list preserves the +// configured restriction instead of allowing Go to select its defaults. +func fipsSafeCurvePreferences() []tls.CurveID { + return []tls.CurveID{tls.CurveP256, tls.CurveP384, tls.CurveP521} +} + // CipherNamesToIDs converts IANA cipher suite names to Go TLS cipher suite IDs func CipherNamesToIDs(cipherNames []string) []uint16 { var cipherIDs []uint16 From c9dce394a75637fa350512ca07d719da5069ef66 Mon Sep 17 00:00:00 2001 From: Todd Short Date: Thu, 22 Oct 2026 11:11:54 -0400 Subject: [PATCH 3/3] test: cover OLMv0 TLS curve preferences Signed-off-by: Todd Short --- pkg/lib/apiserver/tlsconfig.go | 6 ++++++ pkg/lib/apiserver/tlsconfig_test.go | 1 + pkg/package-server/server/server_test.go | 8 ++++++++ 3 files changed, 15 insertions(+) diff --git a/pkg/lib/apiserver/tlsconfig.go b/pkg/lib/apiserver/tlsconfig.go index ce3434b722..3955ebb432 100644 --- a/pkg/lib/apiserver/tlsconfig.go +++ b/pkg/lib/apiserver/tlsconfig.go @@ -60,6 +60,9 @@ func GetSecurityProfileConfig(profile *apiconfigv1.TLSSecurityProfile) (uint16, return minTLSVersion, cipherSuites, curvePreferences } +// filterFIPSCurvePreferences retains only the NIST P-curves. This deliberately +// excludes X25519MLKEM768 even though Go's native FIPS module permits it: OLM +// must also support the OpenSSL FIPS backend, which rejects its X25519 component. func filterFIPSCurvePreferences(preferences []tls.CurveID) []tls.CurveID { approved := make([]tls.CurveID, 0, len(preferences)) for _, preference := range preferences { @@ -105,6 +108,9 @@ func ApplySecureDefaults(config *tls.Config) error { if len(config.CipherSuites) == 0 { config.CipherSuites = crypto.DefaultCiphers() } + if len(config.CurvePreferences) == 0 { + _, _, config.CurvePreferences = GetSecurityProfileConfig(nil) + } config.PreferServerCipherSuites = true return nil } diff --git a/pkg/lib/apiserver/tlsconfig_test.go b/pkg/lib/apiserver/tlsconfig_test.go index 50013c462b..a6a5b45046 100644 --- a/pkg/lib/apiserver/tlsconfig_test.go +++ b/pkg/lib/apiserver/tlsconfig_test.go @@ -118,6 +118,7 @@ func TestApplySecureDefaults(t *testing.T) { if tt.name == "EmptyConfig" { assert.NotZero(t, tt.config.MinVersion, "MinVersion should be set") assert.NotEmpty(t, tt.config.CipherSuites, "CipherSuites should be set") + assert.NotEmpty(t, tt.config.CurvePreferences, "CurvePreferences should be set") } assert.True(t, tt.config.PreferServerCipherSuites, "PreferServerCipherSuites should be true") }) diff --git a/pkg/package-server/server/server_test.go b/pkg/package-server/server/server_test.go index 112acfe02f..18a836adbc 100644 --- a/pkg/package-server/server/server_test.go +++ b/pkg/package-server/server/server_test.go @@ -2,6 +2,7 @@ package server import ( "context" + "crypto/tls" "testing" apiconfigv1 "github.com/openshift/api/config/v1" @@ -74,6 +75,9 @@ func TestApplyClusterTLSProfileWithClients_IntermediateProfile(t *testing.T) { if len(serving.CipherSuites) == 0 { t.Error("expected CipherSuites to be set for Intermediate profile") } + if len(serving.CurvePreferences) == 0 { + t.Error("expected CurvePreferences to be set for Intermediate profile") + } } // TestApplyClusterTLSProfileWithClients_ModernProfile verifies that the Modern @@ -105,6 +109,7 @@ func TestApplyClusterTLSProfileWithClients_FlagsTakePrecedence(t *testing.T) { // Simulate user-supplied flags. serving.MinTLSVersion = "VersionTLS12" serving.CipherSuites = []string{"TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256"} + serving.CurvePreferences = []int32{int32(tls.CurveP256)} err := applyClusterTLSProfileWithClients(context.Background(), cfgClient.Discovery(), cfgClient, serving) if err != nil { @@ -116,6 +121,9 @@ func TestApplyClusterTLSProfileWithClients_FlagsTakePrecedence(t *testing.T) { if len(serving.CipherSuites) != 1 || serving.CipherSuites[0] != "TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256" { t.Errorf("user-supplied CipherSuites should not be overwritten, got %v", serving.CipherSuites) } + if len(serving.CurvePreferences) != 1 || serving.CurvePreferences[0] != int32(tls.CurveP256) { + t.Errorf("user-supplied CurvePreferences should not be overwritten, got %v", serving.CurvePreferences) + } } // TestApplyClusterTLSProfileWithClients_MissingAPIServerCR verifies that a