From 3d72d8918e998da90f7015d557bdfa3868214905 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 --- go.mod | 12 +++---- go.sum | 24 +++++++------- pkg/tls/controller.go | 17 ++++++++++ pkg/tls/controller_test.go | 64 ++++++++++++++++++++++++++++---------- 4 files changed, 83 insertions(+), 34 deletions(-) diff --git a/go.mod b/go.mod index b77e042..21c34ef 100644 --- a/go.mod +++ b/go.mod @@ -6,12 +6,12 @@ require ( github.com/go-logr/logr v1.4.3 github.com/onsi/ginkgo/v2 v2.28.1 github.com/onsi/gomega v1.39.1 - github.com/openshift/api v0.0.0-20260213155647-8fe9fe363807 + github.com/openshift/api v0.0.0-20260317165824-54a3998d81eb github.com/openshift/library-go v0.0.0-20260213153706-03f1709971c5 - k8s.io/apimachinery v0.35.1 - k8s.io/client-go v0.35.1 - k8s.io/utils v0.0.0-20260108192941-914a6e750570 - sigs.k8s.io/controller-runtime v0.23.2 + k8s.io/apimachinery v0.35.2 + k8s.io/client-go v0.35.2 + k8s.io/utils v0.0.0-20260210185600-b8788abfbbc2 + sigs.k8s.io/controller-runtime v0.23.3 ) require ( @@ -64,7 +64,7 @@ require ( gopkg.in/evanphx/json-patch.v4 v4.13.0 // indirect gopkg.in/inf.v0 v0.9.1 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect - k8s.io/api v0.35.1 // indirect + k8s.io/api v0.35.2 // indirect k8s.io/apiextensions-apiserver v0.35.1 // indirect k8s.io/apiserver v0.35.1 // indirect k8s.io/klog/v2 v2.130.1 // indirect diff --git a/go.sum b/go.sum index b54e5c5..e7b8926 100644 --- a/go.sum +++ b/go.sum @@ -89,8 +89,8 @@ github.com/onsi/ginkgo/v2 v2.28.1 h1:S4hj+HbZp40fNKuLUQOYLDgZLwNUVn19N3Atb98NCyI github.com/onsi/ginkgo/v2 v2.28.1/go.mod h1:CLtbVInNckU3/+gC8LzkGUb9oF+e8W8TdUsxPwvdOgE= github.com/onsi/gomega v1.39.1 h1:1IJLAad4zjPn2PsnhH70V4DKRFlrCzGBNrNaru+Vf28= github.com/onsi/gomega v1.39.1/go.mod h1:hL6yVALoTOxeWudERyfppUcZXjMwIMLnuSfruD2lcfg= -github.com/openshift/api v0.0.0-20260213155647-8fe9fe363807 h1:coR/haF16EW8KS1E/PwJfDzMSy4mU9K0H1rcHejqYDY= -github.com/openshift/api v0.0.0-20260213155647-8fe9fe363807/go.mod h1:d5uzF0YN2nQQFA0jIEWzzOZ+edmo6wzlGLvx5Fhz4uY= +github.com/openshift/api v0.0.0-20260317165824-54a3998d81eb h1:iwBR3mzmyE3EMFx7R3CQ9lOccTS0dNht8TW82aGITg0= +github.com/openshift/api v0.0.0-20260317165824-54a3998d81eb/go.mod h1:pyVjK0nZ4sRs4fuQVQ4rubsJdahI1PB94LnQ8sGdvxo= github.com/openshift/library-go v0.0.0-20260213153706-03f1709971c5 h1:9Pe6iVOMjt9CdA/vaKBNUSoEIjIe1po5Ha3ABRYXLJI= github.com/openshift/library-go v0.0.0-20260213153706-03f1709971c5/go.mod h1:K3FoNLgNBFYbFuG+Kr8usAnQxj1w84XogyUp2M8rK8k= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= @@ -173,24 +173,24 @@ gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= -k8s.io/api v0.35.1 h1:0PO/1FhlK/EQNVK5+txc4FuhQibV25VLSdLMmGpDE/Q= -k8s.io/api v0.35.1/go.mod h1:28uR9xlXWml9eT0uaGo6y71xK86JBELShLy4wR1XtxM= +k8s.io/api v0.35.2 h1:tW7mWc2RpxW7HS4CoRXhtYHSzme1PN1UjGHJ1bdrtdw= +k8s.io/api v0.35.2/go.mod h1:7AJfqGoAZcwSFhOjcGM7WV05QxMMgUaChNfLTXDRE60= k8s.io/apiextensions-apiserver v0.35.1 h1:p5vvALkknlOcAqARwjS20kJffgzHqwyQRM8vHLwgU7w= k8s.io/apiextensions-apiserver v0.35.1/go.mod h1:2CN4fe1GZ3HMe4wBr25qXyJnJyZaquy4nNlNmb3R7AQ= -k8s.io/apimachinery v0.35.1 h1:yxO6gV555P1YV0SANtnTjXYfiivaTPvCTKX6w6qdDsU= -k8s.io/apimachinery v0.35.1/go.mod h1:jQCgFZFR1F4Ik7hvr2g84RTJSZegBc8yHgFWKn//hns= +k8s.io/apimachinery v0.35.2 h1:NqsM/mmZA7sHW02JZ9RTtk3wInRgbVxL8MPfzSANAK8= +k8s.io/apimachinery v0.35.2/go.mod h1:jQCgFZFR1F4Ik7hvr2g84RTJSZegBc8yHgFWKn//hns= k8s.io/apiserver v0.35.1 h1:potxdhhTL4i6AYAa2QCwtlhtB1eCdWQFvJV6fXgJzxs= k8s.io/apiserver v0.35.1/go.mod h1:BiL6Dd3A2I/0lBnteXfWmCFobHM39vt5+hJQd7Lbpi4= -k8s.io/client-go v0.35.1 h1:+eSfZHwuo/I19PaSxqumjqZ9l5XiTEKbIaJ+j1wLcLM= -k8s.io/client-go v0.35.1/go.mod h1:1p1KxDt3a0ruRfc/pG4qT/3oHmUj1AhSHEcxNSGg+OA= +k8s.io/client-go v0.35.2 h1:YUfPefdGJA4aljDdayAXkc98DnPkIetMl4PrKX97W9o= +k8s.io/client-go v0.35.2/go.mod h1:4QqEwh4oQpeK8AaefZ0jwTFJw/9kIjdQi0jpKeYvz7g= k8s.io/klog/v2 v2.130.1 h1:n9Xl7H1Xvksem4KFG4PYbdQCQxqc/tTUyrgXaOhHSzk= k8s.io/klog/v2 v2.130.1/go.mod h1:3Jpz1GvMt720eyJH1ckRHK1EDfpxISzJ7I9OYgaDtPE= k8s.io/kube-openapi v0.0.0-20250910181357-589584f1c912 h1:Y3gxNAuB0OBLImH611+UDZcmKS3g6CthxToOb37KgwE= k8s.io/kube-openapi v0.0.0-20250910181357-589584f1c912/go.mod h1:kdmbQkyfwUagLfXIad1y2TdrjPFWp2Q89B3qkRwf/pQ= -k8s.io/utils v0.0.0-20260108192941-914a6e750570 h1:JT4W8lsdrGENg9W+YwwdLJxklIuKWdRm+BC+xt33FOY= -k8s.io/utils v0.0.0-20260108192941-914a6e750570/go.mod h1:xDxuJ0whA3d0I4mf/C4ppKHxXynQ+fxnkmQH0vTHnuk= -sigs.k8s.io/controller-runtime v0.23.2 h1:Oh3FliXaA2CS1chpUXvjVNJtsvGZYUxQH8s7bvR7aXk= -sigs.k8s.io/controller-runtime v0.23.2/go.mod h1:B6COOxKptp+YaUT5q4l6LqUJTRpizbgf9KSRNdQGns0= +k8s.io/utils v0.0.0-20260210185600-b8788abfbbc2 h1:AZYQSJemyQB5eRxqcPky+/7EdBj0xi3g0ZcxxJ7vbWU= +k8s.io/utils v0.0.0-20260210185600-b8788abfbbc2/go.mod h1:xDxuJ0whA3d0I4mf/C4ppKHxXynQ+fxnkmQH0vTHnuk= +sigs.k8s.io/controller-runtime v0.23.3 h1:VjB/vhoPoA9l1kEKZHBMnQF33tdCLQKJtydy4iqwZ80= +sigs.k8s.io/controller-runtime v0.23.3/go.mod h1:B6COOxKptp+YaUT5q4l6LqUJTRpizbgf9KSRNdQGns0= sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730 h1:IpInykpT6ceI+QxKBbEflcR5EXP7sU1kvOlxwZh5txg= sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730/go.mod h1:mdzfpAEoE6DHQEN0uh9ZbOCuHbLK5wOm7dK4ctXE9Tg= sigs.k8s.io/randfill v1.0.0 h1:JfjMILfT8A6RbawdsK2JXGBR5AQVfd+9TbzrlneTyrU= 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 b569ff023a0827b05314befe9943839e3ff2d76b 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) {