Skip to content

ClusterRoleBinding vs RoleBinding Semantics

Vì Sao Quantum Trọng

Rất nhiều security bugs trong Kubernetes deployments bắt nguồn từ MISUNDERSTANDING semantics giữa RoleBinding và ClusterRoleBinding:

  • Namespace override gotcha: ClusterRole dùng ClusterRoleBinding có scope CLUSTER-WIDE. Nhưng ClusterRole bound via RoleBinding chỉ grant permissions trong namespace binding đó. Developers often assume "ClusterRole = cluster-wide" tanpa realize binding scoping override semantics này.
  • Immutability constraint: Nếu mistake binding role đến subject sai, không thể edit roleRef. Phải delete and recreate. Nếu không biết constraint này, akan waste hours debugging.
  • Subject matching semantics: RoleBinding can reference ClusterRole, nhưng RoleBinding itself scoped đến namespace. Hasilnya là asymmetric semantics mà confusing.

Memahami scoping, immutability, và matching logic fundamental để write secure, debuggable RBAC.

RoleBinding Structure

RoleBinding (Namespaced)

yaml
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
  namespace: default
  name: pod-reader-binding
roleRef:
  apiGroup: rbac.authorization.k8s.io
  kind: Role  # or ClusterRole
  name: pod-reader
subjects:
- kind: User
  name: alice@example.com
- kind: ServiceAccount
  namespace: default
  name: my-app
- kind: Group
  name: developers

Structure:

FieldScopingOptions
namespaceBinding scoped to this namespaceRequired
roleRef.kindRole type being boundRole hoặc ClusterRole
roleRef.nameRole nameUnique per kind
subjectsWho gets these permissionsUsers, Groups, SAs

ClusterRoleBinding (Cluster-Scoped)

yaml
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
  name: monitoring-role-binding
roleRef:
  apiGroup: rbac.authorization.k8s.io
  kind: ClusterRole  # Must be ClusterRole
  name: monitoring-reader
subjects:
- kind: User
  name: monitoring-user@example.com
- kind: ServiceAccount
  namespace: monitoring
  name: prometheus

Key difference:

  • No namespace field (cluster-scoped)
  • Can only reference ClusterRole (not Role)

Scope Semantics: The Critical Distinction

Role + RoleBinding = Namespace-Scoped Access

yaml
# Role (namespaced)
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
  namespace: default
  name: pod-reader
rules:
- apiGroups: [""]
  resources: ["pods"]
  verbs: ["get", "list"]
---
# RoleBinding (grants access ONLY in default namespace)
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
  namespace: default
  name: pod-reader-binding
roleRef:
  kind: Role
  name: pod-reader
subjects:
- kind: User
  name: alice

Result: Alice có thể get, list pods chỉ di default namespace. Di kube-system, Alice không có thể truy cập.

ClusterRole + ClusterRoleBinding = Cluster-Wide Access

yaml
# ClusterRole (cluster-scoped)
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
  name: pod-reader-global
rules:
- apiGroups: [""]
  resources: ["pods"]
  verbs: ["get", "list"]
---
# ClusterRoleBinding (grants access EVERYWHERE)
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
  name: pod-reader-global-binding
roleRef:
  kind: ClusterRole
  name: pod-reader-global
subjects:
- kind: User
  name: alice

Result: Alice có thể get, list pods di tất cả namespaces.

ClusterRole + RoleBinding = Namespace-Scoped Access (Asymmetric!)

yaml
# ClusterRole (can be reused across namespaces)
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
  name: pod-reader-global
rules:
- apiGroups: [""]
  resources: ["pods"]
  verbs: ["get", "list"]
---
# RoleBinding in namespace production (restricts to this namespace)
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
  namespace: production
  name: prod-pod-reader-binding
roleRef:
  kind: ClusterRole  # Referencing cluster-scope role
  name: pod-reader-global  # But binding restricts scope!
subjects:
- kind: User
  name: alice

Result: Alice có thể get, list pods chỉ di production namespace, không di namespace lain.

This is THE gotcha: ClusterRole scope is OVERRIDDEN bởi RoleBinding's namespace scope. Binding type menentukan effective scope, không phải role type.

Why This Design?

Kenapa Kubernetes allow ClusterRole + RoleBinding combination?

Reusability: Sama role có thể di-bind di multiple namespaces via RoleBinding, hoặc cluster-wide via ClusterRoleBinding:

yaml
# Single ClusterRole
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
  name: pod-reader
rules:
- apiGroups: [""]
  resources: ["pods"]
  verbs: ["get", "list"]
---
# Bind to namespace A
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
  namespace: team-a
  name: pod-reader
roleRef:
  kind: ClusterRole
  name: pod-reader
subjects:
- kind: ServiceAccount
  namespace: team-a
  name: app
---
# Bind to namespace B (same role, different binding)
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
  namespace: team-b
  name: pod-reader
roleRef:
  kind: ClusterRole
  name: pod-reader
subjects:
- kind: ServiceAccount
  namespace: team-b
  name: app

One role, multiple namespace-scoped bindings = reusability + namespace isolation.

Subject Matching Logic

Subject Identity Resolution

Khi authorization evaluates request, API server match subjects trong binding với request identity:

Request context:

{
  user: "alice@example.com",
  groups: ["developers", "system:authenticated"],
  serviceAccount: "system:serviceaccount:default:my-app"
}

RoleBinding subjects:

yaml
subjects:
- kind: User
  name: alice@example.com        # Matches request.user
- kind: Group
  name: developers               # Matches request.groups
- kind: ServiceAccount
  namespace: default
  name: my-app                   # Expands to system:serviceaccount:default:my-app

Matching algorithm:

For each subject in binding:
  if subject.kind == User:
    if subject.name == request.user:
      MATCH
  else if subject.kind == Group:
    if subject.name in request.groups:
      MATCH
  else if subject.kind == ServiceAccount:
    sa_username = "system:serviceaccount:" + subject.namespace + ":" + subject.name
    if sa_username == request.user:
      MATCH

ServiceAccount Subject Expansion

ServiceAccount subjects automatically expand đến internal username:

yaml
subjects:
- kind: ServiceAccount
  namespace: monitoring
  name: prometheus
  # Internally becomes:
  # system:serviceaccount:monitoring:prometheus

Nếu request từ SA token, kubelet set request.user đến expanded SA username, và matching work.

Group Subject Semantics

Group subjects matched against request.groups array:

yaml
subjects:
- kind: Group
  name: developers

Di-match nếu "developers" trong request.groups. Request.groups populated bởi authentication layer (OIDC, LDAP):

OIDC example:

OIDC token claims:
{
  "sub": "alice@example.com",
  "groups": ["developers", "platform-team"]
}

Authenticator sets request.groups = ["developers", "platform-team", "system:authenticated"]

RoleBinding với subject Group: developers akan match.

Binding Immutability

Critical Constraint: roleRef Cannot Change

Sekali RoleBinding/ClusterRoleBinding created, roleRef field IMMUTABLE:

bash
# This FAILS
kubectl patch rolebinding pod-reader-binding \
  --type='json' -p='[{"op":"replace","path":"/roleRef/name","value":"deployment-reader"}]'
# Error: spec.roleRef is immutable

Reason: Prevent privilege escalation via binding manipulation. Kalau roleRef có thể diubah tanpa creating new binding, auditing becomes impossible.

Recovery:

bash
# Must delete and recreate
kubectl delete rolebinding pod-reader-binding
kubectl create rolebinding pod-reader-binding \
  --clusterrole=deployment-reader \
  --user=alice

Why Immutability Matters

Ngoài audit, immutability ensure semantic stability:

Original binding binds User A đến Role X (permissions: [get pods, list pods])

If roleRef was mutable:
  t=0: Binding grants User A [get pods, list pods]
  t=1: Admin changes roleRef to Role Y (permissions: [*])
  t=2: Audit log doesn't clearly show Who changed it and When
  t=3: User A now has cluster-admin, source of escalation unclear
  
With immutability:
  t=1: Change is explicit new RoleBinding creation
  t=2: Audit shows creation event with full context
  t=3: Clear audit trail linking escalation to creation

Subject Reference Semantics

User Subject

yaml
subjects:
- kind: User
  name: alice@example.com

Matched against: request.user (from authentication)

Notes:

  • Name là opaque string, no namespace
  • Case-sensitive
  • OIDC/LDAP authenticator populates request.user

Group Subject

yaml
subjects:
- kind: Group
  name: developers

Matched against: request.groups array

Notes:

  • Name là opaque string
  • Case-sensitive
  • system:* groups reserved để system use

ServiceAccount Subject

yaml
subjects:
- kind: ServiceAccount
  namespace: default
  name: my-app

Internally expands to username: system:serviceaccount:default:my-app

Important: ServiceAccount subject requires namespace field. It's not optional.

yaml
# WRONG
subjects:
- kind: ServiceAccount
  name: my-app  # Missing namespace!
  
# Cause validation error or unexpected no-match behavior

Authorization Decision with Multiple Bindings

OR Semantics Across Bindings

Nếu user matched multiple bindings, permissions là union:

yaml
# Binding 1
kind: RoleBinding
metadata:
  namespace: default
roleRef:
  kind: Role
  name: pod-reader  # Rules: [get pods, list pods]
subjects:
- kind: User
  name: alice
---
# Binding 2
kind: RoleBinding
metadata:
  namespace: default
roleRef:
  kind: Role
  name: deployment-reader  # Rules: [get deployments, list deployments]
subjects:
- kind: User
  name: alice

Result: Alice trong default namespace có thể:

  • get pods, list pods (từ Binding 1)
  • get deployments, list deployments (từ Binding 2)

Cross-Namespace Isolation

yaml
# Binding in namespace A
kind: RoleBinding
metadata:
  namespace: ns-a
roleRef:
  kind: Role
  name: pod-reader
subjects:
- kind: User
  name: alice
  
# Binding in namespace B (same user, different namespace)
kind: RoleBinding
metadata:
  namespace: ns-b
roleRef:
  kind: Role
  name: pod-writer
subjects:
- kind: User
  name: alice

Result:

  • Alice trong ns-a: có thể get pods, list pods
  • Alice trong ns-b: có thể create pods, update pods
  • Alice di namespace lain: không punya access

Namespace boundary là hard boundary — RoleBinding di ns-a không affect access di ns-b.

Practical Patterns

Pattern 1: Namespace Admin

Beri user admin access chỉ trong một namespace:

yaml
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
  namespace: team-a
  name: team-a-admin
roleRef:
  kind: ClusterRole
  name: admin
subjects:
- kind: User
  name: alice

Alice là admin chỉ trong team-a, không trong cluster.

Pattern 2: Cluster Admin

Beri user admin access cluster-wide:

yaml
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
  name: alice-cluster-admin
roleRef:
  kind: ClusterRole
  name: cluster-admin
subjects:
- kind: User
  name: alice

Alice là cluster admin everywhere.

Pattern 3: Read-Only Access Across Namespaces

Single ClusterRole, multiple RoleBinding per namespace:

yaml
# ClusterRole (define once)
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
  name: pod-reader
rules:
- apiGroups: [""]
  resources: ["pods"]
  verbs: ["get", "list"]
---
# Bind trong each namespace
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
  namespace: team-a
  name: pod-reader
roleRef:
  kind: ClusterRole
  name: pod-reader
subjects:
- kind: ServiceAccount
  namespace: team-a
  name: monitoring
---
# Same role, different namespace
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
  namespace: team-b
  name: pod-reader
roleRef:
  kind: ClusterRole
  name: pod-reader
subjects:
- kind: ServiceAccount
  namespace: team-b
  name: monitoring

Monitoring SA có thể read pods trong namespace-nya sendiri, không across namespaces.

Troubleshooting Binding Issues

Issue 1: Expected Permission Not Granted

bash
# Check if binding exists
kubectl get rolebinding -n team-a -o wide

# Check if subject matches
# (requires manual inspection of role+binding)
kubectl get rolebinding pod-reader-binding -n team-a -o yaml

# Verify role rules
kubectl get role pod-reader -n team-a -o yaml

# Test permission
kubectl auth can-i get pods --as=alice -n team-a

Issue 2: Permission Granted Unexpectedly

bash
# Find all bindings that match user
kubectl get rolebindings,clusterrolebindings -A -o wide | grep alice

# Check each binding's role
for binding in $(kubectl get rolebindings,clusterrolebindings -A -o name | grep alice); do
  kubectl get $binding -o yaml
done

# Check system groups
# alice might be member of system:authenticated or custom group

Summary

ClusterRoleBinding vs RoleBinding decision là scoping decision:

RequirementUse
Cluster-wide accessClusterRoleBinding + ClusterRole
Namespace-only accessRoleBinding + Role
Reusable role across namespacesClusterRole + multiple RoleBindings per namespace
Tight security boundaryNamespace-scoped RoleBinding (even if using ClusterRole)

Key points:

  • Binding type determines scope, not role type
  • roleRef immutable — delete and recreate to change
  • Subject matching includes groups and SA expansion
  • Namespace is hard boundary — never crossed by RoleBinding

Tham Khảo