CAMPUX Cloud Bootcamp Lab · Identity & Governance ← All labs
Hands-On Lab · Intermediate
~30 min · Free · Cloud Shell
CLI & Bicep · torn down at the end
Identity & Governance · Lab C

Assign roles to groups, not people.

Twelve engineers need to read the same secrets, and people join and leave every month. Grant the role to each person and access sprawls until nobody can answer "who can read this?" Grant it once to a group, and access becomes a question of membership — auditable, reversible, and the way real organizations run.

Fig. 1 · RBAC through a security group
Resource group · rg-lab-cmembermemberrole assigned onceEngineer 1Engineer 2Security groupgrp-vault-readersKey Vaultone secretmembership is the access control
● Screen walkthrough Not yet recorded · ~6 min
Reel · 00:00 / 06:00

Grant access through a security group, add a member, and watch the permission inherit down the scope.

Placeholder — the page below stands alone until the reel lands
Why

Membership is the access control

In Lab A you assigned a role to a single identity. That is fine for one app. It does not survive a team of twelve with monthly churn: per-person assignments accumulate, offboarding gets missed, and an access review becomes archaeology. The professional pattern is to assign the role once to a security group and then manage people purely through membership. Onboarding is "add to group"; offboarding is "remove from group"; the role assignment never changes. You will prove it by showing a user inherits the role through the group — with nothing assigned to the person at all — and that moving them in and out of the group flips their effective access.

Onboarding is a membership, not a migration.

Before you begin — one-time setup

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

Lab C needs TWO kinds of permission

Azure: Owner or User Access Administrator (to assign roles). Entra directory: a role like Global Administrator, or Groups Administrator + User Administrator (to create groups and users) — this is separate from Azure RBAC. On a personal tenant you have both. On a work tenant you almost certainly can't create users/groups; use a personal test tenant, or every az ad ... command returns Insufficient privileges.

Step 0

Variables

# Windows/Git Bash only: stop it rewriting /subscriptions/... args (harmless on macOS/Linux)
export MSYS_NO_PATHCONV=1
# if you have more than one subscription, make the intended one active
az account set --subscription "$(az account show --query id -o tsv)"

SUFFIX=$RANDOM
RG="campux-lab-groups-rg"
LOCATION="eastus"
KV="campux-kv-$SUFFIX"
SECRET_NAME="ProductDbConnection"
GROUP_NAME="Campux-KeyVault-Readers"
GROUP_NICK="campux-kv-readers-$SUFFIX"

SUB=$(az account show --query id -o tsv)
VAULT_SCOPE="/subscriptions/$SUB/resourceGroups/$RG/providers/Microsoft.KeyVault/vaults/$KV"

# Default verified domain of your tenant (the *.onmicrosoft.com), for test user names.
DOMAIN=$(az rest --method GET --url "https://graph.microsoft.com/v1.0/domains" --query "value[?isDefault].id | [0]" -o tsv)
# A password that meets Entra complexity rules, for the throwaway test users.
USER_PWD="Cx-$(openssl rand -hex 6)-9A!"
echo "Suffix=$SUFFIX  Domain=$DOMAIN"
Checkpoint $DOMAIN prints something like yourtenant.onmicrosoft.com. If it's empty you lack directory read permission — you can't do this lab on this tenant.
Step 1

A vault and a secret (the thing access protects)

az group create --name "$RG" --location "$LOCATION"
az keyvault create --name "$KV" --resource-group "$RG" --location "$LOCATION" --enable-rbac-authorization true

# Let yourself write one secret (RBAC vaults don't grant the creator data access automatically).
# signed-in-user is empty for personal Gmail/Outlook accounts, so fall back to the token's oid:
ME=$(az ad signed-in-user show --query id -o tsv 2>/dev/null)
if [ -z "$ME" ]; then
  PAYLOAD=$(az account get-access-token --query accessToken -o tsv | cut -d. -f2 | tr '_-' '/+')
  case $(( ${#PAYLOAD} % 4 )) in 2) PAYLOAD="$PAYLOAD==";; 3) PAYLOAD="$PAYLOAD=";; esac
  ME=$(printf '%s' "$PAYLOAD" | base64 -d 2>/dev/null | grep -o '"oid":"[^"]*"' | cut -d'"' -f4)
fi
az role assignment create --role "Key Vault Secrets Officer" \
  --assignee-object-id "$ME" --assignee-principal-type User --scope "$VAULT_SCOPE"

sleep 60
az keyvault secret set --vault-name "$KV" --name "$SECRET_NAME" \
  --value "Server=tcp:campux-sql.database.windows.net,1433;Database=products;Authentication=Active Directory Default;"
Checkpoint az keyvault secret show --vault-name "$KV" --name "$SECRET_NAME" --query value -o tsv prints the string. (If Forbidden, wait 60s and re-run the secret set line.)
Step 2

Create the security group

az ad group create --display-name "$GROUP_NAME" --mail-nickname "$GROUP_NICK"
GROUP_ID=$(az ad group show --group "$GROUP_NAME" --query id -o tsv)
echo "Group object id: $GROUP_ID"
Checkpoint $GROUP_ID is a GUID. The group exists and is empty.
Step 3

Assign the role to the GROUP (once)

This is the whole idea: one assignment, to the group, scoped to just this vault.

# A brand-new group takes a few seconds to replicate across the directory, so a
# role assignment fired immediately can fail with PrincipalNotFound. Retry until
# it lands — the same "wait for replication" pattern used for new identities.
for i in 1 2 3 4 5 6; do
  if az role assignment create \
       --role "Key Vault Secrets User" \
       --assignee-object-id "$GROUP_ID" \
       --assignee-principal-type Group \
       --scope "$VAULT_SCOPE"; then
    echo ">>> Role assigned to the group on attempt $i"; break
  fi
  echo "attempt $i: group still replicating (PrincipalNotFound) — waiting 20s"; sleep 20
done
Checkpoint az role assignment list --scope "$VAULT_SCOPE" --assignee-object-id "$GROUP_ID" --query "[].roleDefinitionName" -o tsv prints Key Vault Secrets User. The group can read secrets; nobody's in it yet, so nobody has access — exactly right. (If you saw one or two PrincipalNotFound retries first, that's the new group replicating, not a mistake — --assignee-principal-type Group plus the retry is exactly how you assign a role to a just-created group.)
Step 4

Create two test engineers

AVA_UPN="ava.okoro@$DOMAIN"
BEN_UPN="ben.carter@$DOMAIN"

az ad user create --display-name "Ava Okoro" --user-principal-name "$AVA_UPN" \
  --password "$USER_PWD" --force-change-password-next-sign-in true
az ad user create --display-name "Ben Carter" --user-principal-name "$BEN_UPN" \
  --password "$USER_PWD" --force-change-password-next-sign-in true

AVA_ID=$(az ad user show --id "$AVA_UPN" --query id -o tsv)
BEN_ID=$(az ad user show --id "$BEN_UPN" --query id -o tsv)
echo "Ava=$AVA_ID  Ben=$BEN_ID"

# A brand-new user can take a few seconds to replicate; pause before "member add".
sleep 15
Checkpoint Both $AVA_ID and $BEN_ID are GUIDs. Two throwaway users exist, neither with any access yet.
Step 5

Onboard Ava, and prove she inherited access

az ad group member add --group "$GROUP_ID" --member-id "$AVA_ID"

# The deterministic proof: is she in the group AND does the group hold the role (Step 3)?
az ad group member check --group "$GROUP_ID" --member-id "$AVA_ID" --query value -o tsv

Now see it the way an admin does — the contrast that makes it click. First what's assigned directly to Ava (nothing), then the same query including group inheritance:

echo "Directly assigned to Ava (expect empty):"
az role assignment list --assignee "$AVA_ID" --scope "$VAULT_SCOPE" \
  --query "[].roleDefinitionName" -o tsv

echo "Effective for Ava, including groups (expect: Key Vault Secrets User):"
az role assignment list --assignee "$AVA_ID" --scope "$VAULT_SCOPE" --include-groups \
  --query "[].roleDefinitionName" -o tsv
Checkpoint Membership prints true. The first list is empty; the second shows Key Vault Secrets User. That gap is the whole lesson: nothing is assigned to Ava, yet she has access — through the group. (If the second is empty too, wait 30–60s for transitive replication; some CLI builds also need --all instead of a resource --scope.)
Step 6

Offboard Ava, onboard Ben — the scale story

A month passes. Ava moves teams; Ben joins. Access changes with membership only — zero role-assignment edits.

az ad group member remove --group "$GROUP_ID" --member-id "$AVA_ID"
az ad group member add    --group "$GROUP_ID" --member-id "$BEN_ID"

echo "Ava in group? $(az ad group member check --group "$GROUP_ID" --member-id "$AVA_ID" --query value -o tsv)"
echo "Ben in group? $(az ad group member check --group "$GROUP_ID" --member-id "$BEN_ID" --query value -o tsv)"
echo "Ben effective KV role: $(az role assignment list --assignee "$BEN_ID" --scope "$VAULT_SCOPE" --include-groups --query "[].roleDefinitionName" -o tsv)"
Checkpoint Ava in group → false, Ben → true; Ben's effective role is Key Vault Secrets User, Ava's is empty. Access revoked and granted — and you never touched a role assignment. That is access management at scale.
Down

Tear it down

az ad group member remove --group "$GROUP_ID" --member-id "$BEN_ID" 2>/dev/null
az ad user delete --id "$AVA_UPN"
az ad user delete --id "$BEN_UPN"
az ad group delete --group "$GROUP_ID"
az group delete --name "$RG" --yes
az keyvault purge --name "$KV" --location "$LOCATION"
Checkpoint az ad group list --display-name "$GROUP_NAME" --query "[].id" -o tsv is empty and az group exists --name "$RG" is false — directory and subscription back to zero, no stray test users.
End

What you can now honestly claim

You implemented group-based Azure RBAC: a least-privilege role assigned once to a security group, with access for individuals managed entirely by membership. Keep four things: assign roles to groups, not people; membership is the auditable, reversible access control; --include-groups answers "what can this person actually do?" including everything inherited transitively — the query you run in a real access review; and directory permissions are a different system from Azure RBAC. The Bicep piece shows the part that belongs with your infrastructure — the role assignment to an existing group — and why the group itself, a Graph object, isn't in the template.