DEV Community

devtocash
devtocash

Posted on Originally published at devtocash.com

Kubernetes RBAC Deep Dive

💡 Originally published on devtocash.com — where this guide stays updated. I write hands-on DevOps/SRE deep-dives there weekly.

Introduction

Kubernetes RBAC is the last line of defense between an attacker who compromises a pod and full cluster control. A single misconfigured RoleBinding — granting pods/exec to a compromised service account — can escalate a container escape into complete cluster takeover.

Yet RBAC is also the most misunderstood Kubernetes security primitive. Teams default to cluster-admin bindings, grant wildcard * verbs, and never audit who accessed what. This guide fixes that with production-tested YAML patterns and real attack scenarios.

Role vs ClusterRole: When Namespace Matters

The fundamental RBAC decision: does this permission apply to one namespace or the entire cluster?

Role: Namespace-scoped. Use for application service accounts that only need access to their own namespace.

apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
  namespace: payment-service
  name: payment-deployer
rules:
- apiGroups: ["apps", ""]
  resources: ["deployments", "pods", "configmaps"]
  verbs: ["get", "list", "watch", "create", "update"]
Enter fullscreen mode Exit fullscreen mode

ClusterRole: Cluster-scoped. Use for cluster-wide resources (nodes, namespaces, persistent volumes) or to bind the same permissions across multiple namespaces.

apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
  name: node-reader
rules:
- apiGroups: [""]
  resources: ["nodes", "nodes/metrics", "nodes/proxy"]
  verbs: ["get", "list", "watch"]
Enter fullscreen mode Exit fullscreen mode

Rule of thumb: Start with Role + RoleBinding. Upgrade to ClusterRole only when you have proven the need for cluster-wide access. ClusterRole + RoleBinding (which scopes the ClusterRole to one namespace) is a legitimate pattern — the ClusterRole defines the permissions template, but binding it in payment-service only grants those permissions in that namespace.

RoleBinding and ClusterRoleBinding: The Grant

RoleBinding grants a Role to a subject (user, group, or service account) within a namespace. ClusterRoleBinding grants a ClusterRole cluster-wide.

# Namespaced grant
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
  name: payment-deployer-binding
  namespace: payment-service
subjects:
- kind: ServiceAccount
  name: payment-deployer
  namespace: payment-service
roleRef:
  kind: Role
  name: payment-deployer
  apiGroup: rbac.authorization.k8s.io
Enter fullscreen mode Exit fullscreen mode
# Cluster-wide grant — USE SPARINGLY
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
  name: node-reader-binding
subjects:
- kind: Group
  name: sre-team
  apiGroup: rbac.authorization.k8s.io
roleRef:
  kind: ClusterRole
  name: node-reader
  apiGroup: rbac.authorization.k8s.io
Enter fullscreen mode Exit fullscreen mode

Never bind to system:masters group. This is equivalent to root. If you see a ClusterRoleBinding with subjects[].name: system:masters, remove it immediately.

Service Account Hardening: The Pod Attack Surface

When a pod runs, it uses a service account. The default service account in each namespace exists even if you never created one — and if RBAC is not configured, it may have unintended permissions.

Step 1: Disable automounting the default token.

apiVersion: v1
kind: ServiceAccount
metadata:
  name: default
  namespace: payment-service
automountServiceAccountToken: false
Enter fullscreen mode Exit fullscreen mode

Step 2: Create named service accounts with explicit permissions.

apiVersion: v1
kind: ServiceAccount
metadata:
  name: payment-api
  namespace: payment-service
---
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
  name: payment-api-role
  namespace: payment-service
rules:
- apiGroups: [""]
  resources: ["configmaps"]
  verbs: ["get"]
  resourceNames: ["payment-config"]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
  name: payment-api-binding
  namespace: payment-service
subjects:
- kind: ServiceAccount
  name: payment-api
roleRef:
  kind: Role
  name: payment-api-role
  apiGroup: rbac.authorization.k8s.io
Enter fullscreen mode Exit fullscreen mode

This service account can read exactly one ConfigMap by name. An attacker who compromises the payment-api pod cannot list secrets, cannot create pods, cannot read any other resource. This is the principle of least privilege applied to Kubernetes.

Real Attack Scenario: Privilege Escalation via Misconfigured RBAC

An attacker compromises a pod running with a service account that has pods/exec and pods/create permissions. Here is the escalation path:

# 1. Create a privileged pod on a control plane node
kubectl run evil-pod --image=alpine --restart=Never \
  --overrides='{"spec":{"nodeName":"control-plane-node","containers":[{"name":"evil","image":"alpine","command":["nsenter","--target","1","--mount","--","bash"],"securityContext":{"privileged":true}}],"hostNetwork":true}}'

# 2. Now the attacker has root on the control plane node
# 3. Extract etcd client certificates
# 4. Direct etcd access = full cluster control
Enter fullscreen mode Exit fullscreen mode

This is not theoretical. This exact path is documented in real-world Kubernetes security incidents. The defense:

  • Never grant pods/exec or pods/create to application service accounts
  • Restrict nodeName in Pod Security Admission
  • Enable audit logging to detect this attack in progress

Audit Logging: Who Did What When

Audit logging answers "who accessed what" — essential for both security investigations and RBAC hygiene. Enable it on the API server:

# audit-policy.yaml
apiVersion: audit.k8s.io/v1
kind: Policy
rules:
# Log all RBAC changes at RequestResponse level
- level: RequestResponse
  verbs: ["create", "update", "patch", "delete"]
  resources:
  - group: "rbac.authorization.k8s.io"
    resources: ["*"]

# Log secret access at Metadata level
- level: Metadata
  verbs: ["get", "list", "watch"]
  resources:
  - group: ""
    resources: ["secrets"]

# Log pod exec at RequestResponse level
- level: RequestResponse
  verbs: ["create"]
  resources:
  - group: ""
    resources: ["pods/exec"]

# Don't log read-only system traffic
- level: None
  userGroups: ["system:nodes"]
  verbs: ["get", "list", "watch"]
Enter fullscreen mode Exit fullscreen mode

Configure the API server to use this policy:

# kube-apiserver manifest snippet
spec:
  containers:
  - command:
    - kube-apiserver
    - --audit-policy-file=/etc/kubernetes/audit-policy.yaml
    - --audit-log-path=/var/log/kubernetes/audit.log
    - --audit-log-maxage=30
    - --audit-log-maxbackup=10
    - --audit-log-maxsize=100
Enter fullscreen mode Exit fullscreen mode

Forward audit logs to your SIEM. When someone accesses a secret or modifies an RBAC rule, you want an alert — not a log file you check after the breach.

RBAC for Platform Teams: Multi-Tenant Patterns

Platform teams supporting multiple application teams need namespace-level isolation with shared platform services. The pattern: one namespace per team, ClusterRole for shared access:

# Platform ClusterRole — reusable across namespaces
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
  name: team-developer
rules:
- apiGroups: ["apps", ""]
  resources: ["deployments", "pods", "services", "configmaps"]
  verbs: ["get", "list", "watch", "create", "update", "patch"]
- apiGroups: ["networking.k8s.io"]
  resources: ["networkpolicies"]
  verbs: ["get", "list"]

---
# Bind per team namespace
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
  name: team-a-developers
  namespace: team-a
subjects:
- kind: Group
  name: team-a-devs
roleRef:
  kind: ClusterRole
  name: team-developer
  apiGroup: rbac.authorization.k8s.io
Enter fullscreen mode Exit fullscreen mode

Each team gets the same ClusterRole template scoped to their namespace. No team can access another team's resources. Platform teams maintain one ClusterRole instead of N identical Roles.

Validating RBAC Configurations

Before applying RBAC, validate:

# Can user alice create deployments in team-a namespace?
kubectl auth can-i create deployments --as alice --namespace team-a

# Can service account payment-api list secrets?
kubectl auth can-i list secrets --as system:serviceaccount:payment-service:payment-api -n payment-service

# What can this service account actually do?
kubectl auth can-i --list --as system:serviceaccount:payment-service:payment-api
Enter fullscreen mode Exit fullscreen mode

Integrate this into CI: after every RBAC change, run kubectl auth can-i checks in a pipeline to validate that permissions match intent.

RBAC and Kubernetes Security: The Big Picture

RBAC is one layer of the defense-in-depth strategy covered in our Kubernetes security best practices guide. Combine it with:

  • Pod Security Admission — enforce restricted profile so pods cannot run as root
  • NetworkPolicy — deny-all by default, whitelist required traffic
  • OPA/Gatekeeper — validate RBAC manifests in CI, block cluster-admin bindings

For GPU-powered workloads, the same RBAC patterns protect inference clusters. See our Kubernetes LLM inference guide for securing multi-tenant GPU clusters with RBAC and network isolation.

RBAC is not a checkbox. It is an ongoing discipline. Audit your bindings monthly. Rotate service accounts. Grant exactly the permissions needed — and nothing more.

Related Reading


📌 Read the latest version of this guide — plus the full library of DevOps, SRE, Kubernetes, observability & cloud-cost guides — on devtocash.com.

Top comments (0)