RBAC — Role-Based Access Control
On this page
- RBAC Data Model
- API Objects: Role, ClusterRole, Binding
- Verbs, Resources, Subresources
- Subjects: User, Group, ServiceAccount
- ClusterRole Aggregation
- Default ClusterRoles
- Privilege Escalation Prevention
- Impersonation
- Node Authorization
- RBAC Auditing Tools
- Common RBAC Patterns
- Multi-Tenant RBAC Design
- Metrics & Alerts
- 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 Type | Role Type | Effective Scope | When to Use |
|---|---|---|---|
RoleBinding | Role | Single namespace (binding's namespace) | Namespace-scoped access for a team or workload |
RoleBinding | ClusterRole | Single namespace (binding's namespace) | Reuse a common ClusterRole across many namespaces without granting cluster-wide access |
ClusterRoleBinding | ClusterRole | All namespaces + cluster-scoped resources | Cluster-wide operators, platform admins |
ClusterRoleBinding | Role | Not allowed — Role is namespace-scoped | N/A |
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 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
| Verb | HTTP Method | Description | Notes |
|---|---|---|---|
get | GET /resource/name | Read a single object by name | Also required for kubectl describe |
list | GET /resource | List all objects (optionally filtered) | Returns full object bodies — more permissive than get |
watch | GET /resource?watch=true | Stream change events | Required for controllers/informers; implies list access effectively |
create | POST /resource | Create a new object | Also required to use pods/exec, pods/portforward |
update | PUT /resource/name | Replace full object (requires resourceVersion) | Full object replacement |
patch | PATCH /resource/name | Partial update (JSON Patch, Merge Patch, SSA) | Commonly used by controllers |
delete | DELETE /resource/name | Delete a single object | |
deletecollection | DELETE /resource | Delete all matching objects | Dangerous — grant separately from delete |
impersonate | X-Impersonate headers | Act as another user/group/SA | Only for admins and test tooling; see impersonation section |
bind | — | Create RoleBindings/ClusterRoleBindings | Privilege escalation guard — see below |
escalate | — | Create/update Roles/ClusterRoles with permissions exceeding your own | Privilege escalation guard — see below |
approve | — | Approve CertificateSigningRequests | Special verb on CSR objects |
sign | — | Sign CertificateSigningRequests | Special verb on CSR objects |
use | — | Use a PodSecurityPolicy (deprecated) or RuntimeClass | PSP removed in 1.25; RuntimeClass still uses this verb |
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:
| Subresource | Required Verb | What It Allows |
|---|---|---|
pods/exec | create | kubectl exec into a running container |
pods/log | get | kubectl logs |
pods/portforward | create | kubectl port-forward |
pods/attach | create | kubectl attach (stdin to running process) |
pods/eviction | create | Eviction API (used by kubectl drain, Cluster Autoscaler) |
pods/status | get, update, patch | Read/write pod status subresource (used by kubelet) |
deployments/scale | get, update, patch | Scale a Deployment via the scale subresource |
nodes/proxy | get | Proxy requests through API server to kubelet HTTP endpoint |
serviceaccounts/token | create | Request a bound token for a ServiceAccount |
certificatesigningrequests/approval | update | Approve a CSR |
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
| Kind | Identifier | Source | Notes |
|---|---|---|---|
User | String name | X.509 cert CN; OIDC sub or email claim | Kubernetes has no User object — users exist only in external systems |
Group | String name | X.509 cert O field; OIDC groups claim | Multiple groups per user; Kubernetes creates some system groups |
ServiceAccount | name + namespace | Created as K8s object; token auto-mounted into pods | Must specify namespace in subject spec |
System Groups
| Group | Who Belongs | Security Notes |
|---|---|---|
system:authenticated | All authenticated requests | Default ClusterRoleBindings grant some read access — audit regularly |
system:unauthenticated | Anonymous requests (if anonymous auth enabled) | Should have zero permissions in production |
system:masters | X.509 certs with O=system:masters | Bypasses RBAC entirely — equivalent to cluster-admin, cannot be restricted by RBAC. Only for break-glass admin certs. |
system:nodes | Kubelets (X.509 CN=system:node:nodename) | Node authorization mode restricts permissions to per-node resources |
system:serviceaccounts | All ServiceAccounts in all namespaces | Avoid 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 |
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
| ClusterRole | Access Level | Typical Binding | Warning |
|---|---|---|---|
cluster-admin | Full access to everything in all namespaces | Break-glass only; never CI/CD or workloads | Use sparingly |
admin | Full namespace access (no ResourceQuota/LimitRange write) | Namespace owners via RoleBinding per namespace | |
edit | Read/write most namespaced objects; no RBAC read | Developers via RoleBinding per namespace | |
view | Read-only on most namespaced objects (no Secrets) | Auditors, monitoring SAs | |
system:node | Kubelet permissions (node authorization handles this) | Auto-bound via node authorization | |
system:auth-delegator | Delegate authentication decisions | Extension API servers | |
system:heapster | Legacy metrics read; deprecated | Deprecated — use metrics-server RBAC |
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
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 Path | Required Permission | Mitigation |
|---|---|---|
| Create/update ClusterRole with excessive permissions | escalate on clusterroles | Only platform-admins; audit log all clusterrole writes |
| Bind cluster-admin to self | bind on clusterrolebindings | Alert on cluster-admin bindings created by non-system principals |
| Create SA + bind cluster-admin to it + exec into pod using SA | create SA + create pod + bind | Restrict bind; alert on new ClusterRoleBindings to cluster-admin |
| Modify existing ClusterRoleBinding subject list | patch on clusterrolebindings | Audit log all binding mutations; treat binding writes as high-severity events |
| Impersonate a higher-privileged user | impersonate on users/groups/serviceaccounts | No workload or user should hold impersonate; only specific test tooling |
| exec into privileged pod → escape to host | create on pods/exec | Restrict exec; PSA restricted prevents privileged pods |
| Read secret containing kubeconfig or cloud credentials | get on secrets | RBAC 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:
- Create a RoleBinding in namespace B referencing the SA from namespace A (the SA subject can be in a different namespace)
- Use ClusterRoleBinding for cluster-wide access (loses namespace isolation)
- Use Kubernetes Network Policy + API Gateway pattern to proxy requests through a service in the target namespace
- For operator patterns: use ClusterRoleBinding with namespace selector logic in the operator itself
Metrics & Alerts
Key Metrics
| Metric | Source | What It Tells You |
|---|---|---|
apiserver_authorization_decisions_total{decision="allow|deny|no-opinion"} | kube-apiserver | Authorization decision counts by decision type and stage |
rest_client_requests_total{verb="POST|GET",resource="clusterrolebindings"} | client-go metrics | Rate of binding creation/mutation from controllers |
apiserver_audit_event_total | audit log | Total audit events — spike may indicate scanning or probing |
apiserver_authentication_attempts_total{result="error|success"} | kube-apiserver | Failed auth attempts — brute force or misconfigured certs |
apiserver_request_total{verb="WATCH",resource="secrets"} | kube-apiserver | Unexpected 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
- 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. - Service account can exec into pods: Run
kubectl auth can-i create pods/exec --as=system:serviceaccount:<ns>:<name>. Find the granting Role/ClusterRole viarbac-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. - High RBAC denial rate: Identify denying requests via audit log (
grep "Forbidden" apiserver.logor 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. - 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.
- 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
- One ServiceAccount per workload, never use default. The
defaultServiceAccount in each namespace is auto-mounted into all pods that don't specify one. SetautomountServiceAccountToken: falseon 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. - 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.
- Never grant wildcard verbs (
*) in production. Wildcards include verbs likedeletecollectionthat 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. - 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.
- 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). - Use ClusterRole aggregation for extensible role design. When deploying CRDs, always create a ClusterRole with
aggregate-to-view/edit/adminlabels 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. - Restrict
pods/exec,pods/portforward, andnodes/proxytightly. 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. - 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.