Platform · Jul 13, 2026 · 7 min read

Kubernetes RBAC drift: when cluster permissions outlive their reason

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.

01How drift accumulates across releases

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.

Release 1 sa/worker get,list configmaps scoped, minimal ──► Release 4 sa/worker get,list configmaps patch secrets sidecar added ──► Release 9 sa/worker get,list configmaps patch secrets sidecar removed * pods debug session, never cleaned ──► ClusterRoleBinding sa/worker ──► cluster-admin "just for the incident"
Permissions accumulate across releases. The footprint grows; the reason for each grant fades from memory.

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.

02Parsing RBAC from IaC - facts, not estimates

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.

worker-rbac.yamlyaml
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.

Proven - CriticalClusterRoleBinding - cluster-admin on workload SA - worker-rbac.yaml:1
ClusterRoleBinding worker-binding subject ──► ServiceAccount worker (ns: production) roleRef ──► ClusterRole cluster-admin resolves to ──► verbs:[*] resources:[*] apiGroups:[*] (cluster-wide) A compromised or hijacked pod running under sa/worker can read Secrets, exec into any pod, modify RBAC, and delete workloads across all namespaces.
Oracle: IaC parse + transitive role resolution. Severity critical - a single compromised process becomes a full cluster takeover path.

03Severity by effective permissions, not role name

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 patternBlast radiusSeverity
verbs:[*] resources:[*] (cluster-wide)Full cluster takeover - read all Secrets, modify RBAC, exec anywhereCritical
create/update on clusterrolebindingsPrivilege escalation - can grant cluster-admin to any subjectCritical
get/list/watch on secrets (cluster-wide)Mass credential exfiltration across all namespacesHigh
patch on pods/exec, pods/attachRemote exec into any pod - container escape prerequisiteHigh
get/list on configmaps (namespaced)Configuration read - low unless configmaps carry secretsLow-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.

04Closing the loop - baseline, gate, re-audit

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.

least-privilege-worker.yamlyaml
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.

0
network requests during RBAC audit
100%
IaC-parse coverage per commit
1
posture view - RBAC alongside code and cloud

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.

Evidence over heuristicsReachable beats foundMonitor → Block: rolling out a gate without slowing teamsIaC-grounded threat modelingAir-gapped AppSec with no phone-homeOne platform vs three tools
See it on your own app →