Skip to content

Commit af410f9

Browse files
jcantrillclaude
authored andcommitted
LOG-9424: Fix API flooding when MultiCLF shares the same serviceAccount (#3295)
* fix(auth): Change SCC Role naming to prevent conflicts when CLFs share service accounts When multiple ClusterLogForwarders in the same namespace share the same spec.serviceAccount, they were creating a shared Role named {sa}-scc. This caused both CLFs to continuously update the Role's ownerReferences, alternating between pointing to CLF #1 and CLF #2, flooding the API server with thousands of update requests. Changed the Role naming from {sa}-scc to {clfName}-{sa}-scc, making each CLF's Role unique. The RoleBinding now correctly references this new Role name format. Both resources keep their owner references for proper cleanup when a CLF is deleted. Also removed owner reference from the metadata-reader ClusterRoleBinding because cluster-scoped resources should not be owned by namespaced resources, following Kubernetes best practices. Resolves: LOG-9424 Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com> * feat(reconcile): Add cleanup logic for old SCC Role resources Add DeleteRole function to enable cleanup of Resources with the old naming scheme ({sa}-scc) during reconciliation. This ensures smooth migration from the previous implementation where multiple CLFs sharing a service account would conflict. The ReconcileRBAC function now deletes any old Role using the legacy naming scheme after creating the new one, preventing orphaned resources during upgrades. Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com> * test(auth): Update tests for new SCC RBAC resource naming scheme Update existing tests to reflect the new naming scheme where SCC Roles are named {clfName}-{sa}-scc instead of {sa}-scc. This ensures each CLF gets a unique Role when sharing a service account. Add test to verify that metadata-reader ClusterRoleBinding does not have owner references, ensuring compliance with Kubernetes best practices (cluster-scoped resources should not be owned by namespaced resources). Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com> * check role references before deleting --------- Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com>
1 parent e23c9a9 commit af410f9

3 files changed

Lines changed: 66 additions & 19 deletions

File tree

internal/auth/rbac.go

Lines changed: 42 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package auth
22

33
import (
4+
"context"
45
"fmt"
56
"github.com/openshift/cluster-logging-operator/internal/reconcile"
67
"github.com/openshift/cluster-logging-operator/internal/runtime"
@@ -12,21 +13,50 @@ import (
1213

1314
// ReconcileRBAC reconciles the RBAC specifically for the service account and SCC
1415
func ReconcileRBAC(k8sClient client.Client, rbacName, saNamespace, saName string, owner metav1.OwnerReference) error {
15-
desiredCRB := NewMetaDataReaderClusterRoleBinding(saNamespace, saName, owner)
16+
desiredCRB := NewMetaDataReaderClusterRoleBinding(saNamespace, saName)
1617
if err := reconcile.ClusterRoleBinding(k8sClient, desiredCRB.Name, func() *rbacv1.ClusterRoleBinding { return desiredCRB }); err != nil {
1718
return err
1819
}
19-
desiredSCCRole := NewServiceAccountSCCRole(saNamespace, saName, owner)
20+
desiredSCCRole := NewServiceAccountSCCRole(saNamespace, rbacName, saName, owner)
2021
if err := reconcile.Role(k8sClient, desiredSCCRole); err != nil {
2122
return err
2223
}
2324

24-
desiredSCCRoleBinding := NewServiceAccountSCCRoleBinding(saNamespace, rbacName, desiredSCCRole.Name, saName, owner)
25-
return reconcile.RoleBinding(k8sClient, desiredSCCRoleBinding)
25+
desiredSCCRoleBinding := NewServiceAccountSCCRoleBinding(saNamespace, rbacName, saName, owner)
26+
if err := reconcile.RoleBinding(k8sClient, desiredSCCRoleBinding); err != nil {
27+
return err
28+
}
29+
30+
// Cleanup old resources with previous naming scheme
31+
oldRoleName := fmt.Sprintf("%s-scc", saName)
32+
33+
// List all RoleBindings in the namespace to check if any reference the old role
34+
roleBindings := &rbacv1.RoleBindingList{}
35+
if err := k8sClient.List(context.TODO(), roleBindings, client.InNamespace(saNamespace)); err != nil {
36+
return err
37+
}
38+
39+
// Check if any RoleBinding references the old role
40+
hasReferences := false
41+
for _, rb := range roleBindings.Items {
42+
if rb.RoleRef.Name == oldRoleName {
43+
hasReferences = true
44+
break
45+
}
46+
}
47+
48+
// Only delete the old role if no RoleBindings reference it
49+
if !hasReferences {
50+
if err := reconcile.DeleteRole(k8sClient, saNamespace, oldRoleName); err != nil {
51+
return err
52+
}
53+
}
54+
55+
return nil
2656
}
2757

2858
// NewMetaDataReaderClusterRoleBinding stubs a clusterrolebinding to allow reading of pod metadata (i.e. labels)
29-
func NewMetaDataReaderClusterRoleBinding(saNamespace, saName string, owner metav1.OwnerReference) *rbacv1.ClusterRoleBinding {
59+
func NewMetaDataReaderClusterRoleBinding(saNamespace, saName string) *rbacv1.ClusterRoleBinding {
3060
name := fmt.Sprintf("metadata-reader-%s-%s", saNamespace, saName)
3161
desired := runtime.NewClusterRoleBinding(name,
3262
rbacv1.RoleRef{
@@ -41,26 +71,26 @@ func NewMetaDataReaderClusterRoleBinding(saNamespace, saName string, owner metav
4171
},
4272
)
4373

44-
utils.AddOwnerRefToObject(desired, owner)
4574
return desired
4675
}
4776

48-
func NewServiceAccountSCCRole(namespace, name string, owner metav1.OwnerReference) *rbacv1.Role {
49-
name = fmt.Sprintf("%s-scc", name)
77+
func NewServiceAccountSCCRole(namespace, name, saName string, owner metav1.OwnerReference) *rbacv1.Role {
78+
roleName := fmt.Sprintf("%s-%s-scc", name, saName)
5079
sccRule := rbacv1.PolicyRule{
5180
APIGroups: []string{"security.openshift.io"},
5281
ResourceNames: []string{sccName},
5382
Resources: []string{"securitycontextconstraints"},
5483
Verbs: []string{"use"},
5584
}
5685

57-
desired := runtime.NewRole(namespace, name, sccRule)
86+
desired := runtime.NewRole(namespace, roleName, sccRule)
5887

5988
utils.AddOwnerRefToObject(desired, owner)
6089
return desired
6190
}
6291

63-
func NewServiceAccountSCCRoleBinding(namespace, name, roleName, saName string, owner metav1.OwnerReference) *rbacv1.RoleBinding {
92+
func NewServiceAccountSCCRoleBinding(namespace, name, saName string, owner metav1.OwnerReference) *rbacv1.RoleBinding {
93+
roleName := fmt.Sprintf("%s-%s-scc", name, saName)
6494
roleRef := rbacv1.RoleRef{
6595
APIGroup: rbacv1.GroupName,
6696
Kind: "Role",
@@ -73,8 +103,8 @@ func NewServiceAccountSCCRoleBinding(namespace, name, roleName, saName string, o
73103
Namespace: namespace,
74104
}
75105

76-
name = fmt.Sprintf("%s-scc", name)
77-
desired := runtime.NewRoleBinding(namespace, name, roleRef, subject)
106+
roleBindingName := fmt.Sprintf("%s-scc", name)
107+
desired := runtime.NewRoleBinding(namespace, roleBindingName, roleRef, subject)
78108

79109
utils.AddOwnerRefToObject(desired, owner)
80110
return desired

internal/auth/rbac_test.go

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

1212
var _ = Describe("NewMetaDataReaderClusterRoleBinding", func() {
1313
It("should stub a well-formed clusterrolebinding", func() {
14-
Expect(test.YAMLString(auth.NewMetaDataReaderClusterRoleBinding(constants.OpenshiftNS, "logcollector", metav1.OwnerReference{}))).To(MatchYAML(
14+
Expect(test.YAMLString(auth.NewMetaDataReaderClusterRoleBinding(constants.OpenshiftNS, "logcollector"))).To(MatchYAML(
1515
`apiVersion: rbac.authorization.k8s.io/v1
1616
kind: ClusterRoleBinding
1717
metadata:
@@ -27,16 +27,21 @@ subjects:
2727
namespace: openshift-logging
2828
`))
2929
})
30+
31+
It("should not have owner references", func() {
32+
crb := auth.NewMetaDataReaderClusterRoleBinding(constants.OpenshiftNS, "logcollector")
33+
Expect(crb.OwnerReferences).To(BeEmpty())
34+
})
3035
})
3136

3237
var _ = Describe("ServiceAccount SCC Role & RoleBinding", func() {
3338
It("should stub a well-formed role", func() {
34-
Expect(test.YAMLString(auth.NewServiceAccountSCCRole(constants.OpenshiftNS, "my-sa", metav1.OwnerReference{}))).To(MatchYAML(
39+
Expect(test.YAMLString(auth.NewServiceAccountSCCRole(constants.OpenshiftNS, "my-clf", "my-sa", metav1.OwnerReference{}))).To(MatchYAML(
3540
`apiVersion: rbac.authorization.k8s.io/v1
3641
kind: Role
3742
metadata:
3843
creationTimestamp: null
39-
name: my-sa-scc
44+
name: my-clf-my-sa-scc
4045
namespace: openshift-logging
4146
rules:
4247
- apiGroups:
@@ -51,20 +56,20 @@ rules:
5156
})
5257

5358
It("should stub a well-formed roleBinding", func() {
54-
Expect(test.YAMLString(auth.NewServiceAccountSCCRoleBinding(constants.OpenshiftNS, "my-sa", "customSA-scc", "customSA", metav1.OwnerReference{}))).To(MatchYAML(
59+
Expect(test.YAMLString(auth.NewServiceAccountSCCRoleBinding(constants.OpenshiftNS, "my-clf", "my-sa", metav1.OwnerReference{}))).To(MatchYAML(
5560
`apiVersion: rbac.authorization.k8s.io/v1
5661
kind: RoleBinding
5762
metadata:
5863
creationTimestamp: null
59-
name: my-sa-scc
64+
name: my-clf-scc
6065
namespace: openshift-logging
6166
roleRef:
6267
apiGroup: rbac.authorization.k8s.io
6368
kind: Role
64-
name: customSA-scc
69+
name: my-clf-my-sa-scc
6570
subjects:
6671
- kind: ServiceAccount
67-
name: customSA
72+
name: my-sa
6873
namespace: openshift-logging
6974
`))
7075
})

internal/reconcile/rbac.go

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import (
77
log "github.com/ViaQ/logerr/v2/log/static"
88
"github.com/openshift/cluster-logging-operator/internal/runtime"
99
rbacv1 "k8s.io/api/rbac/v1"
10+
apierrors "k8s.io/apimachinery/pkg/api/errors"
1011
"sigs.k8s.io/controller-runtime/pkg/client"
1112
"sigs.k8s.io/controller-runtime/pkg/controller/controllerutil"
1213
)
@@ -69,3 +70,14 @@ func DeleteClusterRoleBinding(k8sClient client.Client, name string) error {
6970
log.V(3).Info("Deleting", "object", object)
7071
return k8sClient.Delete(context.TODO(), object)
7172
}
73+
74+
func DeleteRole(k8sClient client.Client, namespace, name string) error {
75+
object := runtime.NewRole(namespace, name)
76+
log.V(3).Info("Deleting Role", "namespace", namespace, "name", name)
77+
err := k8sClient.Delete(context.TODO(), object)
78+
// Ignore NotFound errors - resource is already deleted
79+
if apierrors.IsNotFound(err) {
80+
return nil
81+
}
82+
return err
83+
}

0 commit comments

Comments
 (0)