Build a Least-Privilege RBAC Lab
Deploy a managed identity with Reader access scoped to an Azure resource group.
Introduction
30 Second Summary
When access reaches farther than a task needs, one small mistake can expose far more than expected. The safest permissions are easy to inspect before anyone relies on them.
In this project, you will use Bicep to give a user-assigned managed identity Reader access to one Microsoft Azure resource group. You will use deployment what-if to catch a subscription-wide Azure RBAC assignment before it reaches Azure.
What You'll Build
A single Azure CLI check shows one reusable identity with Reader access ending exactly at its target resource group.
By the end of this project, you'll have:
- A visible identity boundary where Azure CLI shows id-az104-reader separately from the target resources it can inspect.
- Clear what-if evidence that reveals why subscription scope reaches beyond the workload boundary.
- A portfolio-ready GitHub project in 02-identity-rbac that explains the verified design. Its NEXT-PROJECT-BRIEF.md file carries your environment into the Project 03 planning conversation.
- Secret Mission: Reuse the same identity across a second target resource group while keeping both Reader assignments narrow.
Are there any prerequisites?
You need Project 01 or an equivalent Azure governance lab plus Owner access on the training subscription. The lab is expected to stay at $0 because it creates no compute, storage, or paid service instance.
Before We Start
This first checkpoint fixes the intended access boundary before any hands-on work begins. The user-assigned managed identity id-az104-reader receives the built-in Reader role only at the rg-az104-identity-target resource-group scope.
Prepare the Project Folder
A consistent numbered folder keeps this lab aligned with the portfolio you started in Project 01. The continuation brief preserves your environment for future planning conversations.
The same brief records your permissions plus your design preferences. In this step, you will prepare that local workspace before any Azure resources are created.
In this step, get ready to:
- Create the numbered project workspace with Windows PowerShell.
- Select the training subscription with Azure CLI. Confirm that Bicep is available.
- Create the continuation brief for future project handoffs.
Create the numbered project folder
Windows PowerShell gives you a repeatable way to create the exact numbered structure. Visual Studio Code makes that structure visible when you add the handoff file.
- Press the Windows key to open Windows search.
- Type Windows PowerShell into the search field.
- Press Enter to open Windows PowerShell.
- Move to the folder that contains your numbered AZ-104 projects by running this command:
Set-Location -Path [[AZ104_REPOSITORY_PATH="C:\path\to\your\AZ-104-repository"]]
What does this command do?
The Set-Location command moves PowerShell to your existing AZ-104 repository. Replace the path placeholder with the location of that repository on your Windows PC.
- Create the Project 02 folder structure by running these commands:
New-Item -Path ".\02-identity-rbac\modules" -ItemType "Directory" -Force
Set-Location -Path ".\02-identity-rbac"
What do these commands create?
- The New-Item command creates 02-identity-rbac with a modules directory inside it.
- The Set-Location command moves PowerShell into 02-identity-rbac for the rest of the project.
PowerShell prints a directory entry for modules. Its prompt now ends with 02-identity-rbac.
Folder in the wrong location?
- Check that the path in the first command points to the repository containing Project 01.
- Check that your current PowerShell prompt ends with 02-identity-rbac.
Help me troubleshoot this Project 02 folder setup.
Connect Azure CLI to the training subscription
Azure CLI keeps subsequent deployments tied to one active subscription. The Bicep version check confirms that the template tooling is ready before you start writing infrastructure.
What happens during sign-in?
Interactive sign-in uses Microsoft's authentication flow. Follow the account prompt that opens on your Windows PC.
Your credentials stay in the sign-in flow. You do not paste them into PowerShell.
- Sign in to Azure CLI by running this command:
az login
What does this command do?
The az login command starts an interactive Azure sign-in. PowerShell displays the subscriptions available to your account after authentication completes.
- List the subscriptions available to your account by running this command:
az account list --output table
Why list the subscriptions?
The table helps you identify the training subscription before selecting it. This prevents later deployments from landing in another subscription.
- Find the training subscription name or ID in the table.
- Select the training subscription by replacing the placeholder in this command:
az account set -s "<training-subscription-name-or-id>"
What changes here?
The az account set command changes the active subscription for later Azure CLI commands. It does not create or modify any Azure resources.
- Confirm the active subscription plus the Bicep installation by running these commands:
az account show --output table
az bicep version
What do these checks prove?
- The account table identifies the subscription that Azure CLI currently targets.
- The Bicep command prints the installed Bicep CLI version.
That clears the setup risk. PowerShell now targets the training subscription with Bicep ready for the next step.
Azure CLI check not working?
- Repeat the interactive sign-in if your Azure CLI session has expired.
- Check the subscription table for the exact training subscription name or ID.
- Confirm that Azure CLI can access Bicep from the same PowerShell window.
Help me troubleshoot the Azure CLI or Bicep readiness check.
Create the continuation brief
The continuation brief gives later planning conversations a stable record of this portfolio. Git can then detect the brief as a new project file before you commit it.
- Press the Windows key to open Windows search.
- Type Visual Studio Code into the search field.
- Press Enter to open Visual Studio Code.
- Click File in the top menu.
- Click Open Folder.
- Select 02-identity-rbac inside your AZ-104 repository.
- Click Select Folder.
You should see 02-identity-rbac in the Explorer sidebar. The modules directory appears beneath it.
- Click the New File icon in the Explorer sidebar.
- Type NEXT-PROJECT-BRIEF.md as the file name.
- Press Enter to create the file.
- Add the brief's purpose plus the learner environment by pasting this first section:
# AZ-104 Portfolio Continuation Brief
## How to use this file
Upload this file in a new planning conversation and send:
> Let's do Project 03. Use the attached AZ-104 Portfolio Continuation Brief as the source of truth and maintain the same project format.
Change `03` to the next project number you want.
## Learner environment
- Device: Windows PC
- Azure access: Owner on the training subscription
- Ready tools: Azure, VS Code, Azure CLI, Bicep, Git, GitHub
- Repository structure: One AZ-104 repository with numbered project folders
What does this section capture?
- The opening prompt tells a future planning conversation how to continue with Project 03.
- The environment section records the machine, access level, tools, plus repository structure.
- Save NEXT-PROJECT-BRIEF.md by pressing Ctrl+S.
- Confirm that the file starts with AZ-104 Portfolio Continuation Brief.
First section missing?
- Check that the active editor tab is named NEXT-PROJECT-BRIEF.md.
- Check that the file sits directly inside 02-identity-rbac.
Help me fix the first section of the continuation brief.
- Add the portfolio rules beneath the learner environment by pasting this section:
## Format to preserve
- Build one focused AZ-104 portfolio project at a time.
- Keep the project under 60 minutes and at no cost where possible.
- Use Bicep-first infrastructure and Azure CLI commands for Windows PowerShell.
- Put the project in its own numbered folder.
- Include a visible early win, a designed obstacle, what-if validation, deployment verification, cleanup, a README, and a GitHub push.
- Explain the problem each Azure feature solves instead of presenting a configuration checklist.
- Verify current commands, API versions, permissions, and pricing against official Microsoft documentation.
Why preserve these rules?
These rules keep later projects focused on one AZ-104 skill. They also preserve the same automation, verification, cleanup, plus documentation rhythm.
- Save NEXT-PROJECT-BRIEF.md by pressing Ctrl+S.
- Confirm that the file now includes the Format to preserve heading.
Formatting looks uneven?
- Check that every portfolio rule begins with one hyphen.
- Check that one blank line separates the heading from its list.
Help me repair the Markdown formatting in the portfolio rules.
- Finish the brief by pasting the project record beneath the format rules:
## Completed projects
- [x] Project 01: Subscription Governance Guardrails
- Built a sandbox resource group, governance policy assignment, required tags, and cost controls with Bicep and Azure CLI.
- [ ] Project 02: Build a Least-Privilege Azure RBAC Lab
- Created a user-assigned managed identity.
- Used what-if to catch a subscription-scoped Reader assignment before deployment.
- Limited Reader to `rg-az104-identity-target` through a scoped Bicep module.
- Verified the final role and scope with Azure CLI.
## Project 02 repository evidence
- Folder: `02-identity-rbac`
- Main template: `main.bicep`
- Modules: `modules/identity.bicep`, `modules/rbac.bicep`
- Identity: `id-az104-reader`
- Control scope: `rg-az104-identity-control`
- Target scope: `rg-az104-identity-target`
- Final access: Reader at the target resource group only
## Direction for the next project
Choose the next logical AZ-104 skill that builds on governance and identity without repeating them. Prefer a free or free-tier Azure implementation, Bicep automation, Azure CLI verification, cleanup instructions, and a portfolio-ready GitHub result.
Why is Project 02 unchecked?
The unchecked Project 02 entry records the intended evidence without claiming completion. You will mark it complete only after the deployment evidence is documented plus pushed to GitHub.
- Save NEXT-PROJECT-BRIEF.md by pressing Ctrl+S.
- Confirm that Project 01 is checked.
- Confirm that Project 02 remains unchecked.
Checkbox state incorrect?
- Restore the checked marker beside Project 01 exactly as shown in the reference.
- Restore the empty marker beside Project 02 exactly as shown in the reference.
Help me correct the project completion markers in the continuation brief.
✔️ Awesome, I've got everything!
Your continuation brief is saved. Project 02 remains unchecked until the packaging step.
ⓧ I'd like to double check the full code
# AZ-104 Portfolio Continuation Brief
## How to use this file
Upload this file in a new planning conversation and send:
> Let's do Project 03. Use the attached AZ-104 Portfolio Continuation Brief as the source of truth and maintain the same project format.
Change `03` to the next project number you want.
## Learner environment
- Device: Windows PC
- Azure access: Owner on the training subscription
- Ready tools: Azure, VS Code, Azure CLI, Bicep, Git, GitHub
- Repository structure: One AZ-104 repository with numbered project folders
## Format to preserve
- Build one focused AZ-104 portfolio project at a time.
- Keep the project under 60 minutes and at no cost where possible.
- Use Bicep-first infrastructure and Azure CLI commands for Windows PowerShell.
- Put the project in its own numbered folder.
- Include a visible early win, a designed obstacle, what-if validation, deployment verification, cleanup, a README, and a GitHub push.
- Explain the problem each Azure feature solves instead of presenting a configuration checklist.
- Verify current commands, API versions, permissions, and pricing against official Microsoft documentation.
## Completed projects
- [x] Project 01: Subscription Governance Guardrails
- Built a sandbox resource group, governance policy assignment, required tags, and cost controls with Bicep and Azure CLI.
- [ ] Project 02: Build a Least-Privilege Azure RBAC Lab
- Created a user-assigned managed identity.
- Used what-if to catch a subscription-scoped Reader assignment before deployment.
- Limited Reader to `rg-az104-identity-target` through a scoped Bicep module.
- Verified the final role and scope with Azure CLI.
## Project 02 repository evidence
- Folder: `02-identity-rbac`
- Main template: `main.bicep`
- Modules: `modules/identity.bicep`, `modules/rbac.bicep`
- Identity: `id-az104-reader`
- Control scope: `rg-az104-identity-control`
- Target scope: `rg-az104-identity-target`
- Final access: Reader at the target resource group only
## Direction for the next project
Choose the next logical AZ-104 skill that builds on governance and identity without repeating them. Prefer a free or free-tier Azure implementation, Bicep automation, Azure CLI verification, cleanup instructions, and a portfolio-ready GitHub result.
Before you run the final check, which subscription name do you expect? Which local file should Git flag as new?
- Switch back to the Windows PowerShell window from earlier.
- Verify the active subscription plus the new project content by running these commands:
az account show --output table
git status
What does the final check prove?
- The Azure account table confirms that the training subscription remains active.
- Git reports the new Project 02 content as untracked.
You should see the training subscription in the account table. Git should list the new Project 02 content under its untracked files.
New file missing from Git status?
- Check that PowerShell is still inside 02-identity-rbac.
- Check that NEXT-PROJECT-BRIEF.md is saved inside the same folder.
- Check that the AZ-104 repository contains its existing Git metadata.
Help me diagnose why Git cannot see the new Project 02 file.
Your project workspace now has a confirmed Azure context plus a reusable handoff file. Next, you will deploy the resource boundaries and the managed identity.
Deploy the Identity Boundary
Your project folder now gives this lab a stable home. You will use Visual Studio Code to turn the access design into visible Azure boundaries.
Permissions need a principal before they can be assigned safely. This step uses Bicep to deploy that principal inside a control resource group while keeping its future workload boundary separate.
In this step, get ready to:
- Keep the continuation brief limited to work you have completed.
- Define two resource groups and one user-assigned managed identity.
- Preview the foundation deployment before creating the identity boundary.
Keep the continuation brief current
A continuation brief should separate completed evidence from planned work. Project 02 remains unchecked until the packaging step because its role assignment has not been built yet.
- Press the Windows key to open Windows search.
- Type Visual Studio Code into the search field.
- Press Enter to open Visual Studio Code.
- Click File in the top menu.
- Select Open Folder.
- Select the existing 02-identity-rbac folder from your repository.
- Confirm the folder selection.
You should see NEXT-PROJECT-BRIEF.md beside the modules folder in the file list.
- Select NEXT-PROJECT-BRIEF.md from the file list.
- Find this unchecked Project 02 entry with its premature completion details:
- [ ] Project 02: Build a Least-Privilege Azure RBAC Lab
- Created a user-assigned managed identity.
- Used what-if to catch a subscription-scoped Reader assignment before deployment.
- Limited Reader to `rg-az104-identity-target` through a scoped Bicep module.
- Verified the final role and scope with Azure CLI.
Why remove these details?
These nested lines describe evidence from later steps. Removing them keeps the unchecked entry aligned with the current project state.
- Delete the four indented evidence lines.
- Keep the unchecked project line in this form:
- [ ] Project 02: Build a Least-Privilege Azure RBAC Lab
What remains?
The unchecked line preserves Project 02 in the sequence. Step 5 adds the evidence after the deployment has been proven.
- Find this entire future-evidence section:
## Project 02 repository evidence
- Folder: `02-identity-rbac`
- Main template: `main.bicep`
- Modules: `modules/identity.bicep`, `modules/rbac.bicep`
- Identity: `id-az104-reader`
- Control scope: `rg-az104-identity-control`
- Target scope: `rg-az104-identity-target`
- Final access: Reader at the target resource group only
Why is this section premature?
The section claims that the final RBAC module exists. It also claims that the Reader assignment has been verified.
- Delete the complete Project 02 repository evidence section.
- Confirm the end of the completed-project list now flows into this heading:
- [ ] Project 02: Build a Least-Privilege Azure RBAC Lab
## Direction for the next project
What does this edit preserve?
The brief keeps the reusable Project 03 direction. It leaves room for verified Project 02 evidence during packaging.
- Save NEXT-PROJECT-BRIEF.md.
- Return to Windows PowerShell.
- Check that Git still sees the project changes by running:
git status
What does this check prove?
The status output confirms that the updated continuation brief remains part of your local project changes. No Azure resources are affected by this edit.
You should see the 02-identity-rbac project changes waiting to be committed.
Does the brief still show future evidence?
- Confirm that Project 02 still uses an empty checkbox.
- Remove every indented line beneath the Project 02 entry.
- Remove the complete repository-evidence section.
Ask for help reviewing the brief if the sections do not match. Help me compare my NEXT-PROJECT-BRIEF.md with the required Step 2 state.
✔️ Awesome, I've got everything!
Great. Save the brief before moving to the Bicep files.
ⓧ I'd like to double check the full code
# AZ-104 Portfolio Continuation Brief
## How to use this file
Upload this file in a new planning conversation and send:
> Let's do Project 03. Use the attached AZ-104 Portfolio Continuation Brief as the source of truth and maintain the same project format.
Change `03` to the next project number you want.
## Learner environment
- Device: Windows PC
- Azure access: Owner on the training subscription
- Ready tools: Azure, VS Code, Azure CLI, Bicep, Git, GitHub
- Repository structure: One AZ-104 repository with numbered project folders
## Format to preserve
- Build one focused AZ-104 portfolio project at a time.
- Keep the project under 60 minutes and at no cost where possible.
- Use Bicep-first infrastructure and Azure CLI commands for Windows PowerShell.
- Put the project in its own numbered folder.
- Include a visible early win, a designed obstacle, what-if validation, deployment verification, cleanup, a README, and a GitHub push.
- Explain the problem each Azure feature solves instead of presenting a configuration checklist.
- Verify current commands, API versions, permissions, and pricing against official Microsoft documentation.
## Completed projects
- [x] Project 01: Subscription Governance Guardrails
- Built a sandbox resource group, governance policy assignment, required tags, and cost controls with Bicep and Azure CLI.
- [ ] Project 02: Build a Least-Privilege Azure RBAC Lab
## Direction for the next project
Choose the next logical AZ-104 skill that builds on governance and identity without repeating them. Prefer a free or free-tier Azure implementation, Bicep automation, Azure CLI verification, cleanup instructions, and a portfolio-ready GitHub result.
What should match?
Compare every heading and list item with your saved file. Project 02 should remain unchecked without completion evidence beneath it.
Define the identity boundary in Bicep
A user-assigned managed identity is a reusable Azure security principal. It begins without any Azure RBAC role assignments.
Why separate the control and target?
The control resource group holds the identity. The target resource group marks the narrow boundary where Reader access belongs later.
This separation makes subscription-wide access easy to compare with resource-group access. The boundary becomes visible before any permission is granted.
- Select the modules folder in the Explorer view.
- Select New File....
- Enter identity.bicep as the file name.
- Add the managed identity definition by pasting:
param identityName string
param location string
param tags object = {}
resource managedIdentity 'Microsoft.ManagedIdentity/userAssignedIdentities@2024-11-30' = {
name: identityName
location: location
tags: tags
}
output principalId string = managedIdentity.properties.principalId
output clientId string = managedIdentity.properties.clientId
output resourceId string = managedIdentity.id
What does this module do?
- The three parameters let the subscription template supply the identity name and deployment location. The tags remain reusable.
- The managedIdentity resource creates the user-assigned identity with the stable 2024-11-30 API version.
- The outputs expose the generated principal ID and client ID. The resource ID identifies the Azure resource itself.
- Save modules/identity.bicep.
- Confirm the file appears inside the modules folder.
- Confirm the editor shows no red error underline in the module.
Does the identity module show an error?
- Check that the file is named identity.bicep.
- Check that the API version is 2024-11-30.
- Check that all three outputs reference managedIdentity.
Help me troubleshoot my identity module.
✔️ Awesome, I've got everything!
Your identity module is ready for the subscription template.
ⓧ I'd like to double check the full code
param identityName string
param location string
param tags object = {}
resource managedIdentity 'Microsoft.ManagedIdentity/userAssignedIdentities@2024-11-30' = {
name: identityName
location: location
tags: tags
}
output principalId string = managedIdentity.properties.principalId
output clientId string = managedIdentity.properties.clientId
output resourceId string = managedIdentity.id
What should match?
Your module should contain three parameters and one managed identity resource. It should finish with the three outputs shown here.
- Select the top-level 02-identity-rbac folder in the Explorer view.
- Select New File....
- Enter main.bicep as the file name.
- Define the subscription scope and control boundary by pasting:
targetScope = 'subscription'
param location string = deployment().location
var controlResourceGroupName = 'rg-az104-identity-control'
var targetResourceGroupName = 'rg-az104-identity-target'
var identityName = 'id-az104-reader'
var commonTags = {
Environment: 'Training'
Project: 'AZ-104-02'
ManagedBy: 'Bicep'
}
resource controlResourceGroup 'Microsoft.Resources/resourceGroups@2025-04-01' = {
name: controlResourceGroupName
location: location
tags: commonTags
}
What does this foundation define?
- The subscription target allows one template to create resource groups.
- The variables give each boundary a stable name. The identity receives its own stable name.
- The shared tags identify the environment and project. They also record Bicep as the management method.
- The first resource creates the control resource group. That group becomes the identity's home.
- Save main.bicep.
- Confirm the editor shows no red error underline in the control resource group block.
Does the control boundary show an error?
- Check that targetScope is set to subscription.
- Check every property in commonTags for matching quotes.
- Check that the resource group API version is 2025-04-01.
Help me troubleshoot the control boundary.
- Place the cursor below the controlResourceGroup resource.
- Add the separate target boundary by pasting:
resource targetResourceGroup 'Microsoft.Resources/resourceGroups@2025-04-01' = {
name: targetResourceGroupName
location: location
tags: commonTags
}
What does the target boundary represent?
The target resource group represents the smallest workload boundary in this lab. It contains no workload resources during this step.
- Save main.bicep.
- Confirm both resource group blocks use the same location and tags.
Does the target group show an error?
- Check that the symbolic name is targetResourceGroup.
- Check that its name uses targetResourceGroupName.
- Check that the closing brace appears once after the tags property.
Help me fix the target resource group block.
- Place the cursor below the targetResourceGroup resource.
- Connect the identity module to the control boundary by pasting:
module identity './modules/identity.bicep' = {
name: 'deploy-project02-identity'
scope: controlResourceGroup
params: {
identityName: identityName
location: location
tags: commonTags
}
}
How does the module connection work?
- The local path points to the identity module with a forward slash.
- The module scope places the identity inside controlResourceGroup.
- The parameters pass the stable identity name and subscription deployment location. They also pass the shared tags.
- Save main.bicep.
- Confirm the module path resolves without a red error underline.
- Confirm the template contains no role definition or role assignment resource.
Does the module path show an error?
- Check that identity.bicep is inside the modules folder.
- Check that the module path is ./modules/identity.bicep.
- Use a forward slash in the module path.
Help me resolve the identity module path.
✔️ Awesome, I've got everything!
Your subscription template now defines two boundaries and one identity module.
ⓧ I'd like to double check the full code
targetScope = 'subscription'
param location string = deployment().location
var controlResourceGroupName = 'rg-az104-identity-control'
var targetResourceGroupName = 'rg-az104-identity-target'
var identityName = 'id-az104-reader'
var commonTags = {
Environment: 'Training'
Project: 'AZ-104-02'
ManagedBy: 'Bicep'
}
resource controlResourceGroup 'Microsoft.Resources/resourceGroups@2025-04-01' = {
name: controlResourceGroupName
location: location
tags: commonTags
}
resource targetResourceGroup 'Microsoft.Resources/resourceGroups@2025-04-01' = {
name: targetResourceGroupName
location: location
tags: commonTags
}
module identity './modules/identity.bicep' = {
name: 'deploy-project02-identity'
scope: controlResourceGroup
params: {
identityName: identityName
location: location
tags: commonTags
}
}
What should match?
Compare the scope and every project name with your saved file. The template should finish with the identity module and contain no RBAC assignment.
Deploy and verify the foundation
A deployment what-if previews the changes before Azure creates them. The confirmation gate lets you stop if the preview contains anything beyond the two resource groups and identity.
Azure CLI pauses after the preview for your approval. Resource creation can take a few minutes after approval, so a quiet terminal during that period is expected.
Before you run this, which three resources do you expect the preview to show?
- Preview the foundation deployment from Windows PowerShell by running:
az deployment sub create --name az104-project02-foundation --location eastus --template-file main.bicep --confirm-with-what-if
What does this command do?
- The subscription deployment uses main.bicep as its template.
- The deployment location is eastus.
- The confirmation option shows the predicted changes before deployment.
- The deployment name preserves this foundation run in Azure history.
The preview should contain rg-az104-identity-control and rg-az104-identity-target. It should also contain id-az104-reader inside the control resource group.
- Review the preview for any role assignment resource.
- Approve the deployment from the prompt only when the preview contains the expected foundation resources.
That is the foundation live. Azure now has a reusable identity plus two visible resource boundaries.
Did the foundation deployment fail?
- Confirm Windows PowerShell is still located in 02-identity-rbac.
- Confirm both Bicep files were saved before the command ran.
- Confirm the preview uses the selected training subscription.
Help me troubleshoot the foundation deployment.
Before you check the identity, what values do you expect Azure to generate alongside its name?
- Verify the deployed identity by running:
az identity show --name id-az104-reader --resource-group rg-az104-identity-control --query "{name:name,principalId:principalId,clientId:clientId}" --output table
What does this verification show?
- The identity query targets id-az104-reader inside the control resource group.
- The principal ID identifies the service principal used by Azure RBAC.
- The client ID identifies the managed identity to applications.
- The table format places the three values together as evidence.
You should see one row containing id-az104-reader plus generated principal and client IDs. The deployment preview contains no role assignment, so the identity has no Azure resource access yet.
Can't find the managed identity?
- Check that the deployment completed before running the identity query.
- Check that the identity name is id-az104-reader.
- Check that the control resource group is rg-az104-identity-control.
Help me find the deployed managed identity.
Your identity boundary is deployed without any permissions attached. Next, you will use what-if to expose the reach of a subscription-scoped Reader assignment before Azure can create it.
Catch the Over-Scoped Assignment
Your user-assigned managed identity now exists inside the control resource group. This step tests how far a Reader assignment reaches when its declaration has no narrower scope.
A missing narrower scope can push Azure RBAC access beyond one workload boundary. Deployment what-if exposes that reach without changing Azure resources.
In this step, get ready to:
- Add a temporary Reader assignment to the subscription-scoped template.
- Preview the assignment path without deploying it.
- Document why the proposed scope was rejected.
Add the temporary Reader assignment
An Azure role assignment links a security principal to a role definition at a specific scope. This temporary draft connects the existing identity to the built-in Reader role.
- In main.bicep, find this variable pair:
var identityName = 'id-az104-reader'
var commonTags = {
Where Does the Role ID Belong?
The identity name sits immediately above the shared tags. The Reader role ID belongs with these reusable template values.
- Replace that pair with this three-line variable block:
var identityName = 'id-az104-reader'
var readerRoleDefinitionId = 'acdd72a7-3385-48ef-bd42-f606fba81ae7'
var commonTags = {
Why Store the Reader Role ID?
The value identifies Azure's built-in Reader role. Keeping it in readerRoleDefinitionId makes the assignment repeatable.
The Bicep template can reference an existing role definition without creating a new role. The assignment then uses the identity module's principalId output as its principal.
- In main.bicep, locate the closing brace of the identity module.
- Paste these resource declarations directly below the module:
resource readerRoleDefinition 'Microsoft.Authorization/roleDefinitions@2022-04-01' existing = {
name: readerRoleDefinitionId
}
resource overbroadReaderAssignment 'Microsoft.Authorization/roleAssignments@2022-04-01' = {
name: guid(subscription().id, identity.outputs.principalId, readerRoleDefinition.id)
properties: {
roleDefinitionId: readerRoleDefinition.id
principalId: identity.outputs.principalId
principalType: 'ServicePrincipal'
}
}
What Does This Code Do?
- The readerRoleDefinition resource references the built-in Reader role.
- The overbroadReaderAssignment resource connects that role to the managed identity.
- The guid() expression produces the same assignment name from the same scope, principal, and role.
- The ServicePrincipal value tells Azure which principal type receives the role.
- Save main.bicep.
✔️ Awesome, I've Got Everything
- Confirm that main.bicep is saved with the temporary assignment.
ⓧ I'd Like to Double Check the Full Code
targetScope = 'subscription'
param location string = deployment().location
var controlResourceGroupName = 'rg-az104-identity-control'
var targetResourceGroupName = 'rg-az104-identity-target'
var identityName = 'id-az104-reader'
var readerRoleDefinitionId = 'acdd72a7-3385-48ef-bd42-f606fba81ae7'
var commonTags = {
Environment: 'Training'
Project: 'AZ-104-02'
ManagedBy: 'Bicep'
}
resource controlResourceGroup 'Microsoft.Resources/resourceGroups@2025-04-01' = {
name: controlResourceGroupName
location: location
tags: commonTags
}
resource targetResourceGroup 'Microsoft.Resources/resourceGroups@2025-04-01' = {
name: targetResourceGroupName
location: location
tags: commonTags
}
module identity './modules/identity.bicep' = {
name: 'deploy-project02-identity'
scope: controlResourceGroup
params: {
identityName: identityName
location: location
tags: commonTags
}
}
resource readerRoleDefinition 'Microsoft.Authorization/roleDefinitions@2022-04-01' existing = {
name: readerRoleDefinitionId
}
resource overbroadReaderAssignment 'Microsoft.Authorization/roleAssignments@2022-04-01' = {
name: guid(subscription().id, identity.outputs.principalId, readerRoleDefinition.id)
properties: {
roleDefinitionId: readerRoleDefinition.id
principalId: identity.outputs.principalId
principalType: 'ServicePrincipal'
}
}
Preview the inherited scope
What-if evaluates the temporary template against the live subscription. It reports the proposed resource ID without creating the assignment.
Before you run the preview, do you expect the assignment path to stay inside rg-az104-identity-target?
- Preview the temporary assignment by running this command:
az deployment sub what-if --name az104-project02-overbroad --location eastus --template-file main.bicep --result-format ResourceIdOnly
What Does the Preview Prove?
You will see a proposed role assignment directly under /subscriptions/<subscription-id>/providers/Microsoft.Authorization/roleAssignments/. What-if reports this change without deploying it.
You caught the scope mistake before deployment. The proposed path contains no target resource-group boundary.
- Compare the proposed role assignment path with the name rg-az104-identity-target.
The target resource group is absent from the proposed path. The assignment therefore reaches the subscription instead of one workload boundary.
Why Is Subscription Scope Too Broad?
A subscription-level Reader assignment reaches the resource groups beneath it through RBAC inheritance. This identity needs inspection access only inside rg-az104-identity-target.
Preview Command Failing?
- Check that PowerShell is still working inside 02-identity-rbac.
- Check that main.bicep contains the complete readerRoleDefinition resource.
- Check that overbroadReaderAssignment references identity.outputs.principalId exactly.
Help me diagnose my Bicep what-if preview.
- Stop after the what-if preview.
- Leave the temporary assignment undeployed.
Record why the preview was rejected
The rejected preview is useful evidence because it captures the security decision behind the final design. Your README.md records that the broad draft was inspected without being deployed.
- In the Visual Studio Code window from earlier, create README.md inside 02-identity-rbac.
- Paste this evidence into README.md.
# Project 02: Build a Least-Privilege Azure RBAC Lab
## Designed obstacle
A temporary RBAC draft placed a Reader role assignment directly in subscription-scoped `main.bicep`. Deployment what-if showed that Azure would create the assignment at subscription scope. This draft was not deployed because omitting a narrower scope would grant Reader across the subscription.
What Does This Evidence Capture?
The note records the unsafe scope that what-if exposed. It also records that the draft never changed the deployed environment.
- Save README.md.
- Confirm that the file begins with Project 02: Build a Least-Privilege Azure RBAC Lab.
- Confirm that the Designed obstacle section records the rejected subscription scope.
README in the Wrong Folder?
Check that README.md appears beside main.bicep inside 02-identity-rbac. Move it into that folder before continuing if it appears elsewhere.
Help me place the project README correctly.
Before you rerun the final check, do you expect Azure to report a deployed assignment or only a proposed assignment?
- Verify the temporary assignment remains a preview by running:
az deployment sub what-if --name az104-project02-overbroad --location eastus --template-file main.bicep --result-format ResourceIdOnly
What Should You See?
The proposed role assignment appears under /subscriptions/<subscription-id>/providers/Microsoft.Authorization/roleAssignments/. The command previews the assignment without creating it.
That is the safety win in this step. Your deployed identity boundary remains unchanged with no role assignment.
You have caught an excessive permission boundary before it reached Azure. Next, you will move the assignment into the target resource group's scope and prove the identity has only the access it needs.
Apply Least-Privilege Access
Your rejected what-if preview proved that a correct Reader role can still violate least privilege when its scope reaches the entire subscription.
A resource-group-scoped Bicep module will hold the Azure RBAC assignment for the managed identity. Azure CLI will then prove that the target resource group is the permission boundary.
In this step, get ready to:
- Build a reusable resource-group-scoped RBAC module.
- Preview the narrow role-assignment scope.
- Deploy the assignment with Azure CLI proof of its exact scope.
Build the resource-group RBAC module
A Bicep module uses the scope supplied by its parent deployment. Placing the role assignment inside a module scoped to the target resource group keeps Reader access inside that boundary.
- Create modules/rbac.bicep inside 02-identity-rbac by using your editor's file controls.
- Define the resource-group-scoped assignment by pasting this code into modules/rbac.bicep:
param principalId string
var readerRoleDefinitionId = 'acdd72a7-3385-48ef-bd42-f606fba81ae7'
resource readerRoleDefinition 'Microsoft.Authorization/roleDefinitions@2022-04-01' existing = {
scope: subscription()
name: readerRoleDefinitionId
}
resource readerRoleAssignment 'Microsoft.Authorization/roleAssignments@2022-04-01' = {
name: guid(resourceGroup().id, principalId, readerRoleDefinition.id)
properties: {
roleDefinitionId: readerRoleDefinition.id
principalId: principalId
principalType: 'ServicePrincipal'
}
}
output roleAssignmentId string = readerRoleAssignment.id
output roleAssignmentScope string = resourceGroup().id
What Does This Module Do?
- The principalId parameter receives the managed identity's service principal object ID.
- The existing readerRoleDefinition resource references the built-in Reader role at subscription scope.
- The readerRoleAssignment resource creates the assignment at the module's resource-group scope.
- The guid(resourceGroup().id, principalId, readerRoleDefinition.id) expression generates the same assignment name for the same scope.
- The outputs expose the created assignment ID plus its resource-group scope.
- Save modules/rbac.bicep.
- Confirm the file exists by locating modules/rbac.bicep in the editor's file tree.
You should see rbac.bicep beside identity.bicep inside the modules folder.
Cannot Find the RBAC Module?
- Check that the filename is exactly rbac.bicep.
- Check that the file sits inside 02-identity-rbac/modules.
Help me check why my RBAC module is missing from the project folder.
✔️ Awesome, I've got everything!
Your RBAC module is saved inside the modules folder.
ⓧ I'd like to double check the full code
param principalId string
var readerRoleDefinitionId = 'acdd72a7-3385-48ef-bd42-f606fba81ae7'
resource readerRoleDefinition 'Microsoft.Authorization/roleDefinitions@2022-04-01' existing = {
scope: subscription()
name: readerRoleDefinitionId
}
resource readerRoleAssignment 'Microsoft.Authorization/roleAssignments@2022-04-01' = {
name: guid(resourceGroup().id, principalId, readerRoleDefinition.id)
properties: {
roleDefinitionId: readerRoleDefinition.id
principalId: principalId
principalType: 'ServicePrincipal'
}
}
output roleAssignmentId string = readerRoleAssignment.id
output roleAssignmentScope string = resourceGroup().id
What Should Match?
The assignment name starts with resourceGroup().id. The module outputs expose the assignment ID plus the resource-group scope.
Replace the broad assignment and preview the scope
The temporary role assignment inherited the subscription scope from main.bicep. Removing that draft first gives you a clean preview before the scoped module is connected.
- Remove the temporary RBAC draft from main.bicep. Delete the readerRoleDefinitionId variable plus the complete readerRoleDefinition resource block plus the complete overbroadReaderAssignment resource block.
- Save main.bicep.
- Preview the template after removing the broad assignment by running the command below:
Before you run this, predict whether the subscription-scoped role-assignment path will remain in the preview.
az deployment sub what-if --name az104-project02-least-privilege-preview --location eastus --template-file main.bicep --result-format ResourceIdOnly
What Does This Preview Check?
The command compares the current subscription deployment with the edited main.bicep template. What-if performs this comparison without modifying your Azure resources.
The proposed subscription-scoped role assignment should be absent. This confirms that the over-scoped draft has been removed before the safe replacement is introduced.
Still See the Broad Assignment?
- Check that the complete overbroadReaderAssignment resource block was removed from main.bicep.
- Check that main.bicep was saved before the preview ran.
Help me find why the broad role assignment still appears in my Bicep what-if preview.
- Add the scoped readerAssignment module below the existing identity module in main.bicep by pasting this code:
module readerAssignment './modules/rbac.bicep' = {
name: 'assign-reader-to-target'
scope: targetResourceGroup
params: {
principalId: identity.outputs.principalId
}
}
output identityName string = identityName
output identityPrincipalId string = identity.outputs.principalId
output identityClientId string = identity.outputs.clientId
output readerScope string = targetResourceGroup.id
How Does the Scope Become Narrow?
- The local path ./modules/rbac.bicep loads the reusable RBAC module.
- The scope: targetResourceGroup line runs that module inside rg-az104-identity-target.
- The module receives identity.outputs.principalId as the principal that receives Reader access.
- The readerScope output records the target resource group's resource ID.
- Save main.bicep.
- Preview the scoped role assignment by running the command below:
Before you run this, predict which resource group should appear inside the proposed assignment path.
az deployment sub what-if --name az104-project02-least-privilege-preview --location eastus --template-file main.bicep --result-format ResourceIdOnly
What Does the New Path Prove?
The module's deployment scope places the role assignment beneath the target resource group. The preview lets you inspect that boundary before Azure creates the assignment.
You should see the proposed assignment beneath the path for rg-az104-identity-target. The assignment should no longer sit directly beneath the subscription's role-assignment path.
Is the Assignment Still at Subscription Scope?
- Check that readerAssignment contains scope: targetResourceGroup.
- Check that main.bicep no longer contains overbroadReaderAssignment.
- Check that ./modules/rbac.bicep uses forward slashes in the module path.
Help me diagnose why my role assignment still appears at subscription scope.
✔️ Awesome, I've got everything!
Your final main.bicep uses the target resource group as the RBAC module scope.
ⓧ I'd like to double check the full code
targetScope = 'subscription'
param location string = deployment().location
var controlResourceGroupName = 'rg-az104-identity-control'
var targetResourceGroupName = 'rg-az104-identity-target'
var identityName = 'id-az104-reader'
var commonTags = {
Environment: 'Training'
Project: 'AZ-104-02'
ManagedBy: 'Bicep'
}
resource controlResourceGroup 'Microsoft.Resources/resourceGroups@2025-04-01' = {
name: controlResourceGroupName
location: location
tags: commonTags
}
resource targetResourceGroup 'Microsoft.Resources/resourceGroups@2025-04-01' = {
name: targetResourceGroupName
location: location
tags: commonTags
}
module identity './modules/identity.bicep' = {
name: 'deploy-project02-identity'
scope: controlResourceGroup
params: {
identityName: identityName
location: location
tags: commonTags
}
}
module readerAssignment './modules/rbac.bicep' = {
name: 'assign-reader-to-target'
scope: targetResourceGroup
params: {
principalId: identity.outputs.principalId
}
}
output identityName string = identityName
output identityPrincipalId string = identity.outputs.principalId
output identityClientId string = identity.outputs.clientId
output readerScope string = targetResourceGroup.id
What Should Match?
The temporary role-definition variable plus both subscription-scoped RBAC resources are absent. The readerAssignment module is scoped to targetResourceGroup.
Deploy and prove the narrow assignment
The what-if preview has shown the intended boundary. The deployment can now create the assignment that you inspected without widening its scope.
- Create the least-privilege deployment by running this command:
az deployment sub create --name az104-project02-least-privilege --location eastus --template-file main.bicep
What Does This Deployment Create?
The command deploys the final subscription template. The scoped module creates one Reader assignment inside rg-az104-identity-target.
Keep the PowerShell window open while Azure completes the deployment. You should see a successful deployment result before continuing.
Did the Deployment Fail?
- Check that modules/rbac.bicep is saved inside the modules folder.
- Check that the local module path is exactly ./modules/rbac.bicep.
- Check that the training subscription remains selected in Azure CLI.
Help me troubleshoot my least-privilege Bicep deployment.
The deployment result becomes stronger portfolio evidence when README.md records what Azure CLI proved. This note preserves the exact access boundary after the terminal session ends.
- Add this verification section below the existing designed-obstacle section in README.md by pasting the following text:
## Verification
Azure CLI confirms one `Reader` assignment for `id-az104-reader` at `/resourceGroups/rg-az104-identity-target` and no subscription-scoped assignment.
What Does This Evidence Record?
The verification note names the principal plus the role plus the final scope. It also records that the rejected subscription-level assignment does not exist.
- Save README.md.
✔️ Awesome, I've got everything!
Your README.md now records the rejected design plus the final access boundary.
ⓧ I'd like to double check the full code
# Project 02: Build a Least-Privilege Azure RBAC Lab
## Designed obstacle
A temporary RBAC draft placed a Reader role assignment directly in subscription-scoped `main.bicep`. Deployment what-if showed that Azure would create the assignment at subscription scope. This draft was not deployed because omitting a narrower scope would grant Reader across the subscription.
## Verification
Azure CLI confirms one `Reader` assignment for `id-az104-reader` at `/resourceGroups/rg-az104-identity-target` and no subscription-scoped assignment.
What Should Match?
The file contains the designed-obstacle record followed by the verification section. The verification names one Reader assignment at the target resource group.
- Capture the identity's principal ID plus list every assignment for that principal by running these commands:
Before you run this, predict how many rows should appear plus where the scope should end.
$principalId = az identity show --name id-az104-reader --resource-group rg-az104-identity-control --query principalId --output tsv
az role assignment list --all --assignee-object-id $principalId --fill-principal-name false --query "[].{role:roleDefinitionName,scope:scope}" --output table
How Does This Prove Least Privilege?
- The first command stores the managed identity's service principal object ID in $principalId.
- The second command lists role assignments for that exact principal.
- The query limits the table to the role name plus its scope.
- The disabled principal-name lookup keeps the verification focused on the known object ID.
You should see exactly one Reader row whose scope ends with /resourceGroups/rg-az104-identity-target. No row should end at the subscription.
That is the boundary working. Your reusable identity can inspect the target resource group without receiving Reader across the subscription.
Is the Role Table Empty or Too Broad?
- Check that the deployment completed successfully before listing the assignments.
- Check that $principalId contains a value from id-az104-reader.
- Inspect main.bicep again if any row ends at the subscription.
Help me diagnose my managed identity role-assignment table.
Your managed identity now has one proven Reader boundary at the target resource group. Next, you will package this evidence with the completed continuation brief before pushing the project to GitHub.
Package and Push the Evidence
Your Azure RBAC boundary is working. Azure CLI proves that Reader access ends at the target resource group.
Another person needs to understand the design without your live terminal session. Your documentation preserves the decision behind the boundary.
A complete continuation brief carries your environment into the next project. Publishing the sanitized folder to GitHub turns the deployment into lasting portfolio evidence.
In this step, get ready to:
- Complete the project README with deployment evidence and cleanup instructions.
- Update the continuation brief with the completed lab.
- Publish the sanitized project to GitHub.
Complete the project README
A useful README.md explains the goal before it shows commands. It also names the final access boundary so a reviewer knows what the project proves.
- Return to README.md in the editor from earlier.
- Replace its current content with this project overview:
# Project 02: Build a Least-Privilege Azure RBAC Lab
## Goal
Deploy a user-assigned managed identity and grant it the Reader role only at the resource group that represents its target workload boundary.
## Why this project matters
Azure RBAC assignments combine a security principal, a role definition, and a scope. A correct role with an unnecessarily broad scope still creates unnecessary access. This lab uses deployment what-if to expose that mistake before deployment, then fixes it with a resource-group-scoped Bicep module.
## Resources
- Control resource group: `rg-az104-identity-control`
- Target resource group: `rg-az104-identity-target`
- User-assigned managed identity: `id-az104-reader`
- Role: `Reader`
- Final role scope: `rg-az104-identity-target`
## Cost
Expected Azure cost: $0. This lab deploys no compute, storage, or paid service instance. Microsoft documents managed identities as available at no extra cost.
What does this section capture?
- The goal identifies the managed identity as the security principal.
- The project explanation connects least privilege to the assignment scope.
- The resources section records the exact identity and resource-group boundary.
- The cost section explains why the deployed lab has no expected Azure charge.
- Save README.md.
- Confirm the file now identifies rg-az104-identity-target as the final role scope.
Still see the short README?
Make sure you replaced the old content instead of pasting above it. The first line should be the Project 02 title.
Ask for help if the old sections remain: Help me replace my existing README with the complete Project 02 overview without duplicating sections.
The next sections make the lab repeatable. A reviewer can deploy the same Bicep design and verify its resulting scope.
- Place your cursor on a new blank line after the Cost section.
- Add the deployment and verification sections by pasting this content:
## Deploy
Run these commands from this folder in Windows PowerShell:
```powershell
az login
az account set -s "<training-subscription-name-or-id>"
az account show --output table
az bicep version
az deployment sub what-if --name az104-project02-least-privilege-preview --location eastus --template-file main.bicep --result-format ResourceIdOnly
az deployment sub create --name az104-project02-least-privilege --location eastus --template-file main.bicep
```
## Verify
```powershell
az identity show --name id-az104-reader --resource-group rg-az104-identity-control --query "{name:name,principalId:principalId,clientId:clientId}" --output table
$principalId = az identity show --name id-az104-reader --resource-group rg-az104-identity-control --query principalId --output tsv
az role assignment list --all --assignee-object-id $principalId --fill-principal-name false --query "[].{role:roleDefinitionName,scope:scope}" --output table
```
Expected evidence: one `Reader` assignment whose scope ends with `/resourceGroups/rg-az104-identity-target`.
How does this prove the result?
- The deployment sequence previews the resource IDs before applying the final template.
- The identity query retrieves the generated principal details from the control resource group.
- The role query reduces the evidence to the role name and its assigned scope.
- Save README.md.
- Confirm the Verify section ends with the target resource-group path.
Does the Markdown look broken?
Check that each PowerShell section has one opening code fence and one closing code fence. A missing fence can turn the remaining headings into command text.
Ask for help with the formatting: Help me find an unmatched code fence in my Project 02 README.
The designed obstacle is part of the evidence. It shows that the broad assignment was detected during preview and never reached the subscription.
- Place your cursor on a new blank line after the expected evidence sentence.
- Add the rejected design and pause procedure by pasting this content:
## Designed obstacle
The first RBAC draft placed the role assignment directly in the subscription-scoped `main.bicep`. What-if showed that the assignment would be created at subscription scope. That draft was never deployed. The final design places the assignment in `modules/rbac.bicep` and scopes that module to the target resource group.
## Pause access
```powershell
$principalId = az identity show --name id-az104-reader --resource-group rg-az104-identity-control --query principalId --output tsv
$targetScope = az group show --name rg-az104-identity-target --query id --output tsv
az role assignment list --all --assignee-object-id $principalId --fill-principal-name false
az role assignment delete --assignee-object-id $principalId --role Reader --scope $targetScope
```
Rerun the deployment to restore the assignment.
Why include the rejected design?
- The obstacle documents the subscription-level risk revealed by what-if.
- The final design records the resource-group-scoped module as the correction.
- The pause procedure provides a reversible way to remove the assignment.
- Save README.md.
- Confirm the Designed obstacle section states that the broad draft was never deployed.
Is the safety decision unclear?
Keep the preview and deployment outcomes separate. The subscription-scoped assignment belongs only in the what-if explanation.
Ask for a clarity check: Help me confirm that my README clearly separates the rejected preview from the deployed least-privilege design.
Cleanup instructions make the project safe to revisit. The skills list turns the finished files into a concise record of what you can demonstrate.
- Place your cursor on a new blank line after the assignment restoration sentence.
- Finish the README by pasting the deletion and skills sections:
## Delete
Delete the target resource group before deleting the control resource group:
```powershell
az group delete --name rg-az104-identity-target --yes
az group delete --name rg-az104-identity-control --yes
```
## Skills demonstrated
- User-assigned managed identities
- Built-in Azure roles
- RBAC scope and least privilege
- Bicep modules and outputs
- Deterministic role assignment names
- Subscription deployment what-if
- Azure CLI verification
What completes the evidence?
- The deletion order removes the target boundary before the identity control boundary.
- The skills list summarizes the identity and access techniques demonstrated by the lab.
- The final README now explains the full path from risky preview to narrow assignment.
- Save README.md.
- Confirm the final section lists Azure CLI verification as a demonstrated skill.
Missing the final sections?
Make sure the Delete heading begins after the pause section has fully closed. The Skills demonstrated heading should appear after the deletion code fence.
Ask for help checking the order: Help me check the final section order in my Project 02 README.
✔️ Awesome, I've got everything!
- Keep README.md saved with the completed deployment evidence.
ⓧ I'd like to double check the full code
Compare your complete README.md with this reference.
# Project 02: Build a Least-Privilege Azure RBAC Lab
## Goal
Deploy a user-assigned managed identity and grant it the Reader role only at the resource group that represents its target workload boundary.
## Why this project matters
Azure RBAC assignments combine a security principal, a role definition, and a scope. A correct role with an unnecessarily broad scope still creates unnecessary access. This lab uses deployment what-if to expose that mistake before deployment, then fixes it with a resource-group-scoped Bicep module.
## Resources
- Control resource group: `rg-az104-identity-control`
- Target resource group: `rg-az104-identity-target`
- User-assigned managed identity: `id-az104-reader`
- Role: `Reader`
- Final role scope: `rg-az104-identity-target`
## Cost
Expected Azure cost: $0. This lab deploys no compute, storage, or paid service instance. Microsoft documents managed identities as available at no extra cost.
## Deploy
Run these commands from this folder in Windows PowerShell:
```powershell
az login
az account set -s "<training-subscription-name-or-id>"
az account show --output table
az bicep version
az deployment sub what-if --name az104-project02-least-privilege-preview --location eastus --template-file main.bicep --result-format ResourceIdOnly
az deployment sub create --name az104-project02-least-privilege --location eastus --template-file main.bicep
```
## Verify
```powershell
az identity show --name id-az104-reader --resource-group rg-az104-identity-control --query "{name:name,principalId:principalId,clientId:clientId}" --output table
$principalId = az identity show --name id-az104-reader --resource-group rg-az104-identity-control --query principalId --output tsv
az role assignment list --all --assignee-object-id $principalId --fill-principal-name false --query "[].{role:roleDefinitionName,scope:scope}" --output table
```
Expected evidence: one `Reader` assignment whose scope ends with `/resourceGroups/rg-az104-identity-target`.
## Designed obstacle
The first RBAC draft placed the role assignment directly in the subscription-scoped `main.bicep`. What-if showed that the assignment would be created at subscription scope. That draft was never deployed. The final design places the assignment in `modules/rbac.bicep` and scopes that module to the target resource group.
## Pause access
```powershell
$principalId = az identity show --name id-az104-reader --resource-group rg-az104-identity-control --query principalId --output tsv
$targetScope = az group show --name rg-az104-identity-target --query id --output tsv
az role assignment list --all --assignee-object-id $principalId --fill-principal-name false
az role assignment delete --assignee-object-id $principalId --role Reader --scope $targetScope
```
Rerun the deployment to restore the assignment.
## Delete
Delete the target resource group before deleting the control resource group:
```powershell
az group delete --name rg-az104-identity-target --yes
az group delete --name rg-az104-identity-control --yes
```
## Skills demonstrated
- User-assigned managed identities
- Built-in Azure roles
- RBAC scope and least privilege
- Bicep modules and outputs
- Deterministic role assignment names
- Subscription deployment what-if
- Azure CLI verification
How to compare the file
Check each heading in order. Pay close attention to the PowerShell code fences and the final target scope.
Update the continuation brief
The continuation brief gives the next planning conversation a reliable starting point. It needs the completed Project 02 outcome plus the exact repository evidence.
- Return to NEXT-PROJECT-BRIEF.md in the editor from earlier.
- Replace the Completed projects section with this updated version:
## Completed projects
- [x] Project 01: Subscription Governance Guardrails
- Built a sandbox resource group, governance policy assignment, required tags, and cost controls with Bicep and Azure CLI.
- [x] Project 02: Build a Least-Privilege Azure RBAC Lab
- Created a user-assigned managed identity.
- Used what-if to catch a subscription-scoped Reader assignment before deployment.
- Limited Reader to `rg-az104-identity-target` through a scoped Bicep module.
- Verified the final role and scope with Azure CLI.
What does the completion record preserve?
- The checked Project 02 item records that the lab is complete.
- The supporting bullets preserve the managed identity and what-if evidence.
- The final bullet records the verified resource-group scope.
- Save NEXT-PROJECT-BRIEF.md.
- Confirm Project 02 now has a checked completion box.
Still see Project 02 as incomplete?
Make sure you replaced the existing Completed projects section. A second copy can leave the old unchecked item in the file.
Ask for help removing the duplicate: Help me consolidate duplicate Completed projects sections in my continuation brief.
Repository evidence tells the next project exactly what already exists. This prevents future instructions from recreating the identity boundary.
- Place your cursor on a new blank line before the Direction for the next project heading.
- Add the Project 02 repository evidence by pasting this content:
## Project 02 repository evidence
- Folder: `02-identity-rbac`
- Main template: `main.bicep`
- Modules: `modules/identity.bicep`, `modules/rbac.bicep`
- Identity: `id-az104-reader`
- Control scope: `rg-az104-identity-control`
- Target scope: `rg-az104-identity-target`
- Final access: Reader at the target resource group only
Why record these artifacts?
- The folder and file paths preserve the repository structure.
- The identity and scope entries preserve the deployed boundary.
- The final access entry states the least-privilege result in one line.
- Save NEXT-PROJECT-BRIEF.md.
- Confirm the evidence section ends with Reader access at the target resource group only.
Is the evidence section in the wrong place?
Move the repository evidence below Completed projects. Keep it above the direction for the next project.
Ask for help checking the structure: Help me place the Project 02 repository evidence correctly in my continuation brief.
✔️ Awesome, I've got everything!
- Keep NEXT-PROJECT-BRIEF.md saved with Project 02 marked complete.
ⓧ I'd like to double check the full code
Compare your complete NEXT-PROJECT-BRIEF.md with this reference.
# AZ-104 Portfolio Continuation Brief
## How to use this file
Upload this file in a new planning conversation and send:
> Let's do Project 03. Use the attached AZ-104 Portfolio Continuation Brief as the source of truth and maintain the same project format.
Change `03` to the next project number you want.
## Learner environment
- Device: Windows PC
- Azure access: Owner on the training subscription
- Ready tools: Azure, VS Code, Azure CLI, Bicep, Git, GitHub
- Repository structure: One AZ-104 repository with numbered project folders
## Format to preserve
- Build one focused AZ-104 portfolio project at a time.
- Keep the project under 60 minutes and at no cost where possible.
- Use Bicep-first infrastructure and Azure CLI commands for Windows PowerShell.
- Put the project in its own numbered folder.
- Include a visible early win, a designed obstacle, what-if validation, deployment verification, cleanup, a README, and a GitHub push.
- Explain the problem each Azure feature solves instead of presenting a configuration checklist.
- Verify current commands, API versions, permissions, and pricing against official Microsoft documentation.
## Completed projects
- [x] Project 01: Subscription Governance Guardrails
- Built a sandbox resource group, governance policy assignment, required tags, and cost controls with Bicep and Azure CLI.
- [x] Project 02: Build a Least-Privilege Azure RBAC Lab
- Created a user-assigned managed identity.
- Used what-if to catch a subscription-scoped Reader assignment before deployment.
- Limited Reader to `rg-az104-identity-target` through a scoped Bicep module.
- Verified the final role and scope with Azure CLI.
## Project 02 repository evidence
- Folder: `02-identity-rbac`
- Main template: `main.bicep`
- Modules: `modules/identity.bicep`, `modules/rbac.bicep`
- Identity: `id-az104-reader`
- Control scope: `rg-az104-identity-control`
- Target scope: `rg-az104-identity-target`
- Final access: Reader at the target resource group only
## Direction for the next project
Choose the next logical AZ-104 skill that builds on governance and identity without repeating them. Prefer a free or free-tier Azure implementation, Bicep automation, Azure CLI verification, cleanup instructions, and a portfolio-ready GitHub result.
How to compare the brief
Check that Project 02 is marked complete. Confirm that the repository evidence appears before the final direction section.
Sanitize and publish the project
Publishing a repository can feel exposed. A quick privacy pass keeps your generated Azure values local.
Git records the project files before GitHub receives them. This gives you one final chance to inspect the evidence package.
- Review README.md for real subscription IDs or tenant IDs.
- Confirm the training subscription reference remains a placeholder.
- Review NEXT-PROJECT-BRIEF.md for generated principal IDs.
- Review the three Bicep files for pasted credentials.
- Save every reviewed file.
Which identifier stays in the project?
The value acdd72a7-3385-48ef-bd42-f606fba81ae7 is the public built-in Reader role definition ID. It belongs in modules/rbac.bicep.
Generated subscription IDs, tenant IDs, principal IDs, and credentials stay outside the repository.
- Review Git's pending file list by running this command:
git status
What does this check show?
The status output lists the files that Git sees as changed or untracked. It does not modify the project.
You should see the completed project files waiting to be committed inside 02-identity-rbac.
- Stage the project and publish its commit by running these commands:
git add .
git commit -m 'Add Project 02 least-privilege RBAC lab'
git push
What does this publication sequence do?
- The first command stages the completed files in the current project folder.
- The second command creates the Project 02 commit with the required message.
- The final command sends the current branch to its configured remote repository.
Did the push fail?
Confirm that the repository still has its configured remote. Authentication or network access can also block a push.
Ask for help with the result: Help me troubleshoot my Git push without changing or exposing my Azure project files.
Before the final check, what state do you expect Git to report after a successful commit and push?
- Verify the local repository state by running this command:
git status
What does a clean status prove?
A clean status means every saved project change belongs to the commit. No local edits are waiting to be staged.
You should see that the working tree has no pending changes. This confirms the local Project 02 package matches the commit you pushed.
- Navigate to your existing repository on GitHub.
- Select the 02-identity-rbac folder.
You should see main.bicep, README.md, NEXT-PROJECT-BRIEF.md, and the modules folder.
- Select the modules folder.
You should see identity.bicep and rbac.bicep inside it.
Missing files on GitHub?
Return to the local status output and check whether any files remain untracked. A file omitted from the commit cannot appear in the remote folder.
Ask for help comparing both states: Help me find why my local Project 02 files are missing from GitHub after the push.
You have turned a working least-privilege deployment into reusable evidence. The repository now preserves the boundary, the rejected preview, the cleanup path, and the handoff for Project 03.
Secret mission
Reuse the Identity Across Two Narrow Scopes
Extend your least-privilege design with a second target resource group. You will reuse the existing managed identity while keeping each Reader assignment limited to its own resource-group scope.
Clean Up Your Resources
Clean Up Your Resources
This Azure lab is expected to remain at $0 because it deploys no compute or paid data service. Managed identities are available at no extra cost.
Decide whether to keep your resources running, pause them to come back later, or delete them entirely.
Resources you used:
- The rg-az104-identity-control control resource group containing the id-az104-reader user-assigned managed identity.
- The rg-az104-identity-target resource group with one Reader role assignment.
- The rg-az104-identity-target-secondary resource group with the second narrow Reader role assignment.
Keep everything running
No action needed. Choose this if you want to keep the deployed lab available as portfolio evidence.
- Leave rg-az104-identity-control available with the reusable identity.
- Leave both target resource groups available with their separate Reader assignments.
- Keep the 02-identity-rbac folder as the source for rebuilding the lab.
- Continue checking the Azure cost tools from Project 01 even though this lab is expected to remain at $0.
Pause - I'll come back to this later
Revoke both Reader role assignments to pause access. The identity and all three resource groups remain available for your return.
- Switch back to Windows PowerShell from earlier.
- Load the identity and resource group IDs into the session by running:
$principalId = az identity show --name id-az104-reader --resource-group rg-az104-identity-control --query principalId --output tsv
$targetScope = az group show --name rg-az104-identity-target --query id --output tsv
$secondaryTargetScope = az group show --name rg-az104-identity-target-secondary --query id --output tsv
The $principalId value identifies the managed identity. The two scope variables hold the exact target resource group IDs.
- Inspect the identity's current assignments by running:
az role assignment list --all --assignee-object-id $principalId --fill-principal-name false
You should see the two narrow Reader assignments before removing them.
- Revoke both narrow assignments by running:
az role assignment delete --assignee-object-id $principalId --role Reader --scope $targetScope
az role assignment delete --assignee-object-id $principalId --role Reader --scope $secondaryTargetScope
az role assignment list --all --assignee-object-id $principalId --fill-principal-name false
The two delete commands remove access from both target scopes. The final list should return no role assignments for the identity.
- Return to the 02-identity-rbac folder from earlier.
- Review the preview when Azure CLI asks for approval.
- Approve the deployment only if the preview contains two resource-group-scoped assignments.
- Restore both assignments with Bicep by running:
az deployment sub create --name az104-project02-secret --location eastus --template-file main.bicep --confirm-with-what-if
This redeploys the final template. Both Reader assignments return at their separate target resource group scopes.
Delete - I don't want to use this again
Remove the live Azure lab when you no longer need it. Deleting these resource groups permanently removes their contents.
Your committed 02-identity-rbac portfolio files remain in GitHub.
- Delete the primary target resource group by running:
az group delete --name rg-az104-identity-target --yes
This removes rg-az104-identity-target with its narrow Reader assignment.
- Delete the secondary target resource group by running:
az group delete --name rg-az104-identity-target-secondary --yes
This removes rg-az104-identity-target-secondary with its second narrow Reader assignment.
- Delete the control resource group last by running:
az group delete --name rg-az104-identity-control --yes
This removes rg-az104-identity-control with the id-az104-reader managed identity. Your live lab resources are now removed.
Nice Work!
Nice Work!
You did it! Your Azure RBAC lab now proves least privilege with one user-assigned managed identity. That identity has Reader access at exactly two target resource groups with no subscription-scoped assignment.
You've learned how to:
- Build a reusable identity boundary with Bicep modules. Generate deterministic Reader assignments at resource-group scope.
- Use deployment what-if to catch a subscription-scoped assignment before deployment. Verify each Azure RBAC scope with Azure CLI output.
- Document the security decision in README.md. Preserve the reusable Project 03 handoff in NEXT-PROJECT-BRIEF.md. Push the numbered portfolio folder to GitHub.
- Secret Mission: Reuse one managed identity across two narrow resource-group scopes. Confirm that neither Reader assignment reaches the subscription.
Ready to quiz yourself?