20.4 Aggregated ClusterRoles
Right, so you’ve got the hang of regular ClusterRoles and Roles. You can build a neat little permission set, bind it to a user or a ServiceAccount, and call it a day. It’s straightforward, until your infrastructure starts to look less like a tidy garden and more like a jungle. You might have a dozen different controllers, each needing a slightly different set of permissions, and the thought of managing fifty near-identical ClusterRoles is enough to make you consider a career change.
This is where the designers of Kubernetes threw us a bone, and it’s a pretty good one: Aggregated ClusterRoles. The core idea is brilliantly simple and will feel familiar if you’ve worked with tags in other systems. Instead of defining a single, monolithic ClusterRole that lists every single verb and resource, you can create a ClusterRole that aggregates other ClusterRoles together. It’s composition over inheritance, and it’s how you stay sane.
The magic is in a simple label selector. You define a ClusterRole not with rules, but with a label query. The Kubernetes controller manager, which is basically the brain of your cluster, constantly watches for ClusterRoles. When it sees one with an aggregationRule, it finds all the ClusterRoles that match the selector, scoops up their rules, and slams them all together into your aggregated ClusterRole. It’s like having a very efficient, hyper-focused intern who builds your permission sets for you automatically.
How aggregation works under the hood
Don’t think of it as fancy inheritance; think of it as a continuous reconciliation loop. You create a ClusterRole with an aggregationRule. This ClusterRole starts its life with absolutely no permissions of its own. It’s an empty vessel. Then, the controller manager sees it, looks at the aggregationRule, and says, “Right, I need to find all the ClusterRoles that match this query.” It finds them, takes their .rules fields, and merges them all into the aggregated ClusterRole’s .rules field.
This isn’t a one-time thing. It’s dynamic. If you create a new ClusterRole five minutes from now that matches the label selector, its rules get automatically rolled into the aggregate. If you delete one, its rules are removed. The aggregated ClusterRole is a living, breathing collection of the current permissions defined by its constituents. This is why it’s so powerful for building complex, modular permission schemes.
Here’s what the YAML for an aggregator looks like. Notice the distinct lack of a rules section and the presence of the aggregationRule.
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
name: ecosystem-master
# Labels here are for *other* people to use to find *this* role.
# They aren't used for aggregation.
aggregationRule:
clusterRoleSelector:
matchLabels:
rbac.ecosystem.io/aggregate-to-master: "true"
# This role will have its rules populated automatically by the controller.
Now, to add a set of permissions to this ecosystem-master role, you just create another ClusterRole with the matching label. Its name doesn’t matter at all, only its labels and its rules.
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
name: pod-reader-for-master
labels:
rbac.ecosystem.io/aggregate-to-master: "true" # The magic label
rules:
- apiGroups: [""]
resources: ["pods"]
verbs: ["get", "list", "watch"]
And here’s another one:
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
name: secret-manager-for-master
labels:
rbac.ecosystem.io/aggregate-to-master: "true"
rules:
- apiGroups: [""]
resources: ["secrets"]
verbs: ["get", "list", "create", "update", "delete"]
Any user or ServiceAccount bound to the ecosystem-master ClusterRole (via a ClusterRoleBinding) will instantly have the combined permissions of both the pod-reader-for-master and secret-manager-for-master roles. It’s a beautiful way to mix and match.
The inescapable pitfalls and best practices
First, the big one: there is no conflict resolution. This isn’t a “most permissive wins” or a “deny overrides” situation. The aggregation system is dumb, in the best way possible: it just concatenates the lists. If one aggregated ClusterRole gives get on pods and another gives get, list, watch on pods, the final effective rule will just be a list of all those verbs. The Kubernetes API server, which is the final arbiter of authorization, handles the deduplication. So the end result is clean. Don’t waste time worrying about verb conflicts.
The real pitfall is label management. The power—and the danger—lies in those labels. If you make your selector too broad (matchLabels: {}), you will accidentally aggregate every single ClusterRole in the cluster, which is a fantastic way to create a god-role and get a panic-induced call from your security team. Be specific. Use a unique, namespaced prefix for your aggregation labels like rbac.<your-project>.io/aggregate-to-<role-name>.
Another best practice: use this for its intended purpose. The classic use case, which the system itself uses, is for building “core” roles that can be extended by other components. Look at the default ClusterRoles in your cluster:
kubectl get clusterrole view -o yaml
You’ll see it has an aggregationRule that selects ClusterRoles with the label rbac.authorization.k8s.io/aggregate-to-view: "true". This is how an Ingress controller or a custom resource definition can add its own read-only permissions to the built-in view role. It’s a public API for permission extension. You should steal this pattern for your own platform-level roles.
Finally, debug by looking at the data. If your aggregation isn’t working as expected, don’t guess. Just describe the aggregated ClusterRole.
kubectl describe clusterrole ecosystem-master
Look at the Rules section in the output. That shows you what the controller manager has actually compiled. If it’s empty, your label selector is wrong or no roles have the right label. If the permissions are wrong, you’re looking at the source of truth. It’s all right there. This is one of those features that feels a bit meta until you use it, and then you’ll wonder how you ever managed RBAC without it.