Safeguard
Kubernetes Security

A practical guide to least privilege in Kubernetes RBAC

One RBAC flaw, CVE-2018-1002105 (CVSS 9.8), let any authenticated user escalate to cluster-admin — here's how to actually scope roles so that never happens again.

Safeguard Research Team
Research
6 min read

Kubernetes RBAC ships enabled by default in every supported distribution, yet the single most common finding in cluster audits is still a ClusterRoleBinding to cluster-admin that nobody can explain. The stakes of getting this wrong are not theoretical: CVE-2018-1002105, disclosed in December 2018 with a CVSS score of 9.8, let any authenticated user who held RBAC permission to exec, attach, or port-forward into a single pod pivot that connection into a fully privileged request against the API server itself — and because default RBAC let any user run discovery calls against aggregated API servers like metrics-server, some clusters were exploitable even without prior authentication. The bug affected every Kubernetes release since 1.0 and was fixed in v1.10.11, v1.11.5, v1.12.3, and v1.13.0-rc.1. Two years later, CVE-2020-8554 showed the same lesson from a different angle: the Kubernetes Product Security Committee decided this one couldn't be patched at all, because it's a direct consequence of a common RBAC grant — write access to Service objects — doing exactly what it's authorized to do. This post walks through how to scope roles, bindings, and service accounts so that authorization matches actual need, and which tools catch it when it doesn't.

What actually goes wrong when RBAC is too broad?

The two clearest public examples are CVE-2018-1002105 and CVE-2020-8554, and they fail in opposite ways. CVE-2018-1002105 was a kube-apiserver code defect: the proxy handler mishandled connection upgrades on pods/exec, pods/attach, and pods/portforward subresources, letting a request that should have been scoped to one pod's backend connection instead ride upstream with the API server's own credentials — a bug fixed in code, not policy. CVE-2020-8554, by contrast, was never a bug in Kubernetes' code at all. Any user with RBAC permission to create or edit a Service and set spec.externalIPs can redirect cluster traffic destined for that IP to a pod they control — a man-in-the-middle vector that the Kubernetes Product Security Committee explicitly declined to fix because the Service API is working as designed (kubernetes/kubernetes#97076). The only real mitigation is not granting broad services write access in multi-tenant clusters, or enforcing it with admission control such as OPA Gatekeeper. Both CVEs point to the same root cause: RBAC grants that are wider than a workload's actual job.

Why do wildcard verbs and cluster-admin bindings keep showing up?

Wildcard grants — verbs: ["*"] or resources: ["*"] in a Role or ClusterRole, and direct ClusterRoleBinding references to the built-in cluster-admin role — are consistently the top finding in independent RBAC audits, because they are also the fastest way to unblock a deployment during an incident. A developer whose Helm chart fails with a permissions error has two options: spend twenty minutes reading kubectl error output to determine the three verbs and two resources actually missing, or bind the service account to cluster-admin and move on. The second option works immediately and is rarely revisited once the deploy succeeds. The practical fix is a workflow, not just a policy: treat any wildcard verb, wildcard resource, or cluster-admin binding as a required-justification item in code review, and prefer Role/RoleBinding (namespace-scoped) over ClusterRole/ClusterRoleBinding unless a workload genuinely needs cross-namespace or cluster-scoped resources like nodes or persistentvolumes.

How do service account tokens turn a compromised pod into a compromised cluster?

Every pod that doesn't explicitly opt out gets a ServiceAccount token auto-mounted at /var/run/secrets/kubernetes.io/serviceaccount/token, and by default that token carries whatever RBAC permissions are bound to its ServiceAccount. If a container is compromised through an application vulnerability — a deserialization bug, a vulnerable dependency, a supply-chain package — the attacker's next move is almost always to read that token and call the API server with it. When the ServiceAccount is bound to a broad Role (or worse, cluster-admin), the blast radius jumps from "one container" to "the namespace" or "the cluster" in a single kubectl call using the stolen token. Two controls close this gap: set automountServiceAccountToken: false on any pod spec or ServiceAccount that never calls the Kubernetes API, and give every ServiceAccount that does need API access its own narrowly scoped Role rather than sharing a general-purpose one across workloads. A ServiceAccount bound to nothing beyond get/list on the one ConfigMap it reads cannot be pivoted into a cluster-admin session even if the pod is fully compromised.

What's the difference between a Role and a ClusterRole, and which should I default to?

A Role grants permissions within a single namespace; a ClusterRole grants permissions either cluster-wide (for non-namespaced resources like nodes or persistentvolumes) or reusably across multiple namespaces when paired with a RoleBinding in each one. The least-privilege default is Role plus RoleBinding, scoped to the namespace a workload actually runs in — this bounds any misconfiguration or compromise to that namespace instead of the whole cluster. ClusterRole is appropriate when the resource itself is cluster-scoped (there's no namespaced equivalent of nodes), or when the same permission set is genuinely needed in many namespaces and you want one definition instead of copy-pasted Role objects. The mistake to avoid is reaching for ClusterRoleBinding out of convenience when a RoleBinding referencing a shared ClusterRole would deliver the identical permissions inside just the namespaces that need them — the resource definition can be cluster-wide even when its grant of access is not.

How do I verify what a role or service account can actually do, rather than what I intended?

Written RBAC YAML and effective permissions frequently diverge once multiple bindings, aggregated ClusterRoles, and inherited defaults stack up, so verification has to be empirical. kubectl auth can-i <verb> <resource> --as=<user> answers a single yes/no question against the live API server; kubectl auth can-i --list --as=system:serviceaccount:<namespace>:<name> dumps the full effective permission set for a given ServiceAccount, which is the command to run before ever approving a new binding. For broader visibility, rakkess renders an access matrix across every resource type for a given identity in one table, and rbac-tool (originally from Alcide, now maintained under Rapid7's insightCloudSec) can visualize the full graph of who-can-do-what across an entire cluster. kubeaudit and the CNCF incubating project kubescape both run automated checks specifically for RBAC anti-patterns — wildcard verbs, cluster-admin bindings, unused permissions — as part of broader configuration audits, and kube-bench checks RBAC-related controls against the CIS Kubernetes Benchmark. None of these replace API audit logging (--audit-policy-file on kube-apiserver), which records every actual request and RBAC decision after the fact — the log you need when a permission was used, not just granted.

Never miss an update

Weekly insights on software supply chain security, delivered to your inbox.