Skip to main content

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:

  1. Security Principal: Who needs access (user, group, service principal, managed identity)
  2. Role Definition: What they can do (Owner, Contributor, Reader, or custom)
  3. 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)

RolePermissionsUse Case
OwnerFull access + manage accessSubscription/resource group admins
ContributorFull access, cannot manage accessDevelopers, operators
ReaderView-only accessAuditors, monitoring
User Access AdministratorManage access onlyDelegated admin rights
Specific rolesService-specificVirtual 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
}