Skip to main content

Azure RBAC — Roles, Permissions, and Conditional Access Deep Dive

· 9 min read
Goel Academy
DevOps & Cloud Learning Hub

Your intern just deleted the production database because someone gave them Owner access to the subscription. Your contractor can deploy resources in any region because nobody scoped their permissions. Your admin accounts have permanent standing access with no MFA requirement. These are not hypothetical scenarios — they happen every week in organizations that treat identity as an afterthought. Azure RBAC, Conditional Access, and PIM exist to make these disasters structurally impossible.

The RBAC Model — Who, What, Where

Azure RBAC answers three questions for every access decision:

  • Who (Security Principal) — A user, group, service principal, or managed identity
  • What (Role Definition) — A collection of permissions (actions and data actions)
  • Where (Scope) — The level at which the role is assigned

The scope hierarchy flows downward:

Management Group
└── Subscription
└── Resource Group
└── Resource

A role assigned at a subscription applies to every resource group and resource within it. A role assigned at a resource group applies only to resources in that group. Always assign at the narrowest scope possible.

# Assign the Reader role to a user at the resource group scope
az role assignment create \
--assignee "jane@company.com" \
--role "Reader" \
--scope "/subscriptions/<sub-id>/resourceGroups/rg-production"

# Assign Contributor to a group at subscription scope
az role assignment create \
--assignee-object-id "<group-object-id>" \
--assignee-principal-type Group \
--role "Contributor" \
--scope "/subscriptions/<sub-id>"

# Assign a role to a managed identity at resource scope
az role assignment create \
--assignee-object-id "<managed-identity-principal-id>" \
--assignee-principal-type ServicePrincipal \
--role "Storage Blob Data Reader" \
--scope "/subscriptions/<sub-id>/resourceGroups/rg-data/providers/Microsoft.Storage/storageAccounts/stproddata"

Built-In Roles

Azure has 400+ built-in roles. You will use about 10 of them regularly:

RoleScopeWhat It Can Do
OwnerFull controlEverything + manage role assignments
ContributorFull control minus RBACCreate/delete/modify all resources, cannot assign roles
ReaderRead-onlyView all resources, cannot modify anything
User Access AdministratorRBAC onlyManage role assignments, nothing else
Virtual Machine ContributorVMsManage VMs but not the VNet or storage they connect to
Storage Blob Data ContributorStorage data planeRead/write/delete blob data (not the account itself)
Key Vault Secrets UserKey Vault data planeRead secrets, cannot modify them
Network ContributorNetworkingManage VNets, NSGs, load balancers
SQL DB ContributorSQL databasesManage SQL databases, not the server or security
AcrPushContainer RegistryPush images to ACR
# List all built-in roles
az role definition list \
--type BuiltInRole \
--query "[].{Name:roleName, Description:description}" \
--output table

# Show the permissions for a specific role
az role definition list \
--name "Contributor" \
--query "[0].{Actions:permissions[0].actions, NotActions:permissions[0].notActions}" \
--output json

The critical distinction: control plane roles (Owner, Contributor, Reader) manage Azure resources. Data plane roles (Storage Blob Data Reader, Key Vault Secrets User) manage the data inside resources. A Contributor can manage a storage account's settings but cannot read the blobs inside it without a data plane role.

Custom Role Definitions

When built-in roles are too broad or too narrow, create a custom role. For example, a role that can restart VMs but not delete them:

{
"Name": "VM Operator",
"Description": "Can start, stop, and restart VMs but cannot create or delete them",
"Actions": [
"Microsoft.Compute/virtualMachines/start/action",
"Microsoft.Compute/virtualMachines/powerOff/action",
"Microsoft.Compute/virtualMachines/restart/action",
"Microsoft.Compute/virtualMachines/read",
"Microsoft.Compute/virtualMachines/instanceView/read",
"Microsoft.Network/networkInterfaces/read",
"Microsoft.Resources/subscriptions/resourceGroups/read"
],
"NotActions": [],
"DataActions": [],
"NotDataActions": [],
"AssignableScopes": [
"/subscriptions/<subscription-id>"
]
}
# Create the custom role
az role definition create --role-definition @vm-operator-role.json

# Assign the custom role
az role assignment create \
--assignee "oncall-team@company.com" \
--role "VM Operator" \
--scope "/subscriptions/<sub-id>/resourceGroups/rg-production"

# Update a custom role (add an action)
az role definition update --role-definition @vm-operator-role-updated.json

# Delete a custom role
az role definition delete --name "VM Operator"

Custom roles are scoped to the subscription(s) listed in AssignableScopes. You can make them available across multiple subscriptions or an entire management group.

Deny Assignments

Deny assignments explicitly block actions, overriding any role assignments that would allow them. They are more powerful than NotActions (which only exclude from the role itself) because deny assignments apply regardless of what roles the user has.

Deny assignments are primarily created by Azure Blueprints and Azure Managed Applications — you cannot create them directly via CLI or portal. They are used to protect critical resources from modification even by Owners.

# List deny assignments at a scope
az rest \
--method GET \
--url "https://management.azure.com/subscriptions/<sub-id>/providers/Microsoft.Authorization/denyAssignments?api-version=2022-04-01" \
--query "value[].{Name:denyAssignmentName, Principal:principals[0].displayName, Scope:scope}" \
--output table

A common pattern: Azure Blueprints lock critical networking resources with deny assignments so that even subscription Owners cannot modify the hub VNet or firewall rules without going through the change management process.

Conditional Access Policies

Conditional Access is the Azure Entra ID (formerly Azure AD) policy engine that adds conditions to authentication. It goes beyond RBAC — even if a user has the right role, Conditional Access can block their sign-in based on context.

Common Conditional Access policies every organization needs:

1. Require MFA for All Admins

{
"displayName": "Require MFA for admin roles",
"state": "enabled",
"conditions": {
"users": {
"includeRoles": [
"62e90394-69f5-4237-9190-012177145e10",
"f28a1f50-f6e7-4571-818b-6a12f2af6b6c",
"29232cdf-9323-42fd-ade2-1d097af3e4de"
]
},
"applications": {
"includeApplications": ["All"]
}
},
"grantControls": {
"operator": "OR",
"builtInControls": ["mfa"]
}
}

2. Block Legacy Authentication

# Legacy auth protocols (IMAP, SMTP, POP3) don't support MFA
# Block them entirely with a Conditional Access policy

az rest \
--method POST \
--url "https://graph.microsoft.com/v1.0/identity/conditionalAccess/policies" \
--body '{
"displayName": "Block legacy authentication",
"state": "enabled",
"conditions": {
"users": { "includeUsers": ["All"] },
"applications": { "includeApplications": ["All"] },
"clientAppTypes": ["exchangeActiveSync", "other"]
},
"grantControls": {
"operator": "OR",
"builtInControls": ["block"]
}
}'

3. Location-Based Access Restriction

# Create a named location for trusted office IPs
az rest \
--method POST \
--url "https://graph.microsoft.com/v1.0/identity/conditionalAccess/namedLocations" \
--body '{
"@odata.type": "#microsoft.graph.ipNamedLocation",
"displayName": "Corporate Offices",
"isTrusted": true,
"ipRanges": [
{"@odata.type": "#microsoft.graph.iPv4CidrRange", "cidrAddress": "203.0.113.0/24"},
{"@odata.type": "#microsoft.graph.iPv4CidrRange", "cidrAddress": "198.51.100.0/24"}
]
}'

Then create a policy that blocks Azure portal access from outside trusted locations — or at minimum requires MFA for untrusted locations.

Privileged Identity Management (PIM)

PIM eliminates permanent admin access. Instead of giving someone the Owner role permanently, they get it as an "eligible" assignment. When they need it, they activate the role for a limited time with justification and optional approval.

The workflow:

  1. Admin is assigned Owner as eligible (not active)
  2. Admin needs to perform a privileged operation
  3. Admin activates the role in the portal or via CLI
  4. Activation requires MFA + justification
  5. Optionally requires approval from another admin
  6. Role is active for 1-8 hours (configurable)
  7. Role automatically deactivates after the time window
# List eligible role assignments for the current user
az rest \
--method GET \
--url "https://graph.microsoft.com/v1.0/roleManagement/directory/roleEligibilityScheduleInstances?\$filter=principalId eq '<user-object-id>'" \
--query "value[].{Role:roleDefinitionId, Scope:directoryScopeId, Start:startDateTime, End:endDateTime}" \
--output table

# Activate an eligible role (self-service)
az rest \
--method POST \
--url "https://graph.microsoft.com/v1.0/roleManagement/directory/roleAssignmentScheduleRequests" \
--body '{
"action": "selfActivate",
"principalId": "<user-object-id>",
"roleDefinitionId": "<role-definition-id>",
"directoryScopeId": "/",
"justification": "Need to update network configuration for incident INC-2025-0847",
"scheduleInfo": {
"startDateTime": "2025-11-01T10:00:00Z",
"expiration": {
"type": "afterDuration",
"duration": "PT4H"
}
}
}'

PIM requires Azure AD Premium P2 licensing. It is worth the cost. The audit trail alone — knowing exactly who activated what role, when, why, and for how long — is invaluable during incident response and compliance audits.

Audit Logs

Every role assignment, activation, and access attempt is logged. Use these logs for security reviews and compliance.

# List recent role assignment changes
az monitor activity-log list \
--offset 7d \
--query "[?authorization.action=='Microsoft.Authorization/roleAssignments/write'].{Caller:caller, Time:eventTimestamp, Role:authorization.scope, Status:status.value}" \
--output table

# Get sign-in logs (requires Azure AD Premium)
az rest \
--method GET \
--url "https://graph.microsoft.com/v1.0/auditLogs/signIns?\$top=20&\$orderby=createdDateTime desc&\$filter=status/errorCode ne 0" \
--query "value[].{User:userDisplayName, App:appDisplayName, Status:status.failureReason, IP:ipAddress, Time:createdDateTime}" \
--output table

Set up diagnostic settings to send Azure AD sign-in and audit logs to a Log Analytics workspace. Create alert rules for:

  • Role assignments at subscription scope or higher
  • Failed sign-in attempts (possible brute force)
  • PIM role activations outside business hours
  • New Conditional Access policy changes

Best Practices for Least Privilege

  1. Never assign Owner at subscription scope to individuals. Use PIM for eligible access.
  2. Prefer groups over individuals for role assignments. Easier to manage and audit.
  3. Use data plane roles for accessing data (Storage Blob Data Reader, not Contributor).
  4. Scope roles to resource groups, not subscriptions, whenever possible.
  5. Review access quarterly using Access Reviews in Azure AD.
  6. Require MFA for all privileged roles via Conditional Access.
  7. Block legacy auth to prevent MFA bypass.
  8. Use managed identities for service-to-service communication instead of service principals with passwords.
  9. Set activation limits in PIM — 4 hours maximum for production Owner access.
  10. Monitor and alert on role assignment changes and unusual sign-in patterns.
# Create an access review for a resource group's role assignments
az rest \
--method POST \
--url "https://graph.microsoft.com/v1.0/identityGovernance/accessReviews/definitions" \
--body '{
"displayName": "Quarterly Production Access Review",
"scope": {
"query": "/v1.0/roleManagement/directory/roleAssignments?$filter=roleDefinitionId eq '\''<owner-role-id>'\''",
"queryType": "MicrosoftGraph"
},
"reviewers": [{ "query": "/v1.0/users/<security-team-lead-id>" }],
"settings": {
"mailNotificationsEnabled": true,
"reminderNotificationsEnabled": true,
"defaultDecision": "Deny",
"autoApplyDecisionsEnabled": true,
"recurrence": {
"pattern": { "type": "absoluteMonthly", "interval": 3 },
"range": { "type": "noEnd" }
}
}
}'

Wrapping Up

Identity is the new perimeter. RBAC controls what people can do, Conditional Access controls how they authenticate, and PIM controls when they have elevated access. Start by auditing your current role assignments — find every Owner and Contributor at subscription scope and move them to PIM eligible assignments. Block legacy authentication. Require MFA for every admin role. Create custom roles for operational teams that need specific actions without full Contributor access. Review access quarterly. The principle is simple: nobody gets more access than they need, and nobody keeps it longer than they need it.


Next up: We will explore Azure Automation — using runbooks, State Configuration, and Update Management to automate operational tasks, enforce desired state, and keep your VMs patched.