CKS — RBAC Best Practices
RBAC Best Practices for CKS
The CKS exam expects you to go beyond basic RBAC configuration. You need to audit existing policies, identify dangerous permissions, and design least-privilege bindings that minimize blast radius.
Principle of Least Privilege — CKS Perspective
In a security context, least privilege means:
- No identity should have more permissions than required for its function.
- Default deny — if a permission isn’t explicitly granted, it’s denied.
- Prefer namespaced Roles over ClusterRoles wherever possible.
- Prefer specific resource names over wildcard access.
- Regularly audit who has access to what.
The Blast Radius Problem
cluster-admin ClusterRoleBinding
│
▼
┌──────────────────────────────────┐
│ Can do ANYTHING in ANY namespace │
│ • Read all Secrets │
│ • Delete all workloads │
│ • Modify RBAC │
│ • Escalate privileges │
└──────────────────────────────────┘
vs.
Namespaced Role with specific verbs
│
▼
┌──────────────────────────────────┐
│ Can only list/get Pods in "dev" │
│ • Cannot read Secrets │
│ • Cannot modify RBAC │
│ • Cannot escape namespace │
└──────────────────────────────────┘
Avoiding cluster-admin in Production
The built-in cluster-admin ClusterRole grants * (all verbs) on * (all resources) in * (all API groups). Binding a user or ServiceAccount to cluster-admin is the most dangerous RBAC action possible.
Finding cluster-admin Bindings
# List all ClusterRoleBindings referencing cluster-admin
kubectl get clusterrolebindings -o json | jq -r '
.items[] |
select(.roleRef.name == "cluster-admin") |
"\(.metadata.name) → \(.subjects[]?.kind)/\(.subjects[]?.name)"
'
Expected output for a default cluster:
cluster-admin → User/system:masters (bootstrap group)
Any additional bindings to cluster-admin should be investigated. In production, replace them with purpose-built ClusterRoles.
What to Use Instead
# Instead of cluster-admin for a monitoring tool:
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
name: monitoring-reader
rules:
- apiGroups: [""]
resources: ["pods", "nodes", "services", "endpoints"]
verbs: ["get", "list", "watch"]
- apiGroups: ["apps"]
resources: ["deployments", "replicasets", "daemonsets", "statefulsets"]
verbs: ["get", "list", "watch"]
- apiGroups: ["metrics.k8s.io"]
resources: ["pods", "nodes"]
verbs: ["get", "list"]
Auditing Existing RBAC Permissions
Check Permissions for a Specific Identity
# What can a specific ServiceAccount do?
kubectl auth can-i --list \
--as=system:serviceaccount:default:default
# What can a specific user do in a namespace?
kubectl auth can-i --list \
--as=jane \
--namespace=production
# Can this SA create pods?
kubectl auth can-i create pods \
--as=system:serviceaccount:app-ns:app-sa \
--namespace=app-ns
RBAC Audit Commands Reference
| Command | Purpose |
|---|---|
kubectl auth can-i --list --as=<identity> |
List all permissions for an identity |
kubectl auth can-i <verb> <resource> --as=<identity> |
Check a specific permission |
kubectl auth can-i --list --as=<identity> -n <ns> |
Check namespace-scoped permissions |
kubectl get clusterrolebindings -o wide |
List all ClusterRoleBindings with subjects |
kubectl get rolebindings -n <ns> -o wide |
List RoleBindings in a namespace |
kubectl describe clusterrole <name> |
Show rules for a ClusterRole |
kubectl auth whoami |
Show current authenticated identity (1.27+) |
Finding Over-Privileged Bindings
# Find all bindings that grant wildcard access
kubectl get clusterroles -o json | jq -r '
.items[] |
select(.rules[]? | .verbs[]? == "*" or .resources[]? == "*") |
.metadata.name
'
# Find non-system ClusterRoleBindings (potential misconfigurations)
kubectl get clusterrolebindings -o json | jq -r '
.items[] |
select(.metadata.name | startswith("system:") | not) |
"\(.metadata.name): \(.roleRef.name) → \(.subjects[]?.kind)/\(.subjects[]?.name)"
'
Role Aggregation Patterns
ClusterRoles can be aggregated using label selectors. This allows you to build composite roles from smaller, purpose-specific roles.
How Aggregation Works
# Base aggregated ClusterRole (auto-collects rules from labeled roles)
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
name: team-lead
aggregationRule:
clusterRoleSelectors:
- matchLabels:
rbac.myorg.io/aggregate-to-team-lead: "true"
rules: [] # Rules are auto-populated — do NOT set manually
# Contributor role — gets aggregated into team-lead
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
name: team-lead-pods
labels:
rbac.myorg.io/aggregate-to-team-lead: "true"
rules:
- apiGroups: [""]
resources: ["pods", "pods/log"]
verbs: ["get", "list", "watch", "create", "delete"]
# Another component — also aggregated
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
name: team-lead-deployments
labels:
rbac.myorg.io/aggregate-to-team-lead: "true"
rules:
- apiGroups: ["apps"]
resources: ["deployments"]
verbs: ["get", "list", "watch", "create", "update", "patch"]
Built-in roles like admin, edit, and view use aggregation. Custom controllers often label their ClusterRoles for aggregation into these built-in roles.
Designing Role Separation: Admin, Developer, Viewer
Viewer Role (Read-Only)
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
name: viewer
namespace: app-ns
rules:
- apiGroups: [""]
resources: ["pods", "services", "configmaps", "events"]
verbs: ["get", "list", "watch"]
- apiGroups: ["apps"]
resources: ["deployments", "replicasets"]
verbs: ["get", "list", "watch"]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
name: viewer-binding
namespace: app-ns
subjects:
- kind: Group
name: viewers
apiGroup: rbac.authorization.k8s.io
roleRef:
kind: Role
name: viewer
apiGroup: rbac.authorization.k8s.io
Developer Role (Deploy Workloads, No RBAC, No Secrets)
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
name: developer
namespace: app-ns
rules:
- apiGroups: [""]
resources: ["pods", "pods/log", "pods/portforward", "services", "configmaps"]
verbs: ["get", "list", "watch", "create", "update", "patch", "delete"]
- apiGroups: ["apps"]
resources: ["deployments", "replicasets"]
verbs: ["get", "list", "watch", "create", "update", "patch", "delete"]
- apiGroups: ["batch"]
resources: ["jobs", "cronjobs"]
verbs: ["get", "list", "watch", "create", "update", "patch", "delete"]
# NOTE: No access to secrets, RBAC resources, or pods/exec
Namespace Admin Role (Full Namespace Access, No RBAC Escalation)
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
name: namespace-admin
namespace: app-ns
rules:
- apiGroups: ["", "apps", "batch", "networking.k8s.io"]
resources: ["*"]
verbs: ["*"]
- apiGroups: [""]
resources: ["secrets"]
verbs: ["get", "list", "watch", "create", "update", "patch", "delete"]
# Explicitly NOT granting: rbac.authorization.k8s.io resources
# This prevents self-escalation
Dangerous RBAC Permissions
These permissions are high-risk and should be audited carefully:
| Permission | Risk | Why It’s Dangerous |
|---|---|---|
verbs: ["*"] on resources: ["*"] |
Critical | Equivalent to full admin within scope |
secrets — get, list, watch |
High | Exposes all credentials, tokens, TLS keys |
pods/exec |
High | Allows arbitrary command execution inside any Pod |
pods/attach |
High | Attaches to running container processes |
escalate verb on roles/clusterroles |
Critical | Allows creating roles with more permissions than the creator has |
bind verb on roles/clusterroles |
Critical | Allows binding to any role, including cluster-admin |
impersonate verb on users/groups/serviceaccounts |
Critical | Allows acting as any other identity |
create on pods |
Medium | Can mount ServiceAccount tokens and Secrets via volume mounts |
create on serviceaccounts/token |
High | Can generate tokens for any SA in the namespace |
Finding Dangerous Permissions
# Find roles that can exec into pods
kubectl get roles,clusterroles -A -o json | jq -r '
.items[] |
select(.rules[]?.resources[]? == "pods/exec") |
"\(.kind)/\(.metadata.name) in \(.metadata.namespace // "cluster-wide")"
'
# Find roles with escalate or bind verbs
kubectl get clusterroles -o json | jq -r '
.items[] |
select(.rules[]? | (.verbs[]? == "escalate" or .verbs[]? == "bind")) |
.metadata.name
'
# Find roles that can read secrets
kubectl get roles,clusterroles -A -o json | jq -r '
.items[] |
select(.rules[]? | (.resources[]? == "secrets") and
(.verbs[]? == "get" or .verbs[]? == "list" or .verbs[]? == "*")) |
"\(.kind)/\(.metadata.name) in \(.metadata.namespace // "cluster-wide")"
'
Common CKS RBAC Mistakes
Mistake 1: Using ClusterRoleBinding When RoleBinding Suffices
# BAD — grants access in ALL namespaces
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
name: app-logs-reader
subjects:
- kind: ServiceAccount
name: log-collector
namespace: monitoring
roleRef:
kind: ClusterRole
name: pod-log-reader
apiGroup: rbac.authorization.k8s.io
# GOOD — grants access only in the target namespace
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
name: app-logs-reader
namespace: app-ns
subjects:
- kind: ServiceAccount
name: log-collector
namespace: monitoring
roleRef:
kind: ClusterRole # Can still reference a ClusterRole
name: pod-log-reader
apiGroup: rbac.authorization.k8s.io
Mistake 2: Granting create on Pods Without Considering Token Access
A subject that can create Pods can mount any Secret or any ServiceAccount token in the namespace:
# This Pod mounts a privileged SA token
apiVersion: v1
kind: Pod
metadata:
name: exploit
spec:
serviceAccountName: admin-sa # ← Uses a privileged SA
containers:
- name: shell
image: alpine
command: ["sleep", "3600"]
To mitigate: restrict create on Pods and use PodSecurity admission to enforce automountServiceAccountToken: false.
Practice Questions
Q1. A developer reports they need access to read Pods and Deployments in the staging namespace. Design the minimum-privilege RBAC configuration.
Show Answer
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
name: staging-reader
namespace: staging
rules:
- apiGroups: [""]
resources: ["pods"]
verbs: ["get", "list", "watch"]
- apiGroups: ["apps"]
resources: ["deployments"]
verbs: ["get", "list", "watch"]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
name: dev-staging-reader
namespace: staging
subjects:
- kind: User
name: developer
apiGroup: rbac.authorization.k8s.io
roleRef:
kind: Role
name: staging-reader
apiGroup: rbac.authorization.k8s.io
Key points:
- Use a Role (not ClusterRole) to restrict scope to
staging - Only
get,list,watch— no write access - Specific resources — no wildcards
Q2. You discover a ClusterRoleBinding that grants cluster-admin to the system:serviceaccount:default:default identity. What is the security impact, and how do you remediate it?
Show Answer
Impact: Every Pod in the default namespace that uses the default ServiceAccount (which is every Pod unless overridden) now has full cluster-admin access. A compromised container can:
- Read all Secrets across all namespaces
- Delete any resource
- Modify RBAC and grant itself persistent access
Remediation:
# 1. Find and delete the dangerous binding
kubectl get clusterrolebindings -o json | jq -r '
.items[] | select(
.roleRef.name == "cluster-admin" and
(.subjects[]? | .name == "default" and .namespace == "default")
) | .metadata.name'
# 2. Delete it
kubectl delete clusterrolebinding <name-from-above>
# 3. Create a purpose-specific role with minimum permissions
# 4. Audit all Pods in default namespace for what they actually need
Q3. Why is the escalate verb on roles or clusterroles considered critical from a security perspective?
Show Answer
The escalate verb allows a subject to create or modify a Role/ClusterRole with permissions the subject does not currently have. Without escalate, RBAC prevents privilege escalation — you can only create roles with permissions you already possess.
With escalate, a user could:
- Create a new ClusterRole with
*on*(all permissions) - Bind themselves to it
- Effectively become cluster-admin
This is why escalate, along with bind and impersonate, should be granted only to trusted cluster administrators.
Q4. How do you check what permissions the monitoring-sa ServiceAccount in the monitoring namespace has?
Show Answer
# List all permissions for this ServiceAccount
kubectl auth can-i --list \
--as=system:serviceaccount:monitoring:monitoring-sa
# Check namespace-specific permissions
kubectl auth can-i --list \
--as=system:serviceaccount:monitoring:monitoring-sa \
-n monitoring
# Check a specific permission
kubectl auth can-i get secrets \
--as=system:serviceaccount:monitoring:monitoring-sa \
-n monitoring
The --as flag uses impersonation to check permissions from another identity’s perspective. The format for ServiceAccounts is system:serviceaccount:<namespace>:<name>.
Q5. A ClusterRole has verbs: ["*"] on resources: ["pods"]. Does this include pods/exec and pods/log?
Show Answer
No. In Kubernetes RBAC, sub-resources like pods/exec, pods/log, pods/portforward, and pods/attach are treated as separate resources. A wildcard on pods does not automatically grant access to its sub-resources.
To grant access to both:
rules:
- apiGroups: [""]
resources: ["pods", "pods/exec", "pods/log"]
verbs: ["get", "create"] # exec and portforward need "create"
However, verbs: ["*"] on resources: ["*"] does include all sub-resources because the resource wildcard matches everything.
| Previous | Up | Next |
|---|---|---|
| Restrict API Access | CKS Home | ServiceAccount Security |