azure

Azure RBAC

July 20, 2026 🏷 security🏷 RBAC

How to protect Azure resource using RoleBasedAccess adhering to least priviledge principle

Azure Role-Based Access Control: A Deep Dive

Most Azure security incidents don’t start with a sophisticated exploit. They start with someone having more access than they should. Overly broad role assignments and poor identity management are consistently among the leading causes of cloud breaches — and they’re entirely preventable. Let’s get the basics right.

In this article I’ll walk you through Azure Role-Based Access Control (RBAC) from the ground up: how it works, when built-in roles aren’t enough, and how to apply least privilege in practice with a real-world example.


What Is Azure RBAC and Why Does It Matter?

Azure RBAC is the authorization system that controls who can do what across your Azure resources. Instead of granting blanket access and hoping for the best, RBAC lets you define precise permissions at the right level — whether that’s an entire management group or a single storage account.

The core idea is least privilege: every identity should have exactly the access it needs to do its job, and nothing more. A service principal that reads VM performance metrics has no business being able to redeploy that VM. RBAC is how you enforce that boundary.


Core Concepts: The Four Pillars

To understand how RBAC works, you need to know its four building blocks. Every permission in Azure comes down to these.

Security Principal — Who is asking for access?

A security principal is any identity that can request access to Azure resources. There are four types:

In practice, prefer groups over individual users and managed identities over service principals wherever possible. It keeps things clean and auditable.

Role Definition — What actions are allowed?

A role definition is a collection of permissions. It defines what a principal can and cannot do. Each role lists:

Azure ships with over 400 built-in roles covering most scenarios. When none of them fit precisely, you can define your own.

Scope — Where does it apply?

Scope defines which resources the role assignment covers. Azure scopes are hierarchical — permissions assigned at a higher level are inherited by everything below it:

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

Assign roles at the lowest scope that still satisfies the requirement. Assigning Owner at the subscription level when you only need access to one storage account is the kind of thing that creates incidents.

Role Assignment — Putting It All Together

A role assignment is the binding that connects a security principal, a role definition, and a scope. When Azure evaluates whether an identity can perform an action, it looks for a matching role assignment. If one exists at the relevant scope, access is granted. No assignment, no access.


Built-in Roles vs Custom Roles

In most cases, Azure’s built-in roles will cover what you need. But there are situations where they don’t fit precisely enough.

FeatureBuilt-in rolesCustom roles
OriginCreated and maintained by MicrosoftCreated and managed by your organization
ModificationCannot be edited or deletedFully editable, updatable, and deletable
AvailabilityGlobally available at any scopeRestricted to defined AssignableScopes
SetupReady out of the boxRequires defining permissions then assigning
LimitsUnlimitedMaximum 5,000 per Microsoft Entra tenant

When do custom roles make sense?

One prerequisite worth knowing: creating custom roles requires Microsoft Entra ID P1 or P2.


Real-World Example: Scoping a Cosmos DB Role to Least Privilege

Let me walk through a scenario I’ve worked through that shows exactly when a custom role earns its place.

We have an Azure Function that reads and updates a single value in a Cosmos DB container — a visitor counter. The straightforward choice would be to assign the Cosmos DB Built-in Data Contributor role. But when you look at what that role actually allows, it’s far broader than what the function needs:

Our function only needs to read an item and replace its value. Everything else is unnecessary exposure. So we create a custom role with exactly those two data actions.

Step 1: Define the role

Create a JSON file describing the role — its name, the scope it applies to, and the specific permissions it grants:

cat > role-definition.json << 'EOF'
{
  "RoleName": "VisitorCounterReadReplace",
  "Type": "CustomRole",
  "AssignableScopes": [
    "/dbs/<cosmos-account-name>/colls/<container-id>"
  ],
  "Permissions": [
    {
      "DataActions": [
        "Microsoft.DocumentDB/databaseAccounts/readMetadata",
        "Microsoft.DocumentDB/databaseAccounts/sqlDatabases/containers/items/read",
        "Microsoft.DocumentDB/databaseAccounts/sqlDatabases/containers/items/replace"
      ]
    }
  ]
}
EOF

Notice the AssignableScopes is pinned to the specific container — not the database, not the account. Narrowest possible scope.

Step 2: Create the role in Azure

Register the role definition against your Cosmos DB account:

az cosmosdb sql role definition create \
  --account-name <cosmos-account-name> \
  --resource-group <rg-name> \
  --body @role-definition.json

This returns a role definition ID you’ll need in the next step.

Step 3: Assign the role to the function’s managed identity

az cosmosdb sql role assignment create \
  --account-name <cosmos-account-name> \
  --resource-group <rg-name> \
  --scope "/dbs/<db-name>/colls/<container-id>" \
  --principal-id <function-managed-identity-id> \
  --role-definition-id <role-definition-id-from-previous-step>

Step 4: Remove the overly broad assignment

Now that the tighter role is in place, remove the Cosmos DB Built-in Data Contributor assignment. First, list existing assignments to find the one to delete:

az cosmosdb sql role assignment list \
  --account-name <cosmos-account-name> \
  --resource-group <rg-name> \
  --output json

Then delete it by ID:

az cosmosdb sql role assignment delete \
  --account-name <cosmos-account-name> \
  --resource-group <rg-name> \
  --role-assignment-id <assignment-id>

The function now has exactly two data actions available to it. Nothing more.


Best Practices

After working through RBAC configurations across different environments, these are the principles I keep coming back to:

Assign roles to groups, not individuals. When you assign a role directly to a user, you create a management problem that compounds over time. Use Entra ID groups — when someone leaves or changes teams, you update group membership, not role assignments.

Apply roles at the lowest effective scope. If access is only needed for a single resource group, assign the role there — not at the subscription level. The scope hierarchy makes it easy to be lazy here; don’t be.

Avoid the Owner role unless strictly necessary. Owner grants full control including the ability to assign roles to others. It should be rare and tightly audited. Contributor covers most administrative needs without the role-assignment capability.

Use managed identities for workloads, not service principals with secrets. Managed identities eliminate credential management entirely. For any workload running on Azure infrastructure, there is almost no reason to use a service principal with a client secret instead.

Audit role assignments regularly. Access has a tendency to accumulate. Use Azure Policy or Entra ID Access Reviews to periodically verify that assignments still reflect actual requirements. The assignment you created for a contractor six months ago is still active until someone explicitly removes it.

Follow the principle of least privilege from day one. It is significantly harder to reduce permissions in an existing environment than to start with narrow access and expand it when needed.


Wrapping Up

Azure RBAC is one of those foundational pieces that most teams treat as an afterthought until something goes wrong. Getting it right from the start — choosing the right scope, using groups, reaching for custom roles when built-ins are too broad — is what separates a secure environment from one that’s one misconfiguration away from a bad day.

The Cosmos DB example in this article is a pattern I’ve applied across different scenarios: start with what the workload actually needs, check whether a built-in role covers it precisely, and create a custom role when it doesn’t. It takes a bit more upfront work but the reduction in exposure is worth it every time.

If you’re building on Azure and want to go further, Microsoft’s RBAC documentation and the Entra ID built-in roles reference are the two resources I keep open most often.

Back to Blog