From 13685ff03cc010ab7982b9188579b737bc78c76e Mon Sep 17 00:00:00 2001 From: Jeremiah Stuever Date: Thu, 5 Mar 2026 14:20:23 -0800 Subject: [PATCH 1/2] feat: add TLS adherence policy change tracking Add InitialTLSAdherencePolicy and OnAdherencePolicyChange callback to the SecurityProfileWatcher to detect and handle changes to the APIServer's TLS adherence policy. This enables the operator to react appropriately when the TLS adherence policy is modified. Tests have been updated to cover the new policy tracking behavior. Assisted-by: gemini-3.1-pro-preview --- pkg/tls/controller.go | 17 ++++++++++ pkg/tls/controller_test.go | 64 ++++++++++++++++++++++++++++---------- 2 files changed, 65 insertions(+), 16 deletions(-) diff --git a/pkg/tls/controller.go b/pkg/tls/controller.go index b7efbd9..788634f 100644 --- a/pkg/tls/controller.go +++ b/pkg/tls/controller.go @@ -41,6 +41,9 @@ type SecurityProfileWatcher struct { // InitialTLSProfileSpec is the TLS profile spec that was configured when the operator started. InitialTLSProfileSpec configv1.TLSProfileSpec + // InitialTLSAdherencePolicy is the TLS adherence policy that was configured when the operator started. + InitialTLSAdherencePolicy configv1.TLSAdherencePolicy + // OnProfileChange is a function that will be called when the TLS profile changes. // It receives the reconcile context, old and new TLS profile specs. // This allows the caller to make decisions based on the actual profile changes. @@ -66,6 +69,9 @@ type SecurityProfileWatcher struct { // }, // } OnProfileChange func(ctx context.Context, oldTLSProfileSpec, newTLSProfileSpec configv1.TLSProfileSpec) + + // OnAdherencePolicyChange is a function that will be called when the TLS adherence policy changes. + OnAdherencePolicyChange func(ctx context.Context, oldTLSAdherencePolicy, newTLSAdherencePolicy configv1.TLSAdherencePolicy) } // SetupWithManager sets up the controller with the Manager. @@ -139,6 +145,17 @@ func (r *SecurityProfileWatcher) Reconcile(ctx context.Context, req ctrl.Request r.InitialTLSProfileSpec = currentTLSProfileSpec } + // Compare the current TLS adherence policy with the initial one. + if tlsAdherencePolicyChanged := r.InitialTLSAdherencePolicy != apiServer.Spec.TLSAdherence; tlsAdherencePolicyChanged { + // TLS adherence policy has changed, invoke the callback if it is set. + if r.OnAdherencePolicyChange != nil { + r.OnAdherencePolicyChange(ctx, r.InitialTLSAdherencePolicy, apiServer.Spec.TLSAdherence) + } + + // Persist the new adherence policy for future change detection. + r.InitialTLSAdherencePolicy = apiServer.Spec.TLSAdherence + } + // No need to requeue, as the callback will handle further actions. return ctrl.Result{}, nil } diff --git a/pkg/tls/controller_test.go b/pkg/tls/controller_test.go index 04005ff..a2897a1 100644 --- a/pkg/tls/controller_test.go +++ b/pkg/tls/controller_test.go @@ -58,12 +58,18 @@ var _ = Describe("SecurityProfileWatcher controller", func() { new configv1.TLSProfileSpec } + type adherencePolicyChange struct { + old configv1.TLSAdherencePolicy + new configv1.TLSAdherencePolicy + } + var ( - mgrCancel context.CancelFunc - mgrDone chan struct{} - mgr manager.Manager - apiServer *configv1.APIServer - profileChanges *atomicSlice[profileChange] + mgrCancel context.CancelFunc + mgrDone chan struct{} + mgr manager.Manager + apiServer *configv1.APIServer + profileChanges *atomicSlice[profileChange] + adherencePolicyChanges *atomicSlice[adherencePolicyChange] ) BeforeEach(func() { @@ -88,6 +94,7 @@ var _ = Describe("SecurityProfileWatcher controller", func() { // Reset callback tracking. profileChanges = &atomicSlice[profileChange]{} + adherencePolicyChanges = &atomicSlice[adherencePolicyChange]{} }) AfterEach(func() { @@ -101,18 +108,22 @@ var _ = Describe("SecurityProfileWatcher controller", func() { Expect(k8sClient.Delete(ctx, apiServer)).To(Succeed()) }) - startManager := func(initialProfile configv1.TLSProfileSpec) { + startManager := func(initialProfile configv1.TLSProfileSpec, initialAdherencePolicy configv1.TLSAdherencePolicy) { var mgrCtx context.Context mgrCtx, mgrCancel = context.WithCancel(ctx) mgrDone = make(chan struct{}) // Set up the TLS security profile watcher controller. watcher := &SecurityProfileWatcher{ - Client: mgr.GetClient(), - InitialTLSProfileSpec: initialProfile, + Client: mgr.GetClient(), + InitialTLSProfileSpec: initialProfile, + InitialTLSAdherencePolicy: initialAdherencePolicy, OnProfileChange: func(_ context.Context, oldSpec, newSpec configv1.TLSProfileSpec) { profileChanges.Append(profileChange{old: oldSpec, new: newSpec}) }, + OnAdherencePolicyChange: func(_ context.Context, oldPolicy, newPolicy configv1.TLSAdherencePolicy) { + adherencePolicyChanges.Append(adherencePolicyChange{old: oldPolicy, new: newPolicy}) + }, } Expect(watcher.SetupWithManager(mgr)).To(Succeed()) @@ -135,7 +146,7 @@ var _ = Describe("SecurityProfileWatcher controller", func() { // Start with the intermediate profile (same as what's configured). initialProfile, err := GetTLSProfileSpec(apiServer.Spec.TLSSecurityProfile) Expect(err).NotTo(HaveOccurred()) - startManager(initialProfile) + startManager(initialProfile, apiServer.Spec.TLSAdherence) // Wait a bit and verify callback was not invoked. Consistently(profileChanges.Len).Should(Equal(0), "callback should not be invoked") @@ -145,7 +156,7 @@ var _ = Describe("SecurityProfileWatcher controller", func() { // Start with the intermediate profile. initialProfile, err := GetTLSProfileSpec(apiServer.Spec.TLSSecurityProfile) Expect(err).NotTo(HaveOccurred()) - startManager(initialProfile) + startManager(initialProfile, apiServer.Spec.TLSAdherence) // Get the intermediate profile spec to replicate it exactly. intermediateSpec := *configv1.TLSProfiles[configv1.TLSProfileIntermediateType] @@ -179,7 +190,7 @@ var _ = Describe("SecurityProfileWatcher controller", func() { // Start with the custom profile. initialProfile, err := GetTLSProfileSpec(apiServer.Spec.TLSSecurityProfile) Expect(err).NotTo(HaveOccurred()) - startManager(initialProfile) + startManager(initialProfile, apiServer.Spec.TLSAdherence) // Switch to the intermediate profile (which has identical settings). apiServer.Spec.TLSSecurityProfile = &configv1.TLSSecurityProfile{ @@ -197,7 +208,7 @@ var _ = Describe("SecurityProfileWatcher controller", func() { // Start with the intermediate profile. initialProfile, err := GetTLSProfileSpec(apiServer.Spec.TLSSecurityProfile) Expect(err).NotTo(HaveOccurred()) - startManager(initialProfile) + startManager(initialProfile, apiServer.Spec.TLSAdherence) // Update the APIServer to use the Modern profile (which has TLS 1.3). apiServer.Spec.TLSSecurityProfile = &configv1.TLSSecurityProfile{ @@ -220,7 +231,7 @@ var _ = Describe("SecurityProfileWatcher controller", func() { // Start with the intermediate profile. initialProfile, err := GetTLSProfileSpec(apiServer.Spec.TLSSecurityProfile) Expect(err).NotTo(HaveOccurred()) - startManager(initialProfile) + startManager(initialProfile, apiServer.Spec.TLSAdherence) // Define the custom profile we'll switch to. customSpec := configv1.TLSProfileSpec{ @@ -262,7 +273,7 @@ var _ = Describe("SecurityProfileWatcher controller", func() { // Start with the custom profile. initialProfile, err := GetTLSProfileSpec(apiServer.Spec.TLSSecurityProfile) Expect(err).NotTo(HaveOccurred()) - startManager(initialProfile) + startManager(initialProfile, apiServer.Spec.TLSAdherence) // Switch back to the intermediate profile. apiServer.Spec.TLSSecurityProfile = &configv1.TLSSecurityProfile{ @@ -278,7 +289,7 @@ var _ = Describe("SecurityProfileWatcher controller", func() { // Start with the intermediate profile (profile A). initialProfile, err := GetTLSProfileSpec(apiServer.Spec.TLSSecurityProfile) Expect(err).NotTo(HaveOccurred()) - startManager(initialProfile) + startManager(initialProfile, apiServer.Spec.TLSAdherence) // Change from A (Intermediate) to B (Modern). apiServer.Spec.TLSSecurityProfile = &configv1.TLSSecurityProfile{ @@ -325,7 +336,7 @@ var _ = Describe("SecurityProfileWatcher controller", func() { // Start with the default (nil -> intermediate) profile. initialProfile, err := GetTLSProfileSpec(nil) Expect(err).NotTo(HaveOccurred()) - startManager(initialProfile) + startManager(initialProfile, apiServer.Spec.TLSAdherence) // Update the APIServer to use the Modern profile. apiServer.Spec.TLSSecurityProfile = &configv1.TLSSecurityProfile{ @@ -337,4 +348,25 @@ var _ = Describe("SecurityProfileWatcher controller", func() { Eventually(profileChanges.Len).Should(Equal(1), "callback should be invoked once") }) }) + + Context("when the TLS adherence policy changes", func() { + It("should invoke the callback when policy changes", func() { + // Start with the intermediate profile. + initialProfile, err := GetTLSProfileSpec(apiServer.Spec.TLSSecurityProfile) + Expect(err).NotTo(HaveOccurred()) + startManager(initialProfile, apiServer.Spec.TLSAdherence) + + // Update the APIServer to use a different adherence policy. + apiServer.Spec.TLSAdherence = configv1.TLSAdherencePolicyStrictAllComponents + Expect(k8sClient.Update(ctx, apiServer)).To(Succeed()) + + // Verify callback was invoked. + Eventually(adherencePolicyChanges.Len).Should(Equal(1), "callback should be invoked once") + + // Verify the callback received the correct policies. + change := adherencePolicyChanges.Index(0) + Expect(change.old).To(Equal(configv1.TLSAdherencePolicyNoOpinion), "callback should receive the initial policy as old") + Expect(change.new).To(Equal(configv1.TLSAdherencePolicyStrictAllComponents), "callback should receive the current policy as new") + }) + }) }) From 6110d596978cb9644c81aecce4c2b609e2e0865f Mon Sep 17 00:00:00 2001 From: Jeremiah Stuever Date: Thu, 5 Mar 2026 14:29:12 -0800 Subject: [PATCH 2/2] feat: add FetchAPIServerTLSAdherencePolicy function Add the FetchAPIServerTLSAdherencePolicy function to the tls package to allow retrieving the TLS adherence policy configured in the APIServer resource. This enables consumers to determine the expected TLS adherence behavior directly from the OpenShift cluster configuration. Assisted-by: gemini-3.1-pro-preview --- pkg/tls/tls.go | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/pkg/tls/tls.go b/pkg/tls/tls.go index 6b33bd1..ce1e8c7 100644 --- a/pkg/tls/tls.go +++ b/pkg/tls/tls.go @@ -61,6 +61,19 @@ func FetchAPIServerTLSProfile(ctx context.Context, k8sClient client.Client) (con return profile, nil } +// FetchAPIServerTLSAdherencePolicy fetches the TLS adherence policy configured in APIServer. +// If no policy is configured, the default policy is returned. +func FetchAPIServerTLSAdherencePolicy(ctx context.Context, k8sClient client.Client) (configv1.TLSAdherencePolicy, error) { + apiServer := &configv1.APIServer{} + key := client.ObjectKey{Name: APIServerName} + + if err := k8sClient.Get(ctx, key, apiServer); err != nil { + return configv1.TLSAdherencePolicyNoOpinion, fmt.Errorf("failed to get APIServer %q: %w", key.String(), err) + } + + return apiServer.Spec.TLSAdherence, nil +} + // GetTLSProfileSpec returns TLSProfileSpec for the given profile. // If no profile is configured, the default profile is returned. func GetTLSProfileSpec(profile *configv1.TLSSecurityProfile) (configv1.TLSProfileSpec, error) {