Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 19 additions & 3 deletions pkg/package-server-manager/controller.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,9 @@ package controllers

import (
"context"
"crypto/tls"
"fmt"
"strconv"
"strings"
"sync"

Expand All @@ -27,8 +29,8 @@ import (
configv1 "github.com/openshift/api/config/v1"
libcrypto "github.com/openshift/library-go/pkg/crypto"
"github.com/openshift/operator-framework-olm/pkg/manifests"
olmapiserver "github.com/operator-framework/operator-lifecycle-manager/pkg/lib/apiserver"
olmv1alpha1 "github.com/operator-framework/api/pkg/operators/v1alpha1"
olmapiserver "github.com/operator-framework/operator-lifecycle-manager/pkg/lib/apiserver"

corev1 "k8s.io/api/core/v1"
rbacv1 "k8s.io/api/rbac/v1"
Expand Down Expand Up @@ -95,14 +97,18 @@ func (r *PackageServerCSVReconciler) Reconcile(ctx context.Context, req ctrl.Req
return ctrl.Result{}, err
}
} else {
minVersion, cipherSuites := olmapiserver.GetSecurityProfileConfig(apiServer.Spec.TLSSecurityProfile)
minVersion, cipherSuites, curvePreferences := olmapiserver.GetSecurityProfileConfig(apiServer.Spec.TLSSecurityProfile)
minVersionStr := libcrypto.TLSVersionToNameOrDie(minVersion)
cipherSuitesStr := strings.Join(libcrypto.CipherSuitesToNamesOrDie(cipherSuites), ",")
curvePreferencesStr := curvePreferencesToString(curvePreferences)
flags = append(flags,
"--tls-min-version", minVersionStr,
"--tls-cipher-suites", cipherSuitesStr,
)
log.Info("applying cluster TLS security profile to packageserver", "minVersion", minVersionStr, "cipherSuites", cipherSuitesStr)
if curvePreferencesStr != "" {
flags = append(flags, "--tls-curve-preferences", curvePreferencesStr)
}
log.Info("applying cluster TLS security profile to packageserver", "minVersion", minVersionStr, "cipherSuites", cipherSuitesStr, "curvePreferences", curvePreferencesStr)
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Resolved
}

required, err := manifests.NewPackageServerCSV(
Expand All @@ -128,6 +134,16 @@ func (r *PackageServerCSVReconciler) Reconcile(ctx context.Context, req ctrl.Req
return ctrl.Result{}, nil
}

// curvePreferencesToString converts Go TLS curve IDs to the comma-separated
// numeric values accepted by the Kubernetes tls-curve-preferences flag.
func curvePreferencesToString(curvePreferences []tls.CurveID) string {
values := make([]string, len(curvePreferences))
for i, curvePreference := range curvePreferences {
values[i] = strconv.FormatInt(int64(curvePreference), 10)
}
return strings.Join(values, ",")
}

func ensureRBAC(client client.Client, ctx context.Context, namespace string, log logr.Logger) error {
log.Info("checking to see if required RBAC exists")
if err := client.Get(ctx, types.NamespacedName{Name: "olm-operator-serviceaccount", Namespace: namespace}, &corev1.ServiceAccount{}); err != nil {
Expand Down
58 changes: 58 additions & 0 deletions pkg/package-server-manager/controller_test.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package controllers

import (
"context"
"testing"

semver "github.com/blang/semver/v4"
Expand All @@ -11,12 +12,69 @@ import (
"github.com/stretchr/testify/require"
appsv1 "k8s.io/api/apps/v1"
corev1 "k8s.io/api/core/v1"
rbacv1 "k8s.io/api/rbac/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/types"
"k8s.io/apimachinery/pkg/util/intstr"
"k8s.io/utils/pointer"
ctrl "sigs.k8s.io/controller-runtime"
"sigs.k8s.io/controller-runtime/pkg/client/fake"
"sigs.k8s.io/controller-runtime/pkg/log/zap"
)

func TestReconcileAppliesTLSCurvePreferences(t *testing.T) {
command := reconcilePackageServerCommandForTLSGroups(t, []configv1.TLSGroup{configv1.TLSGroupSecP384r1, configv1.TLSGroupSecP256r1})
for i, arg := range command {
if arg == "--tls-curve-preferences" {
require.Less(t, i+1, len(command))
require.Equal(t, "24,23", command[i+1])
return
}
}
t.Fatal("package-server command is missing --tls-curve-preferences")
}

func TestReconcileOmitsEmptyTLSCurvePreferences(t *testing.T) {
command := reconcilePackageServerCommandForTLSGroups(t, nil)
require.NotContains(t, command, "--tls-curve-preferences")
}

func reconcilePackageServerCommandForTLSGroups(t *testing.T, groups []configv1.TLSGroup) []string {
t.Helper()
scheme := runtime.NewScheme()
require.NoError(t, configv1.AddToScheme(scheme))
require.NoError(t, olmv1alpha1.AddToScheme(scheme))
require.NoError(t, corev1.AddToScheme(scheme))
require.NoError(t, rbacv1.AddToScheme(scheme))

client := fake.NewClientBuilder().WithScheme(scheme).WithObjects(
&corev1.ServiceAccount{ObjectMeta: metav1.ObjectMeta{Name: "olm-operator-serviceaccount", Namespace: namespace}},
&rbacv1.ClusterRole{ObjectMeta: metav1.ObjectMeta{Name: "system:controller:operator-lifecycle-manager"}},
&rbacv1.ClusterRoleBinding{ObjectMeta: metav1.ObjectMeta{Name: "olm-operator-binding-openshift-operator-lifecycle-manager"}},
&configv1.Infrastructure{ObjectMeta: metav1.ObjectMeta{Name: infrastructureName}},
&configv1.APIServer{
ObjectMeta: metav1.ObjectMeta{Name: "cluster"},
Spec: configv1.APIServerSpec{TLSSecurityProfile: &configv1.TLSSecurityProfile{
Type: configv1.TLSProfileCustomType,
Custom: &configv1.CustomTLSProfile{TLSProfileSpec: configv1.TLSProfileSpec{
Ciphers: []string{"ECDHE-RSA-AES128-GCM-SHA256"},
MinTLSVersion: configv1.VersionTLS12,
Groups: groups,
}},
}},
},
).Build()
reconciler := &PackageServerCSVReconciler{Client: client, Log: logger, Name: name, Namespace: namespace, Image: image}

_, err := reconciler.Reconcile(context.Background(), ctrl.Request{NamespacedName: types.NamespacedName{Name: name, Namespace: namespace}})
require.NoError(t, err)

csv := &olmv1alpha1.ClusterServiceVersion{}
require.NoError(t, client.Get(context.Background(), types.NamespacedName{Name: name, Namespace: namespace}, csv))
return csv.Spec.InstallStrategy.StrategySpec.DeploymentSpecs[0].Spec.Template.Spec.Containers[0].Command
}

var (
logger = zap.New()
name = "packageserver"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Expand Down
27 changes: 19 additions & 8 deletions staging/operator-lifecycle-manager/pkg/lib/apiserver/syncer.go
Original file line number Diff line number Diff line change
Expand Up @@ -131,14 +131,15 @@ 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()

h.config.MinVersion = minVersion
// 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
}

Expand All @@ -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
}

Expand All @@ -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
Expand All @@ -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()

Expand All @@ -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
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand All @@ -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")
Expand Down Expand Up @@ -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")
Expand Down
46 changes: 42 additions & 4 deletions staging/operator-lifecycle-manager/pkg/lib/apiserver/tlsconfig.go
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -42,8 +45,40 @@ 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)
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
}

return minTLSVersion, cipherSuites
// 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 {
switch preference {
case tls.CurveP256, tls.CurveP384, tls.CurveP521:
approved = append(approved, preference)
}
}
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
Expand Down Expand Up @@ -73,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
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand All @@ -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")
Expand All @@ -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")
Expand All @@ -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")
Expand All @@ -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")
Expand All @@ -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")
Expand Down Expand Up @@ -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")
})
Expand Down
Loading