Safeguard
Kubernetes Security

Designing Least-Privilege Kubernetes RBAC: A Practical Guide

CVE-2018-1002105 (CVSS 9.8) let an unauthenticated request reach cluster-admin through pod exec endpoints — most RBAC breaches since trace back to the same handful of over-broad bindings.

Safeguard Research Team
Research
7 min read

Kubernetes RBAC has been generally available since v1.8, built on exactly four API objects — Role, ClusterRole, RoleBinding, and ClusterRoleBinding — yet a decade later, over-broad bindings remain one of the most common ways a single compromised container turns into full cluster takeover. The canonical proof is CVE-2018-1002105, a CVSS 9.8 flaw in kube-apiserver's proxy-request handling that let an unauthenticated, unauthorized request reach the same aggregated API server backend a legitimate request would, effectively granting privilege escalation through paths like pods/exec. It affected every version from 1.0 and required fixes in 1.10.11, 1.11.5, 1.12.3, and 1.13.0-rc.1. What made it so damaging wasn't the bug alone — it was how many clusters had RBAC configured such that reaching those endpoints meant reaching cluster-admin. Nearly every RBAC anti-pattern that follows here — wildcard verbs, cluster-admin bound to service accounts, unrestricted pods/exec, escalate/bind verbs left open, auto-mounted tokens — either enabled that specific CVE's blast radius or represents the same failure mode independently. This post is a practical walkthrough of how RBAC objects actually compose, the five anti-patterns worth auditing for first, and the commands that show you what a role can really do before an attacker finds out for you.

How do Roles, ClusterRoles, and bindings actually fit together?

A Role grants permissions — verbs like get, list, create, delete against resources like pods or secrets, scoped to a single namespace. A ClusterRole is the same permission model but can be applied cluster-wide or reused across many namespaces. Neither object grants anything on its own; a RoleBinding or ClusterRoleBinding is what actually attaches a subject (a user, group, or service account) to that role. The distinction that trips teams up: a ClusterRole bound via a namespaced RoleBinding only grants those permissions inside that one namespace, even though the role itself is cluster-scoped — a pattern Kubernetes' own documentation recommends specifically so you can define one reusable "view" or "edit"-style ClusterRole and bind it narrowly wherever it's needed, instead of writing near-duplicate Roles per namespace. Only a ClusterRoleBinding grants cluster-wide scope. Auditing RBAC means reading bindings, not just roles — a beautifully scoped Role attached via a ClusterRoleBinding is not scoped at all.

What's the single most damaging RBAC anti-pattern?

Binding cluster-admin — the built-in ClusterRole with * verbs on * resources across * API groups — to a service account, CI/CD pipeline, or Helm-installed operator instead of a scoped role. It's the fastest path from "one workload is compromised" to "the entire cluster is compromised," and it's extremely common because it's the path of least resistance: kubectl create clusterrolebinding my-binding --clusterrole=cluster-admin --serviceaccount=default:my-sa "just works" the first time, so teams ship it and move on. The wildcard pattern (resources: ["*"], verbs: ["*"]) written directly into a custom Role has the identical effect at smaller scope. Kubernetes' own hardening guidance is explicit that cluster-admin should be reserved for human break-glass access, not automation — every CI pipeline, controller, and sidecar should get a Role or ClusterRole scoped to exactly the verbs and resources it invokes, nothing more.

Which specific verb-resource pairs are effectively code execution?

create or get on pods/exec, pods/attach, and pods/portforward are not "read" or "debug" permissions — they are equivalent to shell access on whatever node the pod runs on. pods/exec lets a subject run arbitrary commands inside any pod it can target; pods/attach connects to a running container's console; pods/portforward tunnels arbitrary traffic to a pod's internal ports, bypassing network policy entirely. These are also precisely the endpoints implicated in CVE-2018-1002105's escalation path, since the vulnerable proxy logic sat in front of exactly these aggregated-API request types. A Role that grants broad get/list on pods for observability purposes should not silently also carry pods/exec — they're separate subresources in the RBAC model specifically so they can be split, and Kubernetes' own RBAC docs call this out as a distinct resource for exactly this reason.

How do secrets get over-exposed through RBAC?

Two patterns dominate: granting create on secrets to workloads that never need to mint their own credentials, and granting wildcard get/list/watch on secrets at cluster scope rather than scoping it to specific named secrets in a specific namespace. Since Kubernetes stores Secrets as base64-encoded (not encrypted, unless encryption-at-rest is separately configured) API objects, any subject with cluster-wide secrets list access can enumerate every credential, TLS key, and image-pull token across every namespace in one API call — turning a monitoring service account into a full credential dump if its RBAC was copy-pasted from a broader template. RBAC supports naming specific resourceNames inside a Role to restrict get to individual secrets rather than the whole collection; it's underused because it requires per-secret rule authoring, but it's the difference between "this service can read its own database password" and "this service can read everyone's."

What are escalate and bind, and why are they dangerous?

escalate and bind are verbs on the roles/clusterroles resources themselves, added as an explicit override to RBAC's default privilege-escalation prevention rule (the rule itself shipped with RBAC's 1.8 GA; the escalate/bind override verbs landed later, in Kubernetes 1.12). Without them, RBAC's default rule is that you cannot grant a permission you don't already hold yourself — a subject can't create a Role broader than its own. bind lets a subject attach any role to a binding regardless of that restriction, and escalate lets a subject modify its own role to grant itself new permissions directly. Similarly, impersonate on users, groups, or serviceaccounts lets a subject act as any identity it's permitted to impersonate — including more privileged ones. All three verbs should be treated as tantamount to whatever the most-privileged reachable role is, and audited with the same scrutiny as cluster-admin bindings, per upstream Kubernetes RBAC documentation's own escalation-prevention rules.

How do you actually verify what a role grants before shipping it?

kubectl auth can-i --list --as=system:serviceaccount:<namespace>:<name> prints the full effective permission set for any subject, which is the fastest way to catch a binding that grants more than the workload's manifest suggests. Pair it with --as impersonation against specific actions — kubectl auth can-i create pods/exec --as=system:serviceaccount:default:ci-runner -n production — to test a single risky verb-resource pair directly rather than reading YAML and hoping. For automated pod hygiene, set automountServiceAccountToken: false on any workload that never calls the Kubernetes API server; the default service account auto-mounts a token into every pod regardless of whether it's used, and an unused-but-present token is one more credential an attacker who lands a shell can steal and reuse elsewhere in the cluster.

How Safeguard thinks about least privilege

Safeguard doesn't currently ship Kubernetes cluster-RBAC posture scanning, but the least-privilege discipline this post describes — narrow, explicitly scoped grants over broad defaults, plus a durable audit trail of every access change — is the same model Safeguard applies to its own product-level access control. Safeguard's organization/team/project hierarchy uses built-in roles (Owner, Admin, Security Lead, Security Engineer, Developer, Auditor, Viewer) that default to least privilege, supports custom roles composed from a permissions matrix rather than wildcard grants, and records every role assignment in an append-only, cryptographically chained audit log. The underlying principle is identical whether you're scoping a Kubernetes ServiceAccount or a Safeguard team role: grant the specific verbs a subject needs, bind them at the narrowest scope that works, and keep a record of every change so a future audit doesn't have to guess.

Never miss an update

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