Skip to content
CAMPUX Cloud Bootcamp Lab · Identity & Governance ← All labs
Hands-On Lab · Intermediate
Training resources · Azure charges may apply · Bash
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 · campux-lab-groups-rgmembermemberrole 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
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

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 resource-creation permission plus 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. Check both on your personal training tenant; a new subscription does not prove every directory role is present. On a work tenant you almost certainly cannot create users/groups; use a personal test tenant, or every az ad ... command returns Insufficient privileges.

Step 0

Choose the tenant and name this disposable exercise

Ava and Ben are fictional new starters. You will create two disposable users, give their group access to one vault, then change membership. Use a personal training tenant where you are allowed to create directory objects. Azure role-management permission alone does not create users or groups. Run this block in Bash; it prints the subscription and tenant domain so you can check where the work will happen.

export MSYS_NO_PATHCONV=1
campux_prepare_groups() {
  az account show --query '{subscription:name,tenant:tenantId}' -o table || return 1
  SUFFIX="$(date +%s)$RANDOM"
  RG="campux-groups-$SUFFIX"
  LOCATION="eastus"
  KV="cxkv$SUFFIX"
  SECRET_NAME="TrainingReceipt"
  GROUP_NAME="Campux-Readers-$SUFFIX"
  GROUP_NICK="cxreaders$SUFFIX"
  SUB=$(az account show --query id -o tsv) || return 1
  DOMAIN=$(az rest --method GET --url "https://graph.microsoft.com/v1.0/domains" --query "value[?isDefault].id | [0]" -o tsv) || return 1
  ME=$(az ad signed-in-user show --query id -o tsv) || return 1
  [ -n "$SUB" ] && [ -n "$DOMAIN" ] && [ -n "$ME" ] || { echo "STOP: subscription, domain or user ID is missing."; return 1; }
  RANDOM_BYTES=$(openssl rand -hex 12) || return 1
  USER_PWD="Cx-$RANDOM_BYTES-9A!"
  VAULT_SCOPE="/subscriptions/$SUB/resourceGroups/$RG/providers/Microsoft.KeyVault/vaults/$KV"
  printf 'Ready: group=%s vault=%s domain=%s
' "$RG" "$KV" "$DOMAIN"
}
campux_prepare_groups
Checkpoint Expect the account table, then Ready: with three non-empty names. No password is printed. If a directory query fails, stop and check the signed-in tenant and directory permission. An empty result is not enough evidence to diagnose one particular permission problem. Keep this terminal open.
Step 1

Create the vault and write a harmless receipt

Think of the vault as the locked cupboard. Creating the cupboard does not give its creator permission to read or write what is inside. This step grants your signed-in training user permission to write a sample secret. The sample is not a database credential. Each operation announces itself; if an operation fails, the function stops that step.

campux_create_vault() {
  [ -n "$ME" ] && [ -n "$KV" ] || { echo "STOP: complete Step 0 first."; return 1; }
  echo "Creating the disposable resource group..."
  az group create --name "$RG" --location "$LOCATION" --query provisioningState -o tsv || return 1
  echo "Creating the RBAC vault..."
  az keyvault create --name "$KV" --resource-group "$RG" --location "$LOCATION" --enable-rbac-authorization true --query name -o tsv || return 1
  echo "Granting your training user permission to write the sample..."
  az role assignment create --role "Key Vault Secrets Officer" --assignee-object-id "$ME" --assignee-principal-type User --scope "$VAULT_SCOPE" --query roleDefinitionName -o tsv || return 1
  echo "Waiting 60 seconds before the first write. This is a propagation pause, not a success check."
  sleep 60
  echo "Writing the harmless receipt..."
  az keyvault secret set --vault-name "$KV" --name "$SECRET_NAME" --value "campux-training-only" --query id -o tsv || return 1
  echo "DONE: sample receipt stored."
}
campux_create_vault
Checkpoint Expect a vault name, a secret identifier and DONE:. If the secret write returns Forbidden, check the role assignment, signed-in user and vault network restrictions. Propagation may require another attempt; wait and rerun only the az keyvault secret set command after checking the cause. Do not grant subscription Owner just to make the error disappear.
Step 2

Create the empty readers group

The group is the team badge. It will hold the Azure role once; membership will decide which training users inherit that grant. Creating the group can fail because of Entra directory permissions even when creating the vault succeeded.

campux_create_readers() {
  echo "Creating the readers group..."
  GROUP_ID=$(az ad group create --display-name "$GROUP_NAME" --mail-nickname "$GROUP_NICK" --query id -o tsv) || return 1
  [ -n "$GROUP_ID" ] || { echo "STOP: no group ID returned."; return 1; }
  printf 'Group created: %s
' "$GROUP_ID"
}
campux_create_readers
Checkpoint Expect Group created: followed by a GUID. This command captures the normal JSON output rather than printing it; the final message makes success visible. Insufficient privileges means this step has not completed.
Step 3

Give the group one vault role

This is the one Azure role assignment the team shares. The principal is the group, the role is Key Vault Secrets User, and the scope is this vault. An explicit group principal type avoids asking Azure CLI to discover the new object through another directory lookup. Keep any error; do not label every failure as replication.

campux_grant_group() {
  [ -n "$GROUP_ID" ] || { echo "STOP: complete Step 2 first."; return 1; }
  echo "Assigning the vault reader role to the group..."
  az role assignment create --role "Key Vault Secrets User" --assignee-object-id "$GROUP_ID" --assignee-principal-type Group --scope "$VAULT_SCOPE" --query roleDefinitionName -o tsv || return 1
  echo "Checking the saved group assignment..."
  az role assignment list --scope "$VAULT_SCOPE" --query "[?principalId=='$GROUP_ID'].roleDefinitionName" -o tsv || return 1
}
campux_grant_group
Checkpoint Expect Key Vault Secrets User. PrincipalNotFound may require waiting and retrying this step. AuthorizationFailed needs role-assignment permission at this scope; waiting does not grant it. The assignment proves the group grant exists. It does not yet prove a user can read a secret.
Step 4

Create two disposable training users

Create Ava and Ben only in the training directory you checked. Their unique sign-in names prevent a repeat exercise from changing an existing user. This step stops if either create fails; if Ava exists but Ben failed, keep Ava’s ID for cleanup rather than creating another pair. The temporary password stays in your terminal variable and is not printed.

campux_create_users() {
  AVA_UPN="ava.$SUFFIX@$DOMAIN"
  BEN_UPN="ben.$SUFFIX@$DOMAIN"
  echo "Creating Ava..."
  AVA_ID=$(az ad user create --display-name "Training Ava" --user-principal-name "$AVA_UPN" --password "$USER_PWD" --force-change-password-next-sign-in true --query id -o tsv) || return 1
  echo "Creating Ben..."
  BEN_ID=$(az ad user create --display-name "Training Ben" --user-principal-name "$BEN_UPN" --password "$USER_PWD" --force-change-password-next-sign-in true --query id -o tsv) || return 1
  [ -n "$AVA_ID" ] && [ -n "$BEN_ID" ] || { echo "STOP: a user ID is missing."; return 1; }
  printf 'Created: Ava=%s Ben=%s
' "$AVA_ID" "$BEN_ID"
  echo "Waiting 15 seconds before membership changes..."
  sleep 15
  echo "Pause finished."
}
campux_create_users
Checkpoint Expect Created: with two GUIDs, then the announced pause and Pause finished. A create error means this step has not completed. Check directory permissions or the exact password/domain error; do not continue with an empty ID.
Step 5

Add Ava and inspect the group-derived grant

Adding Ava is quiet on success, so this block announces the operation and then prints the membership check. The role-list comparison shows why you do not need a separate assignment for every colleague. It checks configuration; it does not authenticate as Ava and read the sample secret.

campux_onboard_ava() {
  [ -n "$AVA_ID" ] && [ -n "$GROUP_ID" ] || { echo "STOP: user or group ID is missing."; return 1; }
  echo "Adding Ava to the readers group..."
  az ad group member add --group "$GROUP_ID" --member-id "$AVA_ID" || return 1
  echo "Ava in group? Expect true:"
  az ad group member check --group "$GROUP_ID" --member-id "$AVA_ID" --query value -o tsv || return 1
  echo "Direct vault assignments for Ava? Expect []:"
  az role assignment list --assignee "$AVA_ID" --scope "$VAULT_SCOPE" --query '[].roleDefinitionName' -o json || return 1
  echo "Including groups? Expect Key Vault Secrets User in the list:"
  az role assignment list --assignee "$AVA_ID" --scope "$VAULT_SCOPE" --include-groups --query '[].roleDefinitionName' -o json || return 1
}
campux_onboard_ava
Checkpoint Expect true, an empty direct-assignment list, then a list including Key Vault Secrets User. If the last list is empty, allow directory propagation (about two and a half minutes in our October 2026 run), then rerun the read-only query. Other existing group grants can change the result. A real access review also includes parent-scope assignments and deny assignments; this comparison deliberately focuses on this vault grant.
Step 6

Move the team membership from Ava to Ben

Ava changes teams and Ben takes over. You change the members, not the Azure role assignment. This is the scale benefit: one grant to review, many colleagues managed through membership.

campux_switch_members() {
  [ -n "$AVA_ID" ] && [ -n "$BEN_ID" ] && [ -n "$GROUP_ID" ] || { echo "STOP: training IDs are missing."; return 1; }
  echo "Removing Ava from the readers group..."
  az ad group member remove --group "$GROUP_ID" --member-id "$AVA_ID" || return 1
  echo "Adding Ben to the readers group..."
  az ad group member add --group "$GROUP_ID" --member-id "$BEN_ID" || return 1
  echo "Ava in group? Expect false:"
  az ad group member check --group "$GROUP_ID" --member-id "$AVA_ID" --query value -o tsv || return 1
  echo "Ben in group? Expect true:"
  az ad group member check --group "$GROUP_ID" --member-id "$BEN_ID" --query value -o tsv || return 1
  echo "Ben's group-derived vault assignments:"
  az role assignment list --assignee "$BEN_ID" --scope "$VAULT_SCOPE" --include-groups --query '[].roleDefinitionName' -o json || return 1
}
campux_switch_members
Checkpoint Expect Ava false, Ben true, then Ben’s list including Key Vault Secrets User after propagation. Existing grants can still permit Ava’s access. Permission caches and token refresh also matter; membership removal is not proof that an already-running user session immediately loses data access.
Down

Delete the users, group and vault resources

Delete only this exercise’s unique objects. The messages below tell you which deletion is running; resource-group deletion can take time. If you stopped earlier, an object might never have been created. Check each exact name in the portal and remove the objects that do exist. Do not use empty variables or delete an unrelated group to recover from an error.

campux_cleanup_groups() {
  [ -n "$AVA_UPN" ] && [ -n "$BEN_UPN" ] && [ -n "$GROUP_ID" ] && [ -n "$RG" ] || { echo "STOP: recover the exercise names before cleanup."; return 1; }
  echo "Deleting training Ava..."
  az ad user delete --id "$AVA_UPN" || return 1
  echo "Deleting training Ben..."
  az ad user delete --id "$BEN_UPN" || return 1
  echo "Deleting the training readers group..."
  az ad group delete --group "$GROUP_ID" || return 1
  echo "Deleting the disposable resource group; wait for the prompt..."
  az group delete --name "$RG" --yes || return 1
  echo "Training resource group still exists? Expect false:"
  az group exists --name "$RG" || return 1
  echo "Purging the soft-deleted vault so its name is released..."
  az keyvault purge --name "$KV" --location "$LOCATION" || return 1
  echo "Matching readers group? Expect []:"
  az ad group list --display-name "$GROUP_NAME" --query '[].id' -o json || return 1
}
campux_cleanup_groups
Checkpoint Expect false and [], and confirm both user deletions succeeded. The deleted vault may remain soft-deleted. Purge is a separate permission and cannot bypass enabled purge protection; use the portal to inspect retained vaults rather than assuming the directory and subscription are empty.
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 includes group-derived role assignments; also review parent-scope grants with --include-inherited, applicable deny assignments and the actual data operation before declaring effective access; 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, is not in the template.