Skip to content

Commit e046b5c

Browse files
authored
OPRUN-4798: feat: configure OLMv0 TLS curves from cluster profile (#3946)
* feat: configure OLMv0 TLS curves from cluster profile Signed-off-by: Todd Short <tshort@redhat.com> * fix: preserve TLS curve restrictions in FIPS mode Signed-off-by: Todd Short <tshort@redhat.com> * test: cover OLMv0 TLS curve preferences Signed-off-by: Todd Short <tshort@redhat.com> --------- Signed-off-by: Todd Short <tshort@redhat.com>
1 parent d8ee1d8 commit e046b5c

7 files changed

Lines changed: 91 additions & 26 deletions

File tree

‎pkg/lib/apiserver/querier.go‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,8 @@ func NoopQuerier() Querier {
1414
// Querier is an interface that wraps the QueryTLSConfig method.
1515
//
1616
// QueryTLSConfig updates the provided TLS configuration with cluster-wide
17-
// TLS security profile settings (MinVersion, CipherSuites, PreferServerCipherSuites).
17+
// TLS security profile settings (MinVersion, CipherSuites, CurvePreferences,
18+
// PreferServerCipherSuites).
1819
type Querier interface {
1920
QueryTLSConfig(config *tls.Config) error
2021
}

‎pkg/lib/apiserver/syncer.go‎

Lines changed: 19 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -131,14 +131,15 @@ func newTLSConfigHolder() *tlsConfigHolder {
131131
}
132132

133133
// update atomically updates the stored TLS configuration.
134-
func (h *tlsConfigHolder) update(minVersion uint16, cipherSuites []uint16) {
134+
func (h *tlsConfigHolder) update(minVersion uint16, cipherSuites []uint16, curvePreferences []tls.CurveID) {
135135
h.mu.Lock()
136136
defer h.mu.Unlock()
137137

138138
h.config.MinVersion = minVersion
139139
// Make a defensive copy of the slice
140140
h.config.CipherSuites = make([]uint16, len(cipherSuites))
141141
copy(h.config.CipherSuites, cipherSuites)
142+
h.config.CurvePreferences = append([]tls.CurveID(nil), curvePreferences...)
142143
h.config.PreferServerCipherSuites = true
143144
}
144145

@@ -152,6 +153,7 @@ func (h *tlsConfigHolder) copyTo(config *tls.Config) {
152153
config.MinVersion = h.config.MinVersion
153154
config.CipherSuites = make([]uint16, len(h.config.CipherSuites))
154155
copy(config.CipherSuites, h.config.CipherSuites)
156+
config.CurvePreferences = append([]tls.CurveID(nil), h.config.CurvePreferences...)
155157
config.PreferServerCipherSuites = h.config.PreferServerCipherSuites
156158
}
157159

@@ -177,21 +179,21 @@ func (w *Syncer) SyncAPIServer(object interface{}) error {
177179
}
178180

179181
// Convert the TLS security profile to get new settings
180-
minVersion, cipherSuites := GetSecurityProfileConfig(apiserver.Spec.TLSSecurityProfile)
182+
minVersion, cipherSuites, curvePreferences := GetSecurityProfileConfig(apiserver.Spec.TLSSecurityProfile)
181183

182184
// Check if configuration changed (before updating)
183-
changed := w.hasConfigChanged(minVersion, cipherSuites)
185+
changed := w.hasConfigChanged(minVersion, cipherSuites, curvePreferences)
184186

185187
// Update the stored configuration atomically
186-
w.currentConfig.update(minVersion, cipherSuites)
188+
w.currentConfig.update(minVersion, cipherSuites, curvePreferences)
187189

188190
// Log if configuration changed
189191
if changed {
190192
profileName := getProfileName(apiserver.Spec.TLSSecurityProfile)
191-
w.logger.Infof("APIServer TLS configuration changed: profile=%s, minVersion=%s, cipherCount=%d",
193+
w.logger.Infof("APIServer TLS configuration changed: profile=%s, minVersion=%s, cipherCount=%d, curvePreferences=%v",
192194
profileName,
193195
tlsVersionToString(minVersion),
194-
len(cipherSuites))
196+
len(cipherSuites), curvePreferences)
195197
}
196198

197199
return nil
@@ -206,14 +208,15 @@ func (w *Syncer) HandleAPIServerDelete(object interface{}) {
206208
}
207209

208210
// Reset to secure defaults (Intermediate profile)
209-
w.currentConfig.update(GetSecurityProfileConfig(nil))
211+
minVersion, cipherSuites, curvePreferences := GetSecurityProfileConfig(nil)
212+
w.currentConfig.update(minVersion, cipherSuites, curvePreferences)
210213

211214
w.logger.Info("APIServer TLS configuration deleted, reverted to secure defaults")
212215
return
213216
}
214217

215218
// hasConfigChanged checks if the new TLS settings differ from the current cached settings.
216-
func (w *Syncer) hasConfigChanged(minVersion uint16, cipherSuites []uint16) bool {
219+
func (w *Syncer) hasConfigChanged(minVersion uint16, cipherSuites []uint16, curvePreferences []tls.CurveID) bool {
217220
w.currentConfig.mu.RLock()
218221
defer w.currentConfig.mu.RUnlock()
219222

@@ -228,6 +231,14 @@ func (w *Syncer) hasConfigChanged(minVersion uint16, cipherSuites []uint16) bool
228231
return true
229232
}
230233
}
234+
if len(w.currentConfig.config.CurvePreferences) != len(curvePreferences) {
235+
return true
236+
}
237+
for i := range curvePreferences {
238+
if w.currentConfig.config.CurvePreferences[i] != curvePreferences[i] {
239+
return true
240+
}
241+
}
231242
return false
232243
}
233244

‎pkg/lib/apiserver/syncer_test.go‎

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,7 @@ func TestSyncer_SyncAPIServer_IntermediateProfile(t *testing.T) {
5858
}
5959

6060
// Test that GetSecurityProfileConfig returns expected values for Intermediate
61-
minVersion, cipherSuites := apiserver.GetSecurityProfileConfig(server.Spec.TLSSecurityProfile)
61+
minVersion, cipherSuites, _ := apiserver.GetSecurityProfileConfig(server.Spec.TLSSecurityProfile)
6262

6363
assert.Equal(t, uint16(tls.VersionTLS12), minVersion, "Intermediate should use TLS 1.2")
6464
assert.NotEmpty(t, cipherSuites, "Should have cipher suites")
@@ -79,7 +79,7 @@ func TestSyncer_SyncAPIServer_ModernProfile(t *testing.T) {
7979
}
8080

8181
// Test that GetSecurityProfileConfig returns expected values for Modern
82-
minVersion, cipherSuites := apiserver.GetSecurityProfileConfig(server.Spec.TLSSecurityProfile)
82+
minVersion, cipherSuites, _ := apiserver.GetSecurityProfileConfig(server.Spec.TLSSecurityProfile)
8383

8484
assert.Equal(t, uint16(tls.VersionTLS13), minVersion, "Modern should use TLS 1.3")
8585
assert.NotEmpty(t, cipherSuites, "Should have cipher suites")
@@ -108,7 +108,7 @@ func TestSyncer_SyncAPIServer_CustomProfile(t *testing.T) {
108108
}
109109

110110
// Test that GetSecurityProfileConfig returns expected values for Custom
111-
minVersion, cipherSuites := apiserver.GetSecurityProfileConfig(server.Spec.TLSSecurityProfile)
111+
minVersion, cipherSuites, _ := apiserver.GetSecurityProfileConfig(server.Spec.TLSSecurityProfile)
112112

113113
assert.Equal(t, uint16(tls.VersionTLS13), minVersion, "Custom should respect MinTLSVersion")
114114
assert.NotEmpty(t, cipherSuites, "Should have cipher suites")

‎pkg/lib/apiserver/tlsconfig.go‎

Lines changed: 42 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,19 @@
11
package apiserver
22

33
import (
4+
"crypto/fips140"
45
"crypto/tls"
56

67
apiconfigv1 "github.com/openshift/api/config/v1"
78
"github.com/openshift/library-go/pkg/crypto"
9+
"k8s.io/klog/v2"
810
)
911

10-
// GetSecurityProfileConfig extracts the minimum TLS version and cipher suites
11-
// from a TLSSecurityProfile object. Converts OpenSSL cipher names to Go TLS cipher IDs.
12+
// GetSecurityProfileConfig extracts the minimum TLS version, cipher suites, and
13+
// curve preferences from a TLSSecurityProfile object. Converts OpenSSL cipher
14+
// names and TLS groups to Go TLS identifiers.
1215
// If profile is nil, returns config defined by the Intermediate TLS Profile.
13-
func GetSecurityProfileConfig(profile *apiconfigv1.TLSSecurityProfile) (uint16, []uint16) {
16+
func GetSecurityProfileConfig(profile *apiconfigv1.TLSSecurityProfile) (uint16, []uint16, []tls.CurveID) {
1417
var profileType apiconfigv1.TLSProfileType
1518
if profile == nil {
1619
profileType = apiconfigv1.TLSProfileIntermediateType
@@ -42,8 +45,40 @@ func GetSecurityProfileConfig(profile *apiconfigv1.TLSSecurityProfile) (uint16,
4245
// Convert OpenSSL cipher names to IANA names, then to Go cipher suite IDs
4346
ianaCipherNames := crypto.OpenSSLToIANACipherSuites(profileSpec.Ciphers)
4447
cipherSuites := CipherNamesToIDs(ianaCipherNames)
48+
curvePreferences, unrecognizedGroups := crypto.TLSGroupsToCurveIDs(profileSpec.Groups)
49+
if len(unrecognizedGroups) > 0 {
50+
klog.Warningf("Dropping TLS groups not supported by this Go version: %v", unrecognizedGroups)
51+
}
52+
if fips140.Enabled() {
53+
curvePreferences = filterFIPSCurvePreferences(curvePreferences)
54+
if len(profileSpec.Groups) > 0 && len(curvePreferences) == 0 {
55+
klog.Warning("No configured TLS groups are FIPS-approved; using the FIPS-safe P-curve fallback")
56+
curvePreferences = fipsSafeCurvePreferences()
57+
}
58+
}
59+
60+
return minTLSVersion, cipherSuites, curvePreferences
61+
}
4562

46-
return minTLSVersion, cipherSuites
63+
// filterFIPSCurvePreferences retains only the NIST P-curves. This deliberately
64+
// excludes X25519MLKEM768 even though Go's native FIPS module permits it: OLM
65+
// must also support the OpenSSL FIPS backend, which rejects its X25519 component.
66+
func filterFIPSCurvePreferences(preferences []tls.CurveID) []tls.CurveID {
67+
approved := make([]tls.CurveID, 0, len(preferences))
68+
for _, preference := range preferences {
69+
switch preference {
70+
case tls.CurveP256, tls.CurveP384, tls.CurveP521:
71+
approved = append(approved, preference)
72+
}
73+
}
74+
return approved
75+
}
76+
77+
// fipsSafeCurvePreferences returns the NIST P-curves used when FIPS filtering
78+
// removes every configured TLS group. Returning a non-empty list preserves the
79+
// configured restriction instead of allowing Go to select its defaults.
80+
func fipsSafeCurvePreferences() []tls.CurveID {
81+
return []tls.CurveID{tls.CurveP256, tls.CurveP384, tls.CurveP521}
4782
}
4883

4984
// CipherNamesToIDs converts IANA cipher suite names to Go TLS cipher suite IDs
@@ -73,6 +108,9 @@ func ApplySecureDefaults(config *tls.Config) error {
73108
if len(config.CipherSuites) == 0 {
74109
config.CipherSuites = crypto.DefaultCiphers()
75110
}
111+
if len(config.CurvePreferences) == 0 {
112+
_, _, config.CurvePreferences = GetSecurityProfileConfig(nil)
113+
}
76114
config.PreferServerCipherSuites = true
77115
return nil
78116
}

‎pkg/lib/apiserver/tlsconfig_test.go‎

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ import (
1212

1313
func TestGetSecurityProfileConfig_NilProfile(t *testing.T) {
1414
// When profile is nil, should use Intermediate defaults
15-
minVersion, cipherSuites := apiserver.GetSecurityProfileConfig(nil)
15+
minVersion, cipherSuites, _ := apiserver.GetSecurityProfileConfig(nil)
1616

1717
assert.Equal(t, uint16(tls.VersionTLS12), minVersion, "Intermediate profile should use TLS 1.2")
1818
assert.NotEmpty(t, cipherSuites, "Should have cipher suites")
@@ -23,7 +23,7 @@ func TestGetSecurityProfileConfig_IntermediateProfile(t *testing.T) {
2323
Type: apiconfigv1.TLSProfileIntermediateType,
2424
}
2525

26-
minVersion, cipherSuites := apiserver.GetSecurityProfileConfig(profile)
26+
minVersion, cipherSuites, _ := apiserver.GetSecurityProfileConfig(profile)
2727

2828
assert.Equal(t, uint16(tls.VersionTLS12), minVersion, "Intermediate profile should use TLS 1.2")
2929
assert.NotEmpty(t, cipherSuites, "Should have cipher suites")
@@ -35,7 +35,7 @@ func TestGetSecurityProfileConfig_ModernProfile(t *testing.T) {
3535
Type: apiconfigv1.TLSProfileModernType,
3636
}
3737

38-
minVersion, cipherSuites := apiserver.GetSecurityProfileConfig(profile)
38+
minVersion, cipherSuites, _ := apiserver.GetSecurityProfileConfig(profile)
3939

4040
assert.Equal(t, uint16(tls.VersionTLS13), minVersion, "Modern profile should use TLS 1.3")
4141
assert.NotEmpty(t, cipherSuites, "Should have cipher suites")
@@ -46,7 +46,7 @@ func TestGetSecurityProfileConfig_OldProfile(t *testing.T) {
4646
Type: apiconfigv1.TLSProfileOldType,
4747
}
4848

49-
minVersion, cipherSuites := apiserver.GetSecurityProfileConfig(profile)
49+
minVersion, cipherSuites, _ := apiserver.GetSecurityProfileConfig(profile)
5050

5151
assert.Equal(t, uint16(tls.VersionTLS10), minVersion, "Old profile should use TLS 1.0")
5252
assert.NotEmpty(t, cipherSuites, "Should have cipher suites")
@@ -67,7 +67,7 @@ func TestGetSecurityProfileConfig_CustomProfile(t *testing.T) {
6767
},
6868
}
6969

70-
minVersion, cipherSuites := apiserver.GetSecurityProfileConfig(profile)
70+
minVersion, cipherSuites, _ := apiserver.GetSecurityProfileConfig(profile)
7171

7272
assert.Equal(t, uint16(tls.VersionTLS13), minVersion, "Custom profile should respect MinTLSVersion")
7373
assert.NotEmpty(t, cipherSuites, "Should have cipher suites")
@@ -80,7 +80,7 @@ func TestGetSecurityProfileConfig_CustomProfileWithoutSpec(t *testing.T) {
8080
Custom: nil,
8181
}
8282

83-
minVersion, cipherSuites := apiserver.GetSecurityProfileConfig(profile)
83+
minVersion, cipherSuites, _ := apiserver.GetSecurityProfileConfig(profile)
8484

8585
assert.Equal(t, uint16(tls.VersionTLS12), minVersion, "Should fall back to Intermediate")
8686
assert.NotEmpty(t, cipherSuites, "Should have cipher suites")
@@ -118,6 +118,7 @@ func TestApplySecureDefaults(t *testing.T) {
118118
if tt.name == "EmptyConfig" {
119119
assert.NotZero(t, tt.config.MinVersion, "MinVersion should be set")
120120
assert.NotEmpty(t, tt.config.CipherSuites, "CipherSuites should be set")
121+
assert.NotEmpty(t, tt.config.CurvePreferences, "CurvePreferences should be set")
121122
}
122123
assert.True(t, tt.config.PreferServerCipherSuites, "PreferServerCipherSuites should be true")
123124
})

‎pkg/package-server/server/server.go‎

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -374,7 +374,7 @@ func applyClusterTLSProfile(ctx context.Context, config *rest.Config, serving *g
374374

375375
// applyClusterTLSProfileWithClients is the testable core of applyClusterTLSProfile.
376376
// It applies the cluster-wide APIServer TLS security profile to the SecureServingOptions,
377-
// but only for fields not already set by explicit flags (--tls-min-version / --tls-cipher-suites).
377+
// but only for fields not already set by explicit TLS flags.
378378
// It is a no-op on non-OpenShift clusters.
379379
func applyClusterTLSProfileWithClients(ctx context.Context, discovery apidiscovery.DiscoveryInterface, cfgClient configv1client.Interface, serving *genericoptions.SecureServingOptionsWithLoopback) error {
380380
available, err := openshiftconfig.IsAPIAvailable(discovery)
@@ -390,14 +390,20 @@ func applyClusterTLSProfileWithClients(ctx context.Context, discovery apidiscove
390390
return fmt.Errorf("failed to get APIServer config: %w", err)
391391
}
392392

393-
minVersion, cipherSuites := olmapiserver.GetSecurityProfileConfig(apiServer.Spec.TLSSecurityProfile)
393+
minVersion, cipherSuites, curvePreferences := olmapiserver.GetSecurityProfileConfig(apiServer.Spec.TLSSecurityProfile)
394394
// Only override fields not already set by explicit flags.
395395
if serving.MinTLSVersion == "" {
396396
serving.MinTLSVersion = libcrypto.TLSVersionToNameOrDie(minVersion)
397397
}
398398
if len(serving.CipherSuites) == 0 {
399399
serving.CipherSuites = libcrypto.CipherSuitesToNamesOrDie(cipherSuites)
400400
}
401-
log.Infof("Applying cluster TLS security profile: minVersion=%s cipherSuites=%v", serving.MinTLSVersion, serving.CipherSuites)
401+
if len(serving.CurvePreferences) == 0 {
402+
serving.CurvePreferences = make([]int32, len(curvePreferences))
403+
for i, curvePreference := range curvePreferences {
404+
serving.CurvePreferences[i] = int32(curvePreference)
405+
}
406+
}
407+
log.Infof("Applying cluster TLS security profile: minVersion=%s cipherSuites=%v curvePreferences=%v", serving.MinTLSVersion, serving.CipherSuites, serving.CurvePreferences)
402408
return nil
403409
}

‎pkg/package-server/server/server_test.go‎

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package server
22

33
import (
44
"context"
5+
"crypto/tls"
56
"testing"
67

78
apiconfigv1 "github.com/openshift/api/config/v1"
@@ -56,7 +57,7 @@ func TestApplyClusterTLSProfileWithClients_NonOpenShift(t *testing.T) {
5657
}
5758

5859
// TestApplyClusterTLSProfileWithClients_IntermediateProfile verifies that the
59-
// Intermediate TLS profile populates MinTLSVersion and CipherSuites.
60+
// Intermediate TLS profile populates MinTLSVersion, CipherSuites, and CurvePreferences.
6061
func TestApplyClusterTLSProfileWithClients_IntermediateProfile(t *testing.T) {
6162
apiServer := clusterAPIServer(&apiconfigv1.TLSSecurityProfile{
6263
Type: apiconfigv1.TLSProfileIntermediateType,
@@ -74,6 +75,9 @@ func TestApplyClusterTLSProfileWithClients_IntermediateProfile(t *testing.T) {
7475
if len(serving.CipherSuites) == 0 {
7576
t.Error("expected CipherSuites to be set for Intermediate profile")
7677
}
78+
if len(serving.CurvePreferences) == 0 {
79+
t.Error("expected CurvePreferences to be set for Intermediate profile")
80+
}
7781
}
7882

7983
// TestApplyClusterTLSProfileWithClients_ModernProfile verifies that the Modern
@@ -105,6 +109,7 @@ func TestApplyClusterTLSProfileWithClients_FlagsTakePrecedence(t *testing.T) {
105109
// Simulate user-supplied flags.
106110
serving.MinTLSVersion = "VersionTLS12"
107111
serving.CipherSuites = []string{"TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256"}
112+
serving.CurvePreferences = []int32{int32(tls.CurveP256)}
108113

109114
err := applyClusterTLSProfileWithClients(context.Background(), cfgClient.Discovery(), cfgClient, serving)
110115
if err != nil {
@@ -116,6 +121,9 @@ func TestApplyClusterTLSProfileWithClients_FlagsTakePrecedence(t *testing.T) {
116121
if len(serving.CipherSuites) != 1 || serving.CipherSuites[0] != "TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256" {
117122
t.Errorf("user-supplied CipherSuites should not be overwritten, got %v", serving.CipherSuites)
118123
}
124+
if len(serving.CurvePreferences) != 1 || serving.CurvePreferences[0] != int32(tls.CurveP256) {
125+
t.Errorf("user-supplied CurvePreferences should not be overwritten, got %v", serving.CurvePreferences)
126+
}
119127
}
120128

121129
// TestApplyClusterTLSProfileWithClients_MissingAPIServerCR verifies that a

0 commit comments

Comments
 (0)