Run one step, then check it
Read the step before copying. Bash commands belong in Git Bash on Windows or Azure Cloud Shell set to Bash; PowerShell uses different syntax. A JSON, YAML, Bicep or application-code example belongs in the named file, not directly at the terminal prompt. Follow any shell-specific instructions below.
Keep the same terminal open so earlier variables remain available. Copy one complete command block without the terminal's $ prompt. A backslash continues a command onto the next line. If Bash shows a lone >, it is waiting for the rest of a quote, command or file block. Finish the complete block; if the paste was incomplete, press Ctrl+C and copy it again. Do not paste the next step into an unfinished command.
A line such as RG="training-group" stores a value without printing it. Output captured with $(...) is also stored; a blank screen alone does not prove failure. A sleep deliberately waits, and -o none suppresses normal output. Wait for your normal prompt, then run the step's checkpoint. After an error, stop and keep the exact command and message; later steps cannot repair a missing resource. Never share passwords, access tokens or secret values in a screenshot.
Impersonation gives you a second identity for free
A Role's name and comments do not prove its permissions. kubectl auth can-i <verb> <resource> --as=<identity> -n <namespace> asks the API server whether an impersonated identity is authorized for a particular action. Your current account must have impersonation permission to run this check. It tests authorization for that verb, resource, and namespace; it does not prove how the service account obtains a token or whether the application is otherwise secure. This lab uses native Kubernetes RBAC for a service account, not Entra sign-in or Azure RBAC for a human engineer.
You will create a namespace and service account, then bind a Role that permits reading pods. The can-i checks show whether that identity may perform each named action. After widening the Role on purpose, you will see more actions become available and narrow it again. These checks are one useful part of an access review; also inspect other roles and bindings on a real cluster.
A Role is a claim. can-i is the test.
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.
This lab can reuse the cluster from Your first AKS cluster. If it was deleted, Step 1 recreates it. The AKS control plane is on the Free tier; the node and other Azure resources can still incur charges. The teardown deletes the shared lab cluster, so perform it only when you are finished with other labs that use it, and verify both resource groups are removed.
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 # if Azure says this size "is not allowed in your subscription", the same error lists the sizes # that are; pick a 2-vCPU one from that list (Standard_D2as_v4 worked for us in October 2026) az aks create -n campux-aks -g "$RG" \ --tier free \ --node-count 1 \ --node-vm-size Standard_D2as_v4 \ --enable-managed-identity \ --generate-ssh-keys az aks get-credentials -n campux-aks -g "$RG" kubectl get nodes
kubectl get nodes lists one Ready node. Check the current context with kubectl config current-context. Your identity must be allowed to create Roles and RoleBindings and to impersonate the service account in this exercise. A yes from can-i create rolebindings alone does not prove impersonation permission; the first --as command in Step 2 will test that.A namespace and an unprivileged identity
Create a namespace to scope this exercise and a ServiceAccount with no RoleBinding granting the specific pod and Secret actions tested below. Kubernetes may still allow basic API discovery, so "no permissions at all" would be too broad.
kubectl create namespace demo
kubectl create serviceaccount app-reader -n demo
# test these two actions before binding the Role
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
no for a new service account on this lab cluster. If you receive an impersonation error, your current user lacks the permission required for --as; use an account authorized to impersonate service accounts. If a check returns yes, inspect existing bindings before continuing.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
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
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.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
demo, not only use it itself. 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.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.
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
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.Tear it down
Delete the demo namespace. Delete the shared lab cluster only if you have finished the other labs that use it; leaving it running can continue to incur charges.
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
MC_campux-lab-aks-rg_campux-aks_eastus) are gone.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."