Service accounts accumulate permissions across releases and almost never shed them - parsing every binding to its effective verb set on every commit is what catches drift before it becomes a cluster-takeover path.
Kubernetes RBAC permissions are easy to grant and almost never revoked. A service account that needed create on one resource in v1 still carries that permission in v5, plus three more that a rushed deploy added without a review. The cluster is not misconfigured in any single commit - it drifted there across dozens of them, which is exactly what makes it hard to catch.
Kubernetes RBAC follows a pattern common to every access-control system: permissions are additive in normal operation and rarely audited. A deployment needs read access to ConfigMaps so the team adds a Role and a RoleBinding. Six months later a sidecar needs to patch Secrets; it is faster to extend the same binding than create a new one. A year later the sidecar is gone, the patch permission stays, and the binding now covers a resource it was never meant to reach.
The result is a surface that no single reviewer sees whole. The Terraform or Helm chart in version control reflects the last write, not the cumulative drift from every change before it. Detecting drift means comparing what the manifests say to a baseline of minimum viable permissions - something humans never do consistently at release cadence.
A ClusterRole with verbs: ["*"] and resources: ["*"] is dangerous. A ClusterRoleBinding that attaches that ClusterRole to a workload service account is a fact with a file and a line - not a heuristic and not a conditional guess. The same IaC parser that reads Terraform for public S3 buckets reads Kubernetes manifests for RBAC bindings and surfaces each finding with the manifest path, the subject, the role, and the effective verb-resource pairs.
apiVersion: rbac.authorization.k8s.io/v1 kind: ClusterRoleBinding metadata: name: worker-binding subjects: - kind: ServiceAccount name: worker # workload SA, not a cluster operator namespace: production roleRef: apiGroup: rbac.authorization.k8s.io kind: ClusterRole name: cluster-admin # wildcard on every resource, cluster-wide
Parsing resolves the binding transitively: subject -> role -> effective permissions. A binding to cluster-admin is not just "some wide role" - it resolves to wildcard verbs on every API group cluster-wide, which is the full blast radius of a compromised pod. The finding carries that resolution, not the role name alone.
Not every wide-looking binding is equally dangerous. A ClusterRole that only grants get and list on nodes is different from one that grants create on clusterrolebindings. Severity is decided by the effective permission set the binding resolves to, not the role label someone chose five deployments ago.
| Permission pattern | Blast radius | Severity |
|---|---|---|
| verbs:[*] resources:[*] (cluster-wide) | Full cluster takeover - read all Secrets, modify RBAC, exec anywhere | Critical |
| create/update on clusterrolebindings | Privilege escalation - can grant cluster-admin to any subject | Critical |
| get/list/watch on secrets (cluster-wide) | Mass credential exfiltration across all namespaces | High |
| patch on pods/exec, pods/attach | Remote exec into any pod - container escape prerequisite | High |
| get/list on configmaps (namespaced) | Configuration read - low unless configmaps carry secrets | Low-Med |
The resolution step matters because role composition changes the picture. A custom role that individually looks narrow may aggregate with another binding on the same subject to cover a dangerous combined set. Each subject's full effective permission set - across all its bindings - is what the finding reports, not each binding in isolation.
Detecting RBAC drift once is useful. Detecting it continuously is what prevents re-drift. The same parser that runs on the first scan runs on every subsequent commit - a new ClusterRoleBinding that exceeds the baseline surfaces as a finding before it merges, not three months later during a post-incident review.
apiVersion: rbac.authorization.k8s.io/v1 kind: Role # namespaced, not cluster-wide metadata: namespace: production name: worker-minimal rules: - apiGroups: [""] resources: ["configmaps"] # exactly what the workload reads verbs: ["get", "list"] --- apiVersion: rbac.authorization.k8s.io/v1 kind: RoleBinding metadata: namespace: production name: worker-binding subjects: - kind: ServiceAccount name: worker roleRef: kind: Role name: worker-minimal
The remediation is a scoped Role plus a RoleBinding, not a ClusterRole unless the workload genuinely needs cluster-wide access. Every binding with a legitimate namespace argument should be namespaced. When the cluster is parsed on every commit, the gate can block a new ClusterRoleBinding with wildcard verbs the same way it blocks a public S3 bucket - at the point where stopping it costs the least.
RBAC drift is not a sign of a careless team - it is the default outcome when access control is additive, review is infrequent, and the blast radius of each individual grant looks small. Parsing the manifests on every commit and resolving each binding to its effective permission set turns a quarterly audit into a continuous signal.