Azure RBAC Best Practices
What is Azure RBAC?
Azure Role-Based Access Control (RBAC) is an authorization system that provides fine-grained access management for Azure resources. It allows you to:
- Grant users, groups, and applications the exact permissions they need
- Manage access at different scopes: management group, subscription, resource group, or resource
- Implement least privilege security principle
- Audit and review access permissions
How RBAC Works
RBAC uses role assignments that combine three elements:
- Security Principal: Who needs access (user, group, service principal, managed identity)
- Role Definition: What they can do (Owner, Contributor, Reader, or custom)
- Scope: Where they can do it (management group, subscription, resource group, resource)
Formula: Security Principal + Role + Scope = Access
Key Concepts
Built-in Roles (Most Common)
| Role | Permissions | Use Case |
|---|---|---|
| Owner | Full access + manage access | Subscription/resource group admins |
| Contributor | Full access, cannot manage access | Developers, operators |
| Reader | View-only access | Auditors, monitoring |
| User Access Administrator | Manage access only | Delegated admin rights |
| Specific roles | Service-specific | Virtual Machine Contributor, Storage Blob Data Reader |
Scope Hierarchy
Management Group (highest)
└── Subscription
└── Resource Group
└── Resource (lowest)
Permissions assigned at a higher scope are inherited by child scopes.
Best Practices
1. Grant Least Privilege Access
❌ Don't: Assign Owner role when Contributor is sufficient ❌ Don't: Grant access at subscription level when resource group scope works ❌ Don't: Use wildcard permissions unnecessarily
✅ Do: Start with minimal permissions and add as needed ✅ Do: Use the most specific scope possible ✅ Do: Use specialized roles (e.g., "Virtual Machine Contributor" instead of "Contributor")
# Bad: Too broad
az role assignment create \
--role "Contributor" \
--assignee user@contoso.com \
--scope /subscriptions/{subscription-id}
# Good: Specific role and scope
az role assignment create \
--role "Virtual Machine Contributor" \
--assignee user@contoso.com \
--scope /subscriptions/{subscription-id}/resourceGroups/rg-dev-vms
2. Use Groups Instead of Individual Users
❌ Don't: Assign roles directly to individual users ✅ Do: Create Entra ID groups and assign roles to groups
Benefits:
- Easier to manage at scale
- Clear organization structure
- Simplified onboarding/offboarding
- Better auditability
# Create group in Entra ID
az ad group create \
--display-name "Database Administrators" \
--mail-nickname db-admins
# Assign role to group
az role assignment create \
--role "SQL DB Contributor" \
--assignee-object-id <group-object-id> \
--assignee-principal-type Group \
--scope /subscriptions/{sub-id}/resourceGroups/rg-databases
3. Use Managed Identities for Azure Resources
❌ Don't: Create service principals with long-lived secrets ✅ Do: Use system-assigned or user-assigned managed identities
# Enable system-assigned managed identity on VM
az vm identity assign \
--resource-group rg-app \
--name vm-app-01
# Grant access to Key Vault
az role assignment create \
--role "Key Vault Secrets User" \
--assignee <vm-principal-id> \
--scope /subscriptions/{sub-id}/resourceGroups/rg-security/providers/Microsoft.KeyVault/vaults/kv-prod
4. Regularly Audit Role Assignments
# List all role assignments in subscription
az role assignment list --all -o table
# Find assignments for a specific user
az role assignment list \
--assignee user@contoso.com \
--include-inherited \
--include-groups -o table
# List role assignments at resource group
az role assignment list \
--resource-group rg-prod \
-o table
5. Use Custom Roles When Needed
Create custom roles when built-in roles are too permissive:
# Create custom role definition
az role definition create --role-definition @custom-role.json
custom-role.json:
{
"Name": "Virtual Machine Operator",
"Description": "Can start, stop, and restart VMs only",
"Actions": [
"Microsoft.Compute/virtualMachines/start/action",
"Microsoft.Compute/virtualMachines/restart/action",
"Microsoft.Compute/virtualMachines/powerOff/action",
"Microsoft.Compute/virtualMachines/read"
],
"NotActions": [],
"AssignableScopes": [
"/subscriptions/{subscription-id}"
]
}
6. Implement Separation of Duties
Never combine these roles for the same user:
- Owner + User Access Administrator (too much power)
- Developer + Auditor (conflict of interest)
Use different accounts for different responsibilities.
7. Document Your RBAC Strategy
Maintain documentation of:
- Which roles are used and why
- Group naming conventions
- Access request and approval process
- Regular review schedule
Terraform Example
# Create resource group
resource "azurerm_resource_group" "app" {
name = "rg-app-prod"
location = "eastus"
}
# Get Entra ID group
data "azuread_group" "developers" {
display_name = "Developers"
security_enabled = true
}
# Assign Contributor role to developers group at RG scope
resource "azurerm_role_assignment" "developers_contributor" {
scope = azurerm_resource_group.app.id
role_definition_name = "Contributor"
principal_id = data.azuread_group.developers.object_id
}
# Get Entra ID group for readers
data "azuread_group" "auditors" {
display_name = "Auditors"
security_enabled = true
}
# Assign Reader role to auditors at subscription scope
resource "azurerm_role_assignment" "auditors_reader" {
scope = data.azurerm_subscription.current.id
role_definition_name = "Reader"
principal_id = data.azuread_group.auditors.object_id
}
# Create custom role
resource "azurerm_role_definition" "vm_operator" {
name = "Virtual Machine Operator"
scope = data.azurerm_subscription.current.id
description = "Can start, stop, and restart virtual machines"
permissions {
actions = [
"Microsoft.Compute/virtualMachines/start/action",
"Microsoft.Compute/virtualMachines/restart/action",
"Microsoft.Compute/virtualMachines/powerOff/action",
"Microsoft.Compute/virtualMachines/read",
"Microsoft.Compute/virtualMachines/instanceView/read"
]
not_actions = []
}
assignable_scopes = [
data.azurerm_subscription.current.id
]
}
# Use managed identity with RBAC
resource "azurerm_user_assigned_identity" "app" {
name = "id-app-prod"
resource_group_name = azurerm_resource_group.app.name
location = azurerm_resource_group.app.location
}
resource "azurerm_role_assignment" "app_storage" {
scope = azurerm_storage_account.app.id
role_definition_name = "Storage Blob Data Contributor"
principal_id = azurerm_user_assigned_identity.app.principal_id
}
CI/CD Integration
GitHub Actions with RBAC
name: Deploy Infrastructure
on:
push:
branches: [main]
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Azure Login
uses: azure/login@v1
with:
creds: ${{ secrets.AZURE_CREDENTIALS }}
# Service Principal should have minimum required role
# Example: Contributor at resource group scope only
- name: Deploy resources
run: |
az deployment group create \
--resource-group rg-app-dev \
--template-file main.bicep
Service Principal Setup:
# Create SP with Contributor role scoped to specific resource group
az ad sp create-for-rbac \
--name "github-actions-sp" \
--role "Contributor" \
--scopes /subscriptions/{sub-id}/resourceGroups/rg-app-dev \
--sdk-auth
Azure DevOps with Service Connections
trigger:
- main
pool:
vmImage: 'ubuntu-latest'
variables:
serviceConnection: 'Azure-Production'
steps:
- task: AzureResourceManagerTemplateDeployment@3
inputs:
deploymentScope: 'Resource Group'
azureResourceManagerConnection: $(serviceConnection)
subscriptionId: $(subscriptionId)
action: 'Create Or Update Resource Group'
resourceGroupName: 'rg-app-prod'
location: 'East US'
templateLocation: 'Linked artifact'
csmFile: 'templates/main.bicep'
Things to Avoid
❌ Don't assign Owner role to service principals or automation accounts ❌ Don't use "classic" subscription administrator roles (deprecated) ❌ Don't create hundreds of custom roles (use built-in when possible) ❌ Don't assign roles at management group level unless absolutely necessary ❌ Don't use service principals with permanent passwords (use managed identities) ❌ Don't mix Azure RBAC with resource-level access controls unnecessarily ❌ Don't grant "/write" or "/delete" permissions in custom roles without careful review ❌ Don't ignore "Deny assignments" (used by Azure Blueprints, can override Allow)
✅ Do use groups for role assignments ✅ Do use managed identities wherever possible ✅ Do implement regular access reviews (quarterly minimum) ✅ Do use Privileged Identity Management (PIM) for just-in-time admin access ✅ Do enable diagnostic logs for role assignment changes ✅ Do use Azure Policy to enforce RBAC requirements ✅ Do test permissions before granting them widely ✅ Do document your RBAC design and rationale
Advanced: Privileged Identity Management (PIM)
Requires Azure AD Premium P2:
Benefits:
- Just-in-time admin access (time-limited)
- Approval workflows for role activation
- Access reviews and audit history
- MFA requirement for role activation
Use Cases:
- Global Administrator role
- Owner role at subscription level
- Any highly privileged role
Monitoring & Compliance
Enable Activity Log for RBAC Changes
# Create alert for role assignment changes
az monitor activity-log alert create \
--name RBACChangeAlert \
--resource-group rg-monitoring \
--condition category=Administrative and operationName=Microsoft.Authorization/roleAssignments/write \
--action-group /subscriptions/{sub-id}/resourceGroups/rg-monitoring/providers/Microsoft.Insights/actionGroups/SecurityTeam
Use Azure Policy to Enforce RBAC Standards
# Example: Audit usage of Owner role
az policy assignment create \
--name 'audit-owner-role' \
--policy '/providers/Microsoft.Authorization/policyDefinitions/10ee2ea2-fb4d-45b8-a7e9-a2e770044cd9' \
--scope /subscriptions/{subscription-id}
Query Role Assignments with Azure Resource Graph
authorizationresources
| where type == "microsoft.authorization/roleassignments"
| extend roleDefinitionId = tostring(properties.roleDefinitionId)
| extend principalId = tostring(properties.principalId)
| join kind=inner (
authorizationresources
| where type == "microsoft.authorization/roledefinitions"
| extend roleDefinitionId = id
| project roleDefinitionId, roleName = tostring(properties.roleName)
) on roleDefinitionId
| where roleName == "Owner"
| project scope, principalId, roleName
Common Patterns
Environment-Based Access
- Production: Only specific production support group has Contributor
- Staging: Developers have Contributor
- Development: Developers have Owner
Application-Based Access
- App Team: Contributor on their app's resource group
- Platform Team: Reader on all resource groups, Contributor on shared services
- Security Team: Security Reader at subscription level
Workload Identity Pattern
# App uses managed identity
resource "azurerm_linux_web_app" "app" {
name = "webapp-myapp"
# ...
identity {
type = "SystemAssigned"
}
}
# Grant managed identity access to resources
resource "azurerm_role_assignment" "app_to_sql" {
scope = azurerm_mssql_server.main.id
role_definition_name = "SQL DB Contributor"
principal_id = azurerm_linux_web_app.app.identity[0].principal_id
}