RBAC — Role-Based Access Control

Section 06 › 01 Last updated: 2025 ~35 min read

On this page

  1. RBAC Data Model
  2. API Objects: Role, ClusterRole, Binding
  3. Verbs, Resources, Subresources
  4. Subjects: User, Group, ServiceAccount
  5. ClusterRole Aggregation
  6. Default ClusterRoles
  7. Privilege Escalation Prevention
  8. Impersonation
  9. Node Authorization
  10. RBAC Auditing Tools
  11. Common RBAC Patterns
  12. Multi-Tenant RBAC Design
  13. Metrics & Alerts
  14. Best Practices
Coverage checklist
  • RBAC data model: allow-only, no deny rules
  • Role vs ClusterRole scope
  • RoleBinding vs ClusterRoleBinding scope
  • Full verb list with semantics
  • Resources, subresources, resourceNames
  • Wildcard dangers
  • Subject types: User, Group, ServiceAccount
  • Group membership via OIDC claims
  • system:authenticated / system:unauthenticated groups
  • ClusterRole aggregation with labels
  • Default ClusterRoles: cluster-admin, admin, edit, view
  • system:masters group escalation danger
  • bind / escalate verbs — privilege escalation prevention
  • Impersonation verb + use cases
  • Node authorization + NodeRestriction
  • kubectl auth can-i for verification
  • rbac-lookup, rbac-police, rakkess tools
  • Audit log RBAC event queries
  • Patterns: read-only operator, namespace admin, CI/CD, monitoring
  • Multi-tenant namespace isolation RBAC design
  • Cross-namespace access limitations
  • Least privilege service account per workload
  • RBAC for CRDs and custom resources
  • 5 metrics, 4 alerts, 5 runbooks
  • 8 best practices

RBAC Data Model

Kubernetes RBAC is an allow-only system — there are no deny rules. A request is allowed if at least one Role/ClusterRole grants the requested verb on the requested resource. If no binding grants the permission, the request is denied (implicit deny).

Subject ──► RoleBinding / ClusterRoleBinding ──► Role / ClusterRole
  │                       │                              │
User                  namespaced             rules: apiGroups + resources + verbs
Group                 (RoleBinding)
ServiceAccount        or cluster-wide
                      (ClusterRoleBinding)

Example:
  jane ──► RoleBinding (ns: production) ──► Role "pod-reader"
              grants: get, list, watch on pods in production namespace only

  ci-bot ──► ClusterRoleBinding ──► ClusterRole "deployment-manager"
              grants: get, list, patch on deployments in ALL namespaces

Scope Rules

Binding TypeRole TypeEffective ScopeWhen to Use
RoleBindingRoleSingle namespace (binding's namespace)Namespace-scoped access for a team or workload
RoleBindingClusterRoleSingle namespace (binding's namespace)Reuse a common ClusterRole across many namespaces without granting cluster-wide access
ClusterRoleBindingClusterRoleAll namespaces + cluster-scoped resourcesCluster-wide operators, platform admins
ClusterRoleBindingRoleNot allowed — Role is namespace-scopedN/A
Key insight: ClusterRole + RoleBinding = namespace-scoped access. You can bind a ClusterRole using a RoleBinding to grant its permissions only within a specific namespace. This is the standard pattern for defining common roles once (ClusterRole) and granting them per-namespace (RoleBinding). It is not cluster-wide access.

API Objects: Role, ClusterRole, Binding

Role — namespace-scoped permissions

apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
  namespace: production          # Role lives in this namespace
  name: pod-reader
rules:
- apiGroups: [""]                # "" = core API group (pods, services, configmaps, secrets...)
  resources: ["pods", "pods/log"]
  verbs: ["get", "list", "watch"]
- apiGroups: [""]
  resources: ["pods/exec"]
  verbs: ["create"]              # exec requires "create" on pods/exec subresource

ClusterRole — cluster-scoped or reusable namespace permissions

apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
  name: node-reader
rules:
- apiGroups: [""]
  resources: ["nodes"]           # nodes are cluster-scoped — only ClusterRole can grant
  verbs: ["get", "list", "watch"]
- apiGroups: ["metrics.k8s.io"]
  resources: ["nodes", "pods"]
  verbs: ["get", "list"]
- apiGroups: ["storage.k8s.io"]
  resources: ["storageclasses"]  # cluster-scoped
  verbs: ["get", "list", "watch"]

RoleBinding — grants Role/ClusterRole in one namespace

apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
  name: read-pods
  namespace: production
subjects:
- kind: User
  name: jane                       # must match the name in the X.509 cert CN or OIDC sub claim
  apiGroup: rbac.authorization.k8s.io
- kind: ServiceAccount
  name: pod-monitor
  namespace: monitoring            # SA can be in a different namespace than the binding
roleRef:
  kind: Role                       # or ClusterRole
  name: pod-reader
  apiGroup: rbac.authorization.k8s.io
# roleRef is immutable after creation — delete and recreate to change

ClusterRoleBinding — grants ClusterRole cluster-wide

apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
  name: cluster-monitoring-read
subjects:
- kind: Group
  name: monitoring-team            # OIDC group claim / static group in kubeconfig
  apiGroup: rbac.authorization.k8s.io
roleRef:
  kind: ClusterRole
  name: node-reader
  apiGroup: rbac.authorization.k8s.io
roleRef is immutable. Once a binding is created, the roleRef field cannot be changed. To change which Role/ClusterRole a binding points to, delete the binding and create a new one. This is by design — preventing silent privilege changes through binding mutation.

Verbs, Resources, Subresources

Complete Verb Reference

VerbHTTP MethodDescriptionNotes
getGET /resource/nameRead a single object by nameAlso required for kubectl describe
listGET /resourceList all objects (optionally filtered)Returns full object bodies — more permissive than get
watchGET /resource?watch=trueStream change eventsRequired for controllers/informers; implies list access effectively
createPOST /resourceCreate a new objectAlso required to use pods/exec, pods/portforward
updatePUT /resource/nameReplace full object (requires resourceVersion)Full object replacement
patchPATCH /resource/namePartial update (JSON Patch, Merge Patch, SSA)Commonly used by controllers
deleteDELETE /resource/nameDelete a single object
deletecollectionDELETE /resourceDelete all matching objectsDangerous — grant separately from delete
impersonateX-Impersonate headersAct as another user/group/SAOnly for admins and test tooling; see impersonation section
bindCreate RoleBindings/ClusterRoleBindingsPrivilege escalation guard — see below
escalateCreate/update Roles/ClusterRoles with permissions exceeding your ownPrivilege escalation guard — see below
approveApprove CertificateSigningRequestsSpecial verb on CSR objects
signSign CertificateSigningRequestsSpecial verb on CSR objects
useUse a PodSecurityPolicy (deprecated) or RuntimeClassPSP removed in 1.25; RuntimeClass still uses this verb
list grants more than get. list returns all object bodies including fields like data in ConfigMaps and data in Secrets. A subject with list on secrets can read all secret values in the namespace even without get on individual secrets. Always consider list as equivalent to read access on all objects of that type.

Resources and Subresources

Subresources are accessed as resource/subresource and require separate RBAC rules:

SubresourceRequired VerbWhat It Allows
pods/execcreatekubectl exec into a running container
pods/loggetkubectl logs
pods/portforwardcreatekubectl port-forward
pods/attachcreatekubectl attach (stdin to running process)
pods/evictioncreateEviction API (used by kubectl drain, Cluster Autoscaler)
pods/statusget, update, patchRead/write pod status subresource (used by kubelet)
deployments/scaleget, update, patchScale a Deployment via the scale subresource
nodes/proxygetProxy requests through API server to kubelet HTTP endpoint
serviceaccounts/tokencreateRequest a bound token for a ServiceAccount
certificatesigningrequests/approvalupdateApprove a CSR
pods/exec is effectively root on the node for trusted workloads. A subject with create on pods/exec can run arbitrary commands in any pod in the namespace. If any pod runs as root or has host mounts, this is a node escape vector. Restrict exec access tightly — never grant it to service accounts or CI systems that don't explicitly require it.

ResourceNames — restricting to specific objects

# Allow getting only a specific ConfigMap by name
rules:
- apiGroups: [""]
  resources: ["configmaps"]
  resourceNames: ["app-config", "feature-flags"]  # only these named objects
  verbs: ["get", "watch"]
# NOTE: list and watch on a resource name does not work as expected — list ignores resourceNames
# Use resourceNames only for get/update/patch/delete on specific objects

Wildcards and Dangers

# This grants full access to everything — equivalent to cluster-admin
rules:
- apiGroups: ["*"]
  resources: ["*"]
  verbs: ["*"]

# Avoid wildcard verbs — even in narrow resource scope
rules:
- apiGroups: ["apps"]
  resources: ["deployments"]
  verbs: ["*"]    # includes delete, deletecollection — probably not intended

Subjects: User, Group, ServiceAccount

Subject Types

KindIdentifierSourceNotes
UserString nameX.509 cert CN; OIDC sub or email claimKubernetes has no User object — users exist only in external systems
GroupString nameX.509 cert O field; OIDC groups claimMultiple groups per user; Kubernetes creates some system groups
ServiceAccountname + namespaceCreated as K8s object; token auto-mounted into podsMust specify namespace in subject spec

System Groups

GroupWho BelongsSecurity Notes
system:authenticatedAll authenticated requestsDefault ClusterRoleBindings grant some read access — audit regularly
system:unauthenticatedAnonymous requests (if anonymous auth enabled)Should have zero permissions in production
system:mastersX.509 certs with O=system:mastersBypasses RBAC entirely — equivalent to cluster-admin, cannot be restricted by RBAC. Only for break-glass admin certs.
system:nodesKubelets (X.509 CN=system:node:nodename)Node authorization mode restricts permissions to per-node resources
system:serviceaccountsAll ServiceAccounts in all namespacesAvoid binding roles to this group — it grants access to every SA
system:serviceaccounts:<ns>All ServiceAccounts in namespace <ns>Grants access to all current and future SAs in the namespace
system:masters cannot be revoked via RBAC. A certificate signed with O=system:masters bypasses the RBAC authorization layer entirely. It is checked before RBAC runs. These certificates should be used only for break-glass emergency access and kept offline. Revoking one requires rotating the CA or using a CRL.

OIDC Groups Integration

When using OIDC authentication, the API server extracts group membership from the JWT's groups claim (configured via --oidc-groups-claim). This enables binding ClusterRoles to OIDC groups:

# OIDC token has: { "groups": ["platform-engineers", "k8s-admins"] }
# API server flag: --oidc-groups-claim=groups --oidc-groups-prefix=oidc:

apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
  name: platform-engineers-admin
subjects:
- kind: Group
  name: oidc:platform-engineers   # prefixed group name
  apiGroup: rbac.authorization.k8s.io
roleRef:
  kind: ClusterRole
  name: admin                      # built-in admin ClusterRole
  apiGroup: rbac.authorization.k8s.io

ClusterRole Aggregation

ClusterRole aggregation lets you compose a ClusterRole from multiple other ClusterRoles automatically, using label selectors. The aggregated role is continuously updated — any ClusterRole with a matching label is automatically merged into it.

ClusterRole "monitoring-full"
  aggregationRule:
    clusterRoleSelectors:
    - matchLabels:
        rbac.monitoring.io/aggregate-to-monitoring: "true"

  ← automatically includes rules from:

ClusterRole "prometheus-scrape-pods"          label: rbac.monitoring.io/aggregate-to-monitoring=true
  rules: get/list/watch pods, endpoints

ClusterRole "grafana-read-dashboards"         label: rbac.monitoring.io/aggregate-to-monitoring=true
  rules: get/list configmaps in monitoring ns

(any new ClusterRole with the label is auto-merged in — no edit needed to monitoring-full)
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
  name: monitoring-aggregate
aggregationRule:
  clusterRoleSelectors:
  - matchLabels:
      rbac.authorization.k8s.io/aggregate-to-monitoring: "true"
rules: []  # populated automatically by controller; never set manually on aggregated roles

---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
  name: monitoring-pod-reader
  labels:
    rbac.authorization.k8s.io/aggregate-to-monitoring: "true"  # triggers aggregation
rules:
- apiGroups: [""]
  resources: ["pods", "endpoints", "services"]
  verbs: ["get", "list", "watch"]

The built-in admin, edit, and view ClusterRoles use aggregation — any ClusterRole with rbac.authorization.k8s.io/aggregate-to-admin/edit/view: "true" is automatically merged. This is how CRD operators extend the built-in roles without modifying them.

Default ClusterRoles

ClusterRoleAccess LevelTypical BindingWarning
cluster-adminFull access to everything in all namespacesBreak-glass only; never CI/CD or workloadsUse sparingly
adminFull namespace access (no ResourceQuota/LimitRange write)Namespace owners via RoleBinding per namespace
editRead/write most namespaced objects; no RBAC readDevelopers via RoleBinding per namespace
viewRead-only on most namespaced objects (no Secrets)Auditors, monitoring SAs
system:nodeKubelet permissions (node authorization handles this)Auto-bound via node authorization
system:auth-delegatorDelegate authentication decisionsExtension API servers
system:heapsterLegacy metrics read; deprecatedDeprecated — use metrics-server RBAC
The built-in view ClusterRole does NOT include secrets. view intentionally excludes secrets and serviceaccounts/token. If you need read access to secrets, create a separate Role with explicit secret access. Do not extend the built-in view role for this — create a purpose-specific role.

Default ClusterRoleBindings to Audit

Kubernetes ships with several ClusterRoleBindings pre-installed. Review these in every cluster:

# List all pre-installed ClusterRoleBindings
kubectl get clusterrolebindings \
  -o custom-columns='NAME:.metadata.name,ROLE:.roleRef.name,SUBJECTS:.subjects' \
  | grep -v "^system:"

# Find all ClusterRoleBindings granting cluster-admin
kubectl get clusterrolebindings \
  -o jsonpath='{range .items[?(@.roleRef.name=="cluster-admin")]}{.metadata.name}{"\t"}{.subjects}{"\n"}{end}'

Privilege Escalation Prevention

RBAC has two special verbs that prevent privilege escalation through role creation and binding:

The escalate verb

Without explicit escalate permission, a user cannot create or update a Role/ClusterRole with permissions they don't already have. This prevents a user with limited access from writing a new ClusterRole granting themselves cluster-admin and then binding it to themselves.

# Allow a role manager to create/update roles, including those with
# permissions beyond their own — requires explicit escalate grant
rules:
- apiGroups: ["rbac.authorization.k8s.io"]
  resources: ["roles", "clusterroles"]
  verbs: ["create", "update", "patch",
          "escalate"]   # without this, cannot create roles with permissions
                        # exceeding the role manager's own permissions

The bind verb

Without explicit bind permission, a user cannot create a RoleBinding/ClusterRoleBinding that references a Role/ClusterRole whose permissions exceed their own. This prevents a user from binding cluster-admin to themselves or others.

# Allow creating RoleBindings only for roles the user already possesses
# (standard behavior — no escalate/bind needed for within-permission bindings)
rules:
- apiGroups: ["rbac.authorization.k8s.io"]
  resources: ["rolebindings"]
  verbs: ["create", "update", "patch",
          "bind"]   # required to bind roles beyond your own permissions
                    # e.g., a "RBAC admin" role that can bind any role in a namespace
Granting escalate or bind is equivalent to granting those permissions themselves. A user with escalate on ClusterRoles can write a ClusterRole with secrets/*: [get,list] even if they can't read secrets themselves. A user with bind on ClusterRoleBindings + access to cluster-admin ClusterRole can bind themselves to cluster-admin. Only platform admins should hold escalate or bind verbs.

Common Privilege Escalation Paths to Monitor

Escalation PathRequired PermissionMitigation
Create/update ClusterRole with excessive permissionsescalate on clusterrolesOnly platform-admins; audit log all clusterrole writes
Bind cluster-admin to selfbind on clusterrolebindingsAlert on cluster-admin bindings created by non-system principals
Create SA + bind cluster-admin to it + exec into pod using SAcreate SA + create pod + bindRestrict bind; alert on new ClusterRoleBindings to cluster-admin
Modify existing ClusterRoleBinding subject listpatch on clusterrolebindingsAudit log all binding mutations; treat binding writes as high-severity events
Impersonate a higher-privileged userimpersonate on users/groups/serviceaccountsNo workload or user should hold impersonate; only specific test tooling
exec into privileged pod → escape to hostcreate on pods/execRestrict exec; PSA restricted prevents privileged pods
Read secret containing kubeconfig or cloud credentialsget on secretsRBAC least-privilege on secrets; external secret store; encryption at rest

Impersonation

Impersonation lets a user act as a different user, group, or service account by sending Impersonate-User, Impersonate-Group, or Impersonate-Extra-* headers. The API server substitutes the impersonated identity for authorization checks.

# Grant impersonation for a specific user (e.g., for a CI/CD proxy)
rules:
- apiGroups: [""]
  resources: ["users"]
  verbs: ["impersonate"]
  resourceNames: ["ci-bot@company.com"]  # restrict to specific identity

# Grant impersonation for a group (e.g., for kube-oidc-proxy)
rules:
- apiGroups: [""]
  resources: ["groups"]
  verbs: ["impersonate"]
# kubectl uses impersonation for --as flag
kubectl get pods --as=jane --as-group=developers -n production

# Test what a user can do (uses impersonation internally)
kubectl auth can-i list pods --as=jane -n production
kubectl auth can-i create deployments --as=system:serviceaccount:default:app-sa

Legitimate use cases for impersonation: kube-oidc-proxy (OIDC token → impersonation), debugging authorization issues, CI/CD systems acting on behalf of a specific identity, admission webhook testing.

Node Authorization

The Node authorization mode is a special-purpose authorizer that grants kubelets read/write access only to resources associated with pods scheduled on that specific node. Without it, any kubelet could read all secrets in the cluster.

Node Authorization Mode Logic:

kubelet on node-1 requests GET /api/v1/secrets/db-password (ns: production)
    ↓
Node authorizer checks: is there a Pod scheduled on node-1 that references this secret?
    → YES: ALLOW
    → NO:  DENY (even though kubelet has system:nodes group)

This prevents a compromised node from reading secrets for pods on other nodes.

The NodeRestriction admission plugin complements Node authorization by preventing kubelets from modifying Node or Pod objects belonging to other nodes, and from setting labels with the node-restriction.kubernetes.io/ prefix (which are reserved for admission-controlled trust).

# Verify Node authorization mode is enabled
kubectl get pod kube-apiserver-controlplane -n kube-system \
  -o jsonpath='{.spec.containers[0].command}' | tr ',' '\n' \
  | grep authorization-mode
# Should output: --authorization-mode=Node,RBAC

RBAC Auditing Tools

kubectl auth can-i

# Check your own permissions
kubectl auth can-i list secrets -n production
kubectl auth can-i create deployments --namespace default

# Check another user's permissions (requires impersonate permission)
kubectl auth can-i list pods --as=jane -n production
kubectl auth can-i get secrets --as=system:serviceaccount:default:app-sa

# List all allowed actions for a user in a namespace
kubectl auth can-i --list -n production --as=jane

# List all allowed actions for a service account
kubectl auth can-i --list \
  --as=system:serviceaccount:production:app-sa \
  -n production

rbac-lookup — find roles for a subject

# Install: https://github.com/FairwindsOps/rbac-lookup
rbac-lookup jane                  # find all roles bound to user jane
rbac-lookup app-sa -k sa          # find all roles bound to SA app-sa
rbac-lookup -o wide               # show full details including namespace

rakkess — access matrix

# Install: https://github.com/corneliusweig/rakkess
# Show access matrix for all resources
rakkess --sa app-sa -n production

# Output example:
# NAME                          GET  LIST  CREATE  UPDATE  DELETE
# configmaps                    ✔    ✔     ✗       ✗       ✗
# deployments.apps              ✔    ✔     ✔       ✔       ✗
# secrets                       ✗    ✗     ✗       ✗       ✗

rbac-police — policy analysis

# Install: https://github.com/PaloAltoNetworks/rbac-police
# Scan for risky RBAC configurations
rbac-police eval lib/prod.rego

# Reports on:
# - SAs that can exec into pods
# - SAs that can read secrets
# - Subjects bound to cluster-admin
# - Wildcard verb/resource grants

Audit Log Queries for RBAC Events

# Find all RBAC binding creations in audit log (jq + audit log JSON)
cat audit.log | jq 'select(.objectRef.resource == "clusterrolebindings" and .verb == "create")'

# Find secret reads by unexpected principals
cat audit.log | jq 'select(
  .objectRef.resource == "secrets" and
  .verb == "get" and
  .user.username != "system:serviceaccount:kube-system:*"
)'

# Find exec operations
cat audit.log | jq 'select(.objectRef.subresource == "exec")'

Common RBAC Patterns

Pattern 1: Operator / Controller Service Account

# Minimal permissions for a Deployment-managing operator
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
  name: my-operator
rules:
- apiGroups: ["apps"]
  resources: ["deployments", "replicasets"]
  verbs: ["get", "list", "watch", "create", "update", "patch", "delete"]
- apiGroups: [""]
  resources: ["configmaps", "services", "serviceaccounts"]
  verbs: ["get", "list", "watch", "create", "update", "patch"]
- apiGroups: [""]
  resources: ["events"]
  verbs: ["create", "patch"]
- apiGroups: ["my.operator.io"]
  resources: ["myresources", "myresources/status", "myresources/finalizers"]
  verbs: ["get", "list", "watch", "update", "patch"]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
  name: my-operator
subjects:
- kind: ServiceAccount
  name: my-operator
  namespace: my-operator-system
roleRef:
  kind: ClusterRole
  name: my-operator
  apiGroup: rbac.authorization.k8s.io

Pattern 2: CI/CD Pipeline Service Account

# CI/CD: deploy-only access to specific namespace
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
  namespace: production
  name: cicd-deployer
rules:
- apiGroups: ["apps"]
  resources: ["deployments", "statefulsets", "daemonsets"]
  verbs: ["get", "list", "watch", "create", "update", "patch"]
- apiGroups: [""]
  resources: ["configmaps"]
  verbs: ["get", "list", "watch", "create", "update", "patch"]
- apiGroups: [""]
  resources: ["services"]
  verbs: ["get", "list", "watch", "create", "update", "patch"]
# NOTE: no secrets access — secrets managed by Vault/ESO, not CI pipeline
# NOTE: no delete — prevent accidental deletion; separate break-glass role for deletions

Pattern 3: Read-Only Monitoring Service Account

apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
  name: prometheus-scraper
rules:
- apiGroups: [""]
  resources: ["nodes", "nodes/proxy", "nodes/metrics", "services",
              "endpoints", "pods"]
  verbs: ["get", "list", "watch"]
- apiGroups: ["extensions", "networking.k8s.io"]
  resources: ["ingresses"]
  verbs: ["get", "list", "watch"]
- nonResourceURLs: ["/metrics", "/metrics/cadvisor"]
  verbs: ["get"]

Pattern 4: Namespace Admin (Team Lead)

# Use built-in 'admin' ClusterRole via RoleBinding for namespace scope
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
  name: team-alpha-admins
  namespace: team-alpha
subjects:
- kind: Group
  name: oidc:team-alpha-leads   # OIDC group
  apiGroup: rbac.authorization.k8s.io
roleRef:
  kind: ClusterRole
  name: admin                   # built-in — grants full namespace access, no RBAC management
  apiGroup: rbac.authorization.k8s.io

Pattern 5: RBAC for Custom Resources (CRDs)

# Grant access to custom resources + extend built-in view ClusterRole
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
  name: myapp-crd-view
  labels:
    rbac.authorization.k8s.io/aggregate-to-view: "true"  # extends built-in view
    rbac.authorization.k8s.io/aggregate-to-edit: "true"  # extends built-in edit
    rbac.authorization.k8s.io/aggregate-to-admin: "true" # extends built-in admin
rules:
- apiGroups: ["myapp.io"]
  resources: ["myresources", "myresources/status"]
  verbs: ["get", "list", "watch"]

Multi-Tenant RBAC Design

For namespace-based multi-tenancy, each tenant namespace should have its own RBAC structure. Cross-namespace access is intentionally limited in Kubernetes.

Platform Team (ClusterRoleBinding → cluster-admin)
    │
    ├── Namespace: team-alpha
    │       RoleBinding → admin  (team-alpha-leads group)
    │       RoleBinding → edit   (team-alpha-devs group)
    │       RoleBinding → view   (team-alpha-observers group)
    │       RoleBinding → cicd-deployer (SA: ci-alpha)
    │       NetworkPolicy: default-deny-all
    │       ResourceQuota: team-alpha-quota
    │       LimitRange: team-alpha-limits
    │
    ├── Namespace: team-beta
    │       RoleBinding → admin  (team-beta-leads group)
    │       ... (same pattern)
    │
    └── Namespace: monitoring
            ClusterRoleBinding → prometheus-scraper (SA: prometheus)
            [cluster-wide read for metrics scraping]

Cross-Namespace Access Limitations

RBAC does not have a "cross-namespace" concept. A ServiceAccount in namespace A cannot be granted access to namespace B's resources via a RoleBinding in namespace A. Options for cross-namespace access:

Metrics & Alerts

Key Metrics

MetricSourceWhat It Tells You
apiserver_authorization_decisions_total{decision="allow|deny|no-opinion"}kube-apiserverAuthorization decision counts by decision type and stage
rest_client_requests_total{verb="POST|GET",resource="clusterrolebindings"}client-go metricsRate of binding creation/mutation from controllers
apiserver_audit_event_totalaudit logTotal audit events — spike may indicate scanning or probing
apiserver_authentication_attempts_total{result="error|success"}kube-apiserverFailed auth attempts — brute force or misconfigured certs
apiserver_request_total{verb="WATCH",resource="secrets"}kube-apiserverUnexpected watch on secrets — possible secret enumeration

Alerts

groups:
- name: rbac.rules
  rules:

  - alert: ClusterAdminBindingCreated
    expr: |
      increase(apiserver_audit_event_total[5m]) > 0
      # (use audit log stream filter — Prometheus alone can't filter audit events)
    # Implementation: stream audit log to alerting system; match:
    # verb=create, objectRef.resource=clusterrolebindings,
    # requestObject.roleRef.name=cluster-admin
    annotations:
      summary: "New cluster-admin binding created"
      description: "A ClusterRoleBinding referencing cluster-admin was created by {{ $labels.user }}"
    labels:
      severity: critical

  - alert: ExcessiveAuthorizationDenials
    expr: |
      rate(apiserver_authorization_decisions_total{decision="deny"}[5m]) > 10
    for: 2m
    annotations:
      summary: "High RBAC denial rate — possible misconfiguration or probing"
    labels:
      severity: warning

  - alert: WildcardRoleCreated
    # Implement via OPA/Gatekeeper or Kyverno policy + audit
    annotations:
      summary: "Role with wildcard verb or resource created"
    labels:
      severity: high

  - alert: SecretsListFromUnexpectedSA
    # Implement via audit log: verb=list, resource=secrets,
    # user.username not in allowlist
    annotations:
      summary: "Secrets listed by unexpected service account"
    labels:
      severity: high

Runbooks

  1. Unexpected cluster-admin binding detected: Immediately identify who created the binding (kubectl describe clusterrolebinding <name>, cross-check audit log). Determine if the binding is legitimate (platform admin performing break-glass) or unauthorized. If unauthorized: revoke binding, rotate credentials for the creating principal, audit all actions performed during the window, consider cluster compromise response.
  2. Service account can exec into pods: Run kubectl auth can-i create pods/exec --as=system:serviceaccount:<ns>:<name>. Find the granting Role/ClusterRole via rbac-lookup. Assess whether exec access is genuinely needed. If not: remove the pods/exec rule. Consider adding an OPA/Gatekeeper policy to prevent future grants.
  3. High RBAC denial rate: Identify denying requests via audit log (grep "Forbidden" apiserver.log or SIEM query). Determine if it's a misconfigured application (missing RBAC rule) or probing. For misconfigured apps: create minimal required Role. For probing: investigate source IP/SA, consider blocking.
  4. Secrets enumeration detected (unexpected list on secrets): Identify the SA/user from audit log. Check if access was intentional (new deployment) or anomalous. Revoke access if unauthorized. Check what secrets were accessed. Rotate any potentially exposed secret values.
  5. Rotate compromised service account: Delete the SA token secret (for legacy tokens) or wait for bound token expiry (default 1h). Delete and recreate the ServiceAccount if needed. Update all RoleBindings referencing the SA. Audit all API calls made by the SA in the compromise window via audit log.

Best Practices

  1. One ServiceAccount per workload, never use default. The default ServiceAccount in each namespace is auto-mounted into all pods that don't specify one. Set automountServiceAccountToken: false on the default ServiceAccount in every namespace, and create dedicated SAs for workloads that need API server access. This ensures each workload has only the permissions it needs, and compromising one pod doesn't compromise the namespace's default SA token.
  2. Prefer RoleBinding+ClusterRole over ClusterRoleBinding. For namespace-scoped access, bind a ClusterRole using a RoleBinding rather than a ClusterRoleBinding. This reuses the ClusterRole definition without granting cluster-wide access. Only use ClusterRoleBinding for genuinely cluster-wide operators or platform infrastructure.
  3. Never grant wildcard verbs (*) in production. Wildcards include verbs like deletecollection that can destroy all objects of a type. Always enumerate specific verbs. Enforce this via OPA/Gatekeeper or Kyverno policy that rejects Role/ClusterRole objects with wildcard verbs.
  4. Separate read from write from delete in role design. Define separate roles for read-only, read-write, and read-write-delete access. Bind CI/CD pipelines to the narrowest role needed. Rarely does a CI system need delete access — create an explicit break-glass role for deletion operations.
  5. Audit all ClusterRoleBindings to cluster-admin weekly. Run kubectl get clusterrolebindings -o json | jq '[.items[] | select(.roleRef.name=="cluster-admin")]' and review the subject list. Any non-system, non-platform-admin principal binding to cluster-admin is a finding. Automate this check in your security posture management pipeline (Starboard, Trivy Operator, Kubescape).
  6. Use ClusterRole aggregation for extensible role design. When deploying CRDs, always create a ClusterRole with aggregate-to-view/edit/admin labels so operators of your CRD can be managed with the standard built-in roles. This prevents the need to grant wildcards or cluster-admin to manage custom resources.
  7. Restrict pods/exec, pods/portforward, and nodes/proxy tightly. These subresources bypass normal application-level access controls. Exec into a container is as powerful as SSH. Treat grants of these verbs as high-privilege and require justification. Monitor exec operations via audit log.
  8. Implement OPA/Gatekeeper or Kyverno policies as guardrails for RBAC. Pure RBAC cannot prevent an admin from creating wildcard roles or cluster-admin bindings for non-admins. Add admission policies that: block wildcard verbs on non-platform namespaces, block new cluster-admin ClusterRoleBindings without a specific label, require all Roles to have an owner annotation. This creates a policy-as-code safety net around RBAC.