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.

A Role's YAML describes intended access; an authorization check tells you what the API server permits. Create a Role and RoleBinding in one namespace, use kubectl auth can-i --as to test named actions for a service account, widen the Role on purpose, and then narrow it. The check tests authorization, not the full sign-in path.

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.
Start

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.

Why

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.

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 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.

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
# 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
Checkpoint 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.
Step 2

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
Checkpoint Both checks should return 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.
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 create new RoleBindings in its own namespace — which means it can hand this exact same access to any other identity in 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.
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

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
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."