CKS — RBAC Best Practices

8 minute read

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:

  1. Create a new ClusterRole with * on * (all permissions)
  2. Bind themselves to it
  3. 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

Updated: