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:

  1. Created two Entra ID users and a security group
  2. Assigned scoped RBAC roles (Contributor to group, Reader to user)
  3. Verified access boundaries — what Contributor can and cannot do
  4. Created a Deny policy blocking a specific resource type
  5. 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: --assignee in az role assignment create accepts 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 — Deny effect only blocks future creation/modification. To report existing non-compliance, use Audit effect 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

TrapWhat goes wrongCorrect approach
Using --assignee with UPN in federated tenantsResolves to wrong user or fails silentlyAlways use --assignee-object-id + --assignee-principal-type
Testing policy immediately after assignmentPolicy not yet propagated — creation succeedsWait 5–10 minutes before testing
Contributor trying to remove a lock403 AuthorizationFailedOnly Owner or User Access Admin can manage locks
Deleting RG before removing policy assignmentPolicy conflicts with cleanupAlways remove policy assignments before deleting the scoped resource
Dynamic group empty after creationEntra ID P1 not assigned to usersAssign P1 license to users for dynamic membership to evaluate

Lab Takeaways

  1. Assign RBAC to groups, not individuals — scales better; adding a new admin just means adding to the group.
  2. Object IDs over UPNs in CLI scripts — federated tenants make UPN lookups unreliable.
  3. Policy lag is real in production — always account for 5–10 minutes propagation.
  4. Lock management ≠ resource management — Contributor can create resources but cannot protect or unprotect them with locks.
  5. Cleanup has an order — remove policy assignments first, locks second, then resources, then the RG.