Skip to content
CAMPUX Cloud Bootcamp Lab · Class 27e · AKS ← All labs
Hands-On Lab · Beginner
~20 min · Free control plane · Cloud Shell
AKS & kubectl · torn down
Class 27e · Security

Do not trust the RBAC you wrote. Prove it.

Kubernetes RBAC is a YAML file that describes a permission — it is not, by itself, evidence that the permission is correct. This lab writes a Role and RoleBinding for one namespace, then uses kubectl auth can-i with --as impersonation to interrogate the API server the same way it would interrogate a real service account: can this identity list pods here? Can it read a Secret? Then it over-grants on purpose, proves the blast radius grew, and narrows the Role back to what the job actually needs.

Fig. 1 · A Role is a claim; can-i is the test
A Role plus a RoleBinding is only a claim about permission; kubectl auth can-i --as tests it directly against the API server. Role · pod-reader get, list, watch on: pods RoleBinding binds Role → app-reader can-i --as=app-reader list pods → yes get secrets → the RoleBinding is a claim. can-i is the test against the live API server.
Why

Impersonation gives you a second identity for free

Every real RBAC bug looks the same in hindsight: someone wrote a Role, believed it granted only what the comment said, and never checked. The check is one command. kubectl auth can-i <verb> <resource> --as=<identity> -n <namespace> asks the API server's authorizer the exact question a real request would ask, without you needing a second kubeconfig, a second cluster user, or a second terminal signed in as someone else. The --as flag runs the check as if you were that service account, using your own admin credentials to ask the question.

You will create a namespace, a service account with no permissions of its own, a Role that grants exactly what a "read pods" job needs, and a RoleBinding connecting the two. Then you interrogate it with can-i, over-grant it on purpose to see the blast radius change, and narrow it back to least privilege — the loop every access review is supposed to run and rarely does.

A Role is a claim. can-i is the test.

Before you begin — one-time setup

You need a free Azure account and the Azure CLI (az), signed in with az login. First time? The 15-minute Set up your machine page covers the account, the installs, and sign-in. Prefer zero installs? Run everything in Azure Cloud Shell (Bash)az and kubectl are preinstalled and already signed in.

Cost & teardown

This lab assumes the cluster from Your first AKS cluster. If you already tore that one down, Step 1 recreates it with the same command. Everything after that is API objects, not compute — the only ongoing cost is the same one node, cents per hour. Tear down the cluster at the end regardless.

Step 1

The cluster

If you still have the cluster from Your first AKS cluster, skip to get-credentials. Otherwise recreate it with the identical command.

# Windows/Git Bash: stop it mangling /subscriptions/... arguments (harmless on macOS/Linux)
export MSYS_NO_PATHCONV=1

RG="campux-lab-aks-rg"
az group create -n "$RG" -l eastus

# same command as the first-cluster lab: free control plane, one small node
az aks create -n campux-aks -g "$RG" \
  --tier free \
  --node-count 1 \
  --node-vm-size Standard_B2s \
  --enable-managed-identity \
  --generate-ssh-keys

az aks get-credentials -n campux-aks -g "$RG"
kubectl get nodes
Checkpoint kubectl get nodes lists one node, status Ready. Confirm you are cluster-admin for this exercise — you need the authority to grant permissions before you can test taking some away: kubectl auth can-i create rolebindings --all-namespaces returns yes.
Step 2

A namespace and an unprivileged identity

Create a namespace to scope everything to, and a ServiceAccount that starts with zero permissions of its own — Kubernetes grants nothing by default.

kubectl create namespace demo
kubectl create serviceaccount app-reader -n demo

# prove it starts with nothing
kubectl auth can-i list pods --as=system:serviceaccount:demo:app-reader -n demo
kubectl auth can-i get secrets --as=system:serviceaccount:demo:app-reader -n demo
Checkpoint Both checks return no. app-reader exists but has no Role bound to it yet — this is the default-deny posture Kubernetes RBAC starts from, and it is exactly why the next step is additive rather than something you have to remember to restrict.
Step 3

Grant exactly what a pod-reading job needs

Write a Role scoped to the demo namespace that allows reading pods and nothing else, then bind it to app-reader with a RoleBinding.

cat > pod-reader-role.yaml <<'EOF'
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
  namespace: demo
  name: pod-reader
rules:
- apiGroups: [""]
  resources: ["pods"]
  verbs: ["get", "list", "watch"]
EOF

cat > pod-reader-binding.yaml <<'EOF'
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
  name: pod-reader-binding
  namespace: demo
subjects:
- kind: ServiceAccount
  name: app-reader
  namespace: demo
roleRef:
  kind: Role
  name: pod-reader
  apiGroup: rbac.authorization.k8s.io
EOF

kubectl apply -f pod-reader-role.yaml
kubectl apply -f pod-reader-binding.yaml
Checkpoint kubectl get role,rolebinding -n demo shows both objects. That is what the YAML claims. The next command is what actually matters.
kubectl auth can-i list pods --as=system:serviceaccount:demo:app-reader -n demo
kubectl auth can-i get pods --as=system:serviceaccount:demo:app-reader -n demo
kubectl auth can-i get secrets --as=system:serviceaccount:demo:app-reader -n demo
kubectl auth can-i delete pods --as=system:serviceaccount:demo:app-reader -n demo
kubectl auth can-i list pods --as=system:serviceaccount:demo:app-reader -n default
Checkpoint list pods and get pods in demo both return yes. get secrets, delete pods, and list pods in the default namespace all return no — a RoleBinding only reaches its own namespace, and the Role granted only get/list/watch on pods, nothing else. Five checks, five answers that match the YAML exactly. That match is what "least privilege" means in practice: not a policy document, a set of can-i answers you can point to.
Step 4

Over-grant on purpose — and prove the difference

Now make the mistake most RBAC reviews exist to catch: widen the Role to a wildcard on all resources and verbs, "just to unblock someone." Re-run the same checks and watch the answers change.

cat > pod-reader-role.yaml <<'EOF'
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
  namespace: demo
  name: pod-reader
rules:
- apiGroups: ["*"]
  resources: ["*"]
  verbs: ["*"]
EOF

kubectl apply -f pod-reader-role.yaml

kubectl auth can-i get secrets --as=system:serviceaccount:demo:app-reader -n demo
kubectl auth can-i delete pods --as=system:serviceaccount:demo:app-reader -n demo
kubectl auth can-i create rolebindings --as=system:serviceaccount:demo:app-reader -n demo
Checkpoint All three now return yes. The same service account that could only read pods a minute ago can now read Secrets (including anything mounted as an environment variable or file — credentials, connection strings), delete pods, and even create new RoleBindings in its own namespace, which is one step from granting itself more. Nothing about the ServiceAccount changed. Only the Role did — and it still reads, on a glance, like "give the reader access." This is the exact gap between what a Role says it does and what it actually authorizes, and can-i is the only way to close it without waiting for an incident to find out.
What just happened

A wildcard resources: ["*"] with verbs: ["*"] is a common shortcut when a permission error is blocking someone and the fastest fix is "grant everything." It works, which is the danger — it removes the error without anyone noticing the scope just grew from one resource type to every resource type Kubernetes knows about, in that namespace, including Secrets and the RBAC objects themselves. A Role's rules are additive and there is no separate "except this" clause; the only ceiling is what you write.

Step 5

Narrow it back to least privilege

Reapply the original scoped Role. The RoleBinding does not need to change — only the Role it points at.

cat > pod-reader-role.yaml <<'EOF'
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
  namespace: demo
  name: pod-reader
rules:
- apiGroups: [""]
  resources: ["pods"]
  verbs: ["get", "list", "watch"]
EOF

kubectl apply -f pod-reader-role.yaml

kubectl auth can-i get secrets --as=system:serviceaccount:demo:app-reader -n demo
kubectl auth can-i delete pods --as=system:serviceaccount:demo:app-reader -n demo
kubectl auth can-i list pods --as=system:serviceaccount:demo:app-reader -n demo
Checkpoint get secrets and delete pods are back to no; list pods is back to yes. Nothing about the ServiceAccount or the RoleBinding moved — narrowing the Role was the entire fix, because a Role is the only place the permission actually lives. Re-run the full five-check block from Step 3 if you want the full before/after side by side.
Down

Tear it down

Clear the namespace, then the cluster.

kubectl delete namespace demo

az group delete -n campux-lab-aks-rg --yes --no-wait
az group exists -n campux-lab-aks-rg      # -> false once complete
Checkpoint Confirm in the portal that both the cluster's resource group and its auto-managed node group (named like MC_campux-lab-aks-rg_campux-aks_eastus) are gone.
End

What you can now honestly claim

You created a Role and RoleBinding, then proved what they actually granted with kubectl auth can-i --as instead of trusting the YAML — five checks whose answers had to match the rules exactly, or something was wrong. You widened the Role to a wildcard, watched a Secret-read and a pod-delete both flip to yes with no other change, and narrowed it back. In an access review, that is the whole job: state the claim, run the test, and never let "I wrote a Role for that" stand in for "I checked what it allows."