Aggregated ClusterRole Labels Silently Grant Unreviewed RBAC Permissions
A ClusterRole label that happens to match an existing aggregation selector silently folds its rules into a trusted aggregate role, granting permissions no reviewer approved.
Operational summary
At a glance
- Symptom
- A subject bound to a supposedly reviewed, narrowly scoped ClusterRole unexpectedly succeeds against kubectl auth can-i checks for verbs and resources that were never…
- Likely cause
- Kubernetes RBAC supports ClusterRole aggregation: a ClusterRole can define an aggregationRule with label selectors, and the API server's controller manager continuously collects the rules…
- Impact
- A ClusterRole intended to be narrow and reviewed in isolation instead widens the effective permissions of every subject bound to the aggregate role it feeds, including cluster-admin-adjacent roles…
- Verification signal
- Re-run the same diagnostic commands used to detect the trap and confirm the aggregate role's effective rules have shrunk back to its pre-incident state.
- Safe correction
- Remove the unintended aggregation label from the narrow ClusterRole, or replace it with a label that does not match any existing clusterRoleSelectors in the cluster, so the role…
- Rollback or recovery
- If removing the label produces unexpected denial of access that a dependent workload actually required, restore the previous label value from the version-controlled manifest history and re-apply it…
Symptom
A subject bound to a supposedly reviewed, narrowly scoped ClusterRole unexpectedly succeeds against kubectl auth can-i checks for verbs and resources that were never explicitly granted in that role’s own rules. The team reviewing the ClusterRole manifest sees only a handful of read-only rules, yet the effective permissions observed via kubectl auth can-i --list --as=<subject> include write access to secrets or other sensitive resources across the cluster.
False Assumption
The reviewer assumes that a ClusterRole’s YAML manifest is a complete and closed description of what it grants: read the rules block, sum the verbs and resources, and that is the entire permission surface. This assumption holds for an ordinary ClusterRole with no aggregation labels, so it is easy to generalise from experience with those roles to all ClusterRoles. It does not hold once aggregationRule.clusterRoleSelectors is present on a target role and other ClusterRoles in the cluster carry matching labels.
Root Cause
Kubernetes RBAC supports ClusterRole aggregation: a ClusterRole can define an aggregationRule with label selectors, and the API server’s controller manager continuously collects the rules from every other ClusterRole whose labels match those selectors, merging them into the aggregate role’s effective rules field. This is documented, intended behaviour, not a bug. The trap occurs when a new ClusterRole is created with a label that happens to match an existing aggregation selector (for example, the built-in rbac.authorization.k8s.io/aggregate-to-admin: "true"" label consumed by the cluster's default admin ClusterRole), and the author of the new role has no reason to inspect the wider cluster for aggregation selectors before adding that label. The new role's rules are silently folded into every aggregate role that selects on that label, and any subject already bound to one of those aggregate roles inherits the new permissions immediately, with no admission control step, no diff, and no explicit approval of the newly aggregated rules.
Impact
A ClusterRole intended to be narrow and reviewed in isolation instead widens the effective permissions of every subject bound to the aggregate role it feeds, including cluster-admin-adjacent roles in clusters that use the default aggregate-to-admin or aggregate-to-edit conventions. Because the merge happens continuously and automatically, permissions can change after a role is reviewed and merged into version control, without any further pull request, simply because a label match now exists. The blast radius depends entirely on which aggregate roles select the label and which subjects are bound to those aggregates; in clusters that bind admin broadly, this can extend to workload identities and human operators well outside the original role's intended scope.
Diagnosis
Confirm aggregation is in play before assuming any other cause. First inspect the ClusterRole believed to be narrowly scoped and check for the labels it carries, then search the cluster for ClusterRoles with an aggregationRule whose clusterRoleSelectors match those labels. Compare the ClusterRole's own rules block against the effective merged rules on any aggregate role it feeds, and compare a subject's kubectl auth can-i --list output against what the narrow role's static manifest would suggest in isolation.
kubectl get clusterrole <narrow-role-name> -o jsonpath='{.metadata.labels}'
kubectl get clusterroles -o json | jq -r '.items[] | select(.aggregationRule != null) | {name:.metadata.name, selectors:.aggregationRule.clusterRoleSelectors}'
kubectl get clusterrole <aggregate-role-name> -o jsonpath='{.rules}'
kubectl auth can-i --list --as=<subject>
Expected evidence: the aggregation search returns at least one aggregate ClusterRole whose selector labels match the narrow role's labels, and the aggregate role's merged rules field contains entries not present in the narrow role's own manifest.
Correction
Remove the unintended aggregation label from the narrow ClusterRole, or replace it with a label that does not match any existing clusterRoleSelectors in the cluster, so the role no longer feeds an aggregate it was not designed to feed. Apply the corrected manifest in the isolated validation environment first, confirm the aggregate role's merged rules no longer includes the narrow role's entries, and only then promote the change through the normal review pipeline to any environment where the original mislabelled role was applied.
metadata:
labels:
# remove or rename this label if it unintentionally matches
# an existing aggregationRule.clusterRoleSelectors entry
rbac.authorization.k8s.io/aggregate-to-admin: "true"
Validation
Re-run the same diagnostic commands used to detect the trap and confirm the aggregate role's effective rules have shrunk back to its pre-incident state. After applying the corrected label, re-check the aggregate ClusterRole's merged rules field and re-run kubectl auth can-i --list --as=<subject> for any subject bound to the affected aggregate role, confirming the previously unexpected permissions are gone and no unrelated permissions were removed.
Rollback
If removing the label produces unexpected denial of access that a dependent workload actually required, restore the previous label value from the version-controlled manifest history and re-apply it in the validation environment, then re-open the labelling decision as a reviewed, deliberate aggregation choice rather than an accidental one. Keep the pre-change and post-change manifests and the two kubectl auth can-i --list outputs as the rollback evidence trail; do not re-apply the original unreviewed label directly to any environment without that recorded decision.
Prevention
Before adding or changing labels on any ClusterRole, run the aggregation-selector search shown in Diagnosis against the target cluster and treat a match as a required, explicit review item rather than an incidental detail. Where the cluster's RBAC model uses the built-in aggregation convention, document which labels are reserved for aggregation and require any ClusterRole author to check that list before applying labels, and add the aggregation search command to routine RBAC change review rather than relying on manifest inspection alone.
Apply the safer control
Before you change production
Confirm the affected scope, export the current configuration, and test the replacement control in a non-production environment first.
Fix commands and configuration
kubectl auth can-ikubectl auth can-i --list --as=<subject>aggregationRule.clusterRoleSelectorsVerify, roll back or escalate
Verify
Re-run the same diagnostic commands used to detect the trap and confirm the aggregate role's effective rules have shrunk back to its pre-incident state.
Rollback
If removing the label produces unexpected denial of access that a dependent workload actually required, restore the previous label value from the version-controlled manifest history and re-apply it in the validation environment, then re-open the labelling decision as a reviewed…
Escalate
Escalate when the blast radius is uncertain, the control cannot be tested safely, or remediation requires an outage or security exception.