Lab 01: RBAC, Policies, and Resource Locks
Estimated time: 45–60 minutes Difficulty: ⭐⭐☆☆☆ Environment: Azure free account — portal + Azure CLI
Prerequisites
- Azure free account with an active subscription
- Azure CLI ≥ 2.50 (
az --version) - Logged in:
az login
Set your default subscription:
az account set --subscription "<your-subscription-id>"
az account show --query "{name:name, id:id}" -o table
Lab Objectives
By the end you will have:
- Created two Entra ID users and a security group
- Assigned scoped RBAC roles (Contributor to group, Reader to user)
- Verified access boundaries — what Contributor can and cannot do
- Created a Deny policy blocking a specific resource type
- Applied a CanNotDelete lock and confirmed Contributor cannot remove it
Step 1: Create the Lab Resource Group
az group create \
--name rg-az104-lab01 \
--location eastus
echo "Resource group created."
All resources in this lab are scoped to this RG. This contains blast radius.
Step 2: Create Two Entra ID Users
Requires User Administrator or Global Administrator in Entra ID. On a personal free Azure account you typically have Global Admin.
# Discover your tenant's default domain
DOMAIN=$(az rest \
--method GET \
--url "https://graph.microsoft.com/v1.0/organization" \
--query "value[0].verifiedDomains[?isDefault].name | [0]" \
--output tsv)
echo "Tenant domain: $DOMAIN"
# Create Alice (will get Contributor via group)
az ad user create \
--display-name "Alice Storage" \
--user-principal-name "alice@$DOMAIN" \
--password "P@ssw0rd!Azure104" \
--force-change-password-next-sign-in false
# Create Bob (will get Reader directly)
az ad user create \
--display-name "Bob ReadOnly" \
--user-principal-name "bob@$DOMAIN" \
--password "P@ssw0rd!Azure104" \
--force-change-password-next-sign-in false
# Capture Object IDs — required for all subsequent operations
ALICE_ID=$(az ad user show --id "alice@$DOMAIN" --query id --output tsv)
BOB_ID=$(az ad user show --id "bob@$DOMAIN" --query id --output tsv)
echo "Alice OID: $ALICE_ID"
echo "Bob OID: $BOB_ID"
⚠️ Tricky spot:
--assigneeinaz role assignment createaccepts UPN, Object ID, or display name — but in federated tenants (AAD Connect, ADFS), UPN-based lookup can fail silently or resolve to the wrong user. Always use Object ID in production and exam automation scripts.
Step 3: Create a Security Group and Add Alice
# Create the group
GROUP_ID=$(az ad group create \
--display-name "Storage-Admins" \
--mail-nickname "Storage-Admins" \
--query id --output tsv)
echo "Group OID: $GROUP_ID"
# Add Alice to the group
az ad group member add \
--group "Storage-Admins" \
--member-id $ALICE_ID
# Verify
az ad group member list --group "Storage-Admins" --query "[].displayName" -o tsv
Step 4: Assign RBAC Roles
RG_ID=$(az group show --name rg-az104-lab01 --query id --output tsv)
# Contributor to the Storage-Admins group (scoped to RG)
az role assignment create \
--assignee-object-id $GROUP_ID \
--assignee-principal-type Group \
--role "Contributor" \
--scope $RG_ID
# Reader to Bob directly (scoped to RG)
az role assignment create \
--assignee-object-id $BOB_ID \
--assignee-principal-type User \
--role "Reader" \
--scope $RG_ID
# Verify both assignments
az role assignment list \
--resource-group rg-az104-lab01 \
--query "[].{Principal:principalName, Role:roleDefinitionName, Scope:scope}" \
-o table
Why use
--assignee-principal-type? It skips an extra Graph API lookup and prevents errors in tenants with strict Graph API permissions. Always use it when you have the Object ID.
Step 5: Test What Contributor Can and Cannot Do
Create a storage account as Alice's action (you're still the admin here, but simulate the outcome):
# Create a storage account — a Contributor CAN do this
SA_NAME="staz104$(openssl rand -hex 4)"
az storage account create \
--name $SA_NAME \
--resource-group rg-az104-lab01 \
--location eastus \
--sku Standard_LRS
echo "Storage account: $SA_NAME"
Now test what Contributor cannot do — try to create a role assignment:
# This SHOULD fail if run as Alice (Contributor)
# Running as admin here to demonstrate the concept
az role assignment create \
--assignee-object-id $BOB_ID \
--assignee-principal-type User \
--role "Reader" \
--scope $RG_ID
# → AuthorizationFailed: Contributor does not have Microsoft.Authorization/roleAssignments/write
Takeaway: Contributor sees and manages resources, but role management is reserved for Owner and User Access Administrator. The exam will test this boundary multiple times.
Step 6: Create a Custom Deny Policy
Create a policy that denies creation of public IP addresses in this resource group — simulating a "no public internet exposure" governance rule.
SUB_ID=$(az account show --query id --output tsv)
# Define the policy
az policy definition create \
--name "deny-public-ip" \
--display-name "Deny Public IP Creation" \
--description "Blocks creation of Public IP resources" \
--rules '{
"if": {
"field": "type",
"equals": "Microsoft.Network/publicIPAddresses"
},
"then": {
"effect": "deny"
}
}' \
--mode All \
--subscription $SUB_ID
# Assign the policy to the resource group
az policy assignment create \
--name "deny-public-ip-rg" \
--display-name "Deny Public IP in Lab RG" \
--policy "deny-public-ip" \
--scope $RG_ID
Test the policy — this should fail:
# Wait 2-3 minutes for policy to propagate, then test
az network public-ip create \
--name pip-blocked \
--resource-group rg-az104-lab01 \
--location eastus \
--sku Standard
# Expected error: RequestDisallowedByPolicy
# Error code: PolicyViolation
⚠️ Tricky spot: Policy assignments have a 5–10 minute propagation delay in real Azure tenants. If the creation succeeds immediately, wait a few minutes and retry. In the exam's live lab environment the propagation is faster.
⚠️ Tricky spot: If you have an existing public IP resource already in the RG (created before the policy), the policy does NOT delete it —
Denyeffect only blocks future creation/modification. To report existing non-compliance, useAuditeffect or check compliance under the policy assignment.
Step 7: Apply a Resource Lock
# Apply CanNotDelete lock on the resource group
az lock create \
--name "protect-lab01" \
--resource-group rg-az104-lab01 \
--lock-type CanNotDelete \
--notes "Exam lab protection lock"
# Verify
az lock list --resource-group rg-az104-lab01 -o table
Test the lock — try to delete the RG (will fail):
az group delete --name rg-az104-lab01 --yes
# Expected: ScopeLocked — cannot delete a resource group with a CanNotDelete lock
Now test that Contributor CANNOT remove the lock:
Even though Alice has Contributor on the RG, she cannot delete the lock:
# As Alice (Contributor), this would fail:
az lock delete \
--name "protect-lab01" \
--resource-group rg-az104-lab01
# Expected: AuthorizationFailed — Contributor lacks Microsoft.Authorization/locks/delete
Only Owner or User Access Admin can delete the lock:
# As admin (Owner), this succeeds:
az lock delete \
--name "protect-lab01" \
--resource-group rg-az104-lab01
Step 8: Clean Up
Cleanup order matters — remove assignments and policies before deleting the RG.
# 1. Remove policy assignment (policy blocks cleanup otherwise)
az policy assignment delete \
--name "deny-public-ip-rg" \
--scope $RG_ID
# 2. Delete the resource group (no lock now, so this works)
az group delete --name rg-az104-lab01 --yes --no-wait
# 3. Remove Entra ID users and group
az ad user delete --id $ALICE_ID
az ad user delete --id $BOB_ID
az ad group delete --group "Storage-Admins"
# 4. Remove the policy definition (optional — clean namespace)
az policy definition delete --name "deny-public-ip" --subscription $SUB_ID
Lab Tricky Spots Summary
| Trap | What goes wrong | Correct approach |
|---|---|---|
Using --assignee with UPN in federated tenants | Resolves to wrong user or fails silently | Always use --assignee-object-id + --assignee-principal-type |
| Testing policy immediately after assignment | Policy not yet propagated — creation succeeds | Wait 5–10 minutes before testing |
| Contributor trying to remove a lock | 403 AuthorizationFailed | Only Owner or User Access Admin can manage locks |
| Deleting RG before removing policy assignment | Policy conflicts with cleanup | Always remove policy assignments before deleting the scoped resource |
| Dynamic group empty after creation | Entra ID P1 not assigned to users | Assign P1 license to users for dynamic membership to evaluate |
Lab Takeaways
- Assign RBAC to groups, not individuals — scales better; adding a new admin just means adding to the group.
- Object IDs over UPNs in CLI scripts — federated tenants make UPN lookups unreliable.
- Policy lag is real in production — always account for 5–10 minutes propagation.
- Lock management ≠ resource management — Contributor can create resources but cannot protect or unprotect them with locks.
- Cleanup has an order — remove policy assignments first, locks second, then resources, then the RG.