DEV Community

Cover image for Least-privilege Azure RBAC for read-only architecture review
Prateek Singh for Cloudeval AI

Posted on Originally published at docs.cloudeval.ai

Least-privilege Azure RBAC for read-only architecture review

TL;DR: Least-privilege Azure RBAC for a read-only architecture scanner is the Reader action */read plus two actions: Microsoft.Resources/deployments/exportTemplate/action and Microsoft.Network/networkWatchers/topology/action. Put them in a custom role with no data actions, assign it to a dedicated service principal at resource-group scope, and don't use Contributor for normal sync.

By Prateek Singh, founder of Ganak AI Labs. Updated 6 October 2026.

The fastest way to stall a security review is a setup guide that starts with az ad sp create-for-rbac --role Contributor.

The tool only wants to look at your Azure environment. So why should it be able to change it?

Usually it shouldn't. Most architecture tools need to read Azure Resource Manager metadata, plus one or two specific actions. Contributor shows up in quick-start guides because it's one line and always works.

Here's a least-privilege setup for read-only architecture review: what each permission is for, how to scope it and how to fix gaps without widening the role. The example is Cloudeval's Cloud sync, but the pattern fits any read-only scanner.

Disclosure: I'm the founder of Cloudeval.

Key terms used below

  • Azure RBAC: Azure role-based access control. A role definition lists allowed actions (control plane) and data actions (data plane). A role assignment grants it to a principal at a scope.
  • Service principal: the identity an application uses to sign in to Microsoft Entra ID and call Azure.
  • Cloud sync: Cloudeval's live Azure path. It reads deployed resources, exports templates for analysis and stores a project snapshot.

Why do scanners ask for Contributor in the first place?

Because it's convenient. Contributor can manage everything except access, so nothing fails during setup.

That convenience is the problem. A credential that can delete resources is a far bigger risk to store in a third-party service than one that can only read.

Microsoft's Azure RBAC best practices say it plainly: grant the least privilege needed, and "avoid assigning broader roles at broader scopes even if it initially seems more convenient."

Cloudeval Cloud sync doesn't need Contributor for normal sync. The full list is on the Azure Sync Permissions for Cloudeval page.

What permissions does a read-only architecture review need?

Permission What it allows Why Cloud sync uses it
*/read Read control plane information for all Azure resources (the only action in the built-in Reader role) Read subscription, resource group, resource, provider, deployment and topology metadata in scope
Microsoft.Resources/deployments/exportTemplate/action Export the template for a deployment Build report inputs from live resources
Microsoft.Network/networkWatchers/topology/action Get a network-level view of resources and their relationships in a resource group Add Network Watcher topology to the live architecture graph
Data actions None Not needed. No blob contents, no Key Vault secret values

That's Reader plus two actions. Nothing else goes in the role.

How do I set up least-privilege Azure RBAC for Cloud sync?

  1. Create a dedicated service principal with no role assigned.
  2. Define a custom role with */read and the two actions, and no data actions.
  3. Assign it at resource-group scope, plus NetworkWatcherRG if you want topology.
  4. Connect Cloudeval with those credentials and confirm what it can see.
  5. Fix gaps by adding the specific missing action, not a broader role.

Step 1: Create a dedicated service principal

Use one principal per tool, so you can rotate or revoke it without touching anything else.

Current Azure CLI versions don't assign a role in az ad sp create-for-rbac unless you pass --role. So the principal starts with no access:

SUB_ID="$(az account show --query id -o tsv)"
RG="<target-resource-group>"
SP_NAME="cloudeval-live-sync"

SP_JSON="$(az ad sp create-for-rbac --name "$SP_NAME" -o json)"
APP_ID="$(echo "$SP_JSON" | jq -r .appId)"
CLIENT_SECRET="$(echo "$SP_JSON" | jq -r .password)"
TENANT_ID="$(echo "$SP_JSON" | jq -r .tenant)"
Enter fullscreen mode Exit fullscreen mode

Treat CLIENT_SECRET like any production secret. Keep it out of shared terminals and CI logs, store it in your secret manager and note its expiry date.

An expired secret is one of the most common reasons a connection test fails later.

Step 2: Define the custom role

{
  "Name": "CloudEval Live Sync Reader",
  "IsCustom": true,
  "Description": "Read Azure resources and export deployment templates for Cloudeval Cloud sync.",
  "Actions": [
    "*/read",
    "Microsoft.Resources/deployments/exportTemplate/action",
    "Microsoft.Network/networkWatchers/topology/action"
  ],
  "NotActions": [],
  "DataActions": [],
  "NotDataActions": [],
  "AssignableScopes": ["/subscriptions/<subscription-id>"]
}
Enter fullscreen mode Exit fullscreen mode
az role definition create --role-definition cloudeval-live-sync-reader.json
Enter fullscreen mode Exit fullscreen mode

AssignableScopes controls where the role can be assigned, not where it is assigned. To change assignable scopes on an existing definition, run az role definition update with the same JSON.

Step 3: Assign it at resource-group scope

Resource-group scope is the right default:

az role assignment create \
  --assignee "$APP_ID" \
  --role "CloudEval Live Sync Reader" \
  --scope "/subscriptions/$SUB_ID/resourceGroups/$RG"
Enter fullscreen mode Exit fullscreen mode

Repeat it for each resource group you want reviewed, and pick the same groups in the Cloudeval connection form.

Network Watcher usually lives in its own group, NetworkWatcherRG by default. If you want topology and Network Watcher sits outside your target group, assign the role there too.

Skip that and Cloud sync still reads resources and exports templates, but network relationships may be sparse.

Use subscription scope only when the tool should sync every resource group it can reach. It's a deliberate widening, so write down why.

Step 4: Connect Cloudeval and confirm what it sees

In Cloudeval, open Connections, then Add connection, and choose Azure and Cloud sync. Enter the client ID, client secret, tenant ID, subscription ID and target resource groups.

Cloudeval validates the credentials when it creates the connection. Then create a project from that connection and start the sync.

Cloudeval New Project dialog at the Connection Selection step listing available Azure connections to include in the project
Link an existing Azure connection when you create a project.

The CLI does the same in one command. It reads the AZURE_* environment variables when you leave out the flags:

cloudeval projects create \
  --cloud-sync \
  --azure-tenant-id "$TENANT_ID" \
  --azure-client-id "$APP_ID" \
  --azure-client-secret "$CLIENT_SECRET" \
  --azure-subscription-id "$SUB_ID" \
  --resource-group "$RG" \
  --name "Cloud sync $RG" \
  --format json \
  --output ./cloudeval-project.json
Enter fullscreen mode Exit fullscreen mode

Here's the two-minute product demo, including connecting live Azure:

Start with one representative resource group. With a huge multi-subscription scope, it's hard to tell whether a gap comes from permissions or from the scope.

Step 5: Fix gaps without widening the role

When something looks incomplete, the fix is almost always one action or one assignment, not Contributor:

Symptom Likely cause Fix
Connection authenticates, but no resource groups appear Assignment hasn't propagated, or it's on another subscription or group Wait a few minutes, check the scope, test again
Resource group is visible, but reports fail to build Role is missing deployments/exportTemplate/action Update the role definition and re-run sync
Graph works, but network relationships are sparse Missing networkWatchers/topology/action, or no assignment on NetworkWatcherRG Add the action, assign on the Network Watcher group
Sync includes too many resource groups Role assigned at subscription scope, or no target groups selected in Cloudeval Assign at resource-group scope and select target groups

Cloudeval Evidence and Progress report tab for an Azure project showing artifacts, warnings, assumptions and report history
Check warnings and assumptions here after a sync that looks incomplete.

Re-run the sync only after you've confirmed the scope. Otherwise you just get a fresher copy of the wrong project.

Is the built-in Reader role enough?

Almost, and that's the point. Reader is exactly */read, with no other actions and no data actions.

For Cloud sync it's missing the two actions above. Without exportTemplate, report inputs can be incomplete. Without topology, the network graph is thinner.

Two caveats:

  • */read is a wildcard. Microsoft recommends listing actions explicitly in custom roles, because a wildcard also picks up future actions. Here it covers reads only and mirrors Reader, so your main lever is scope. Assign per resource group, and review the role if you need a stricter list.
  • Control plane only. With no data actions, the principal can't read blob contents or Key Vault secret values. If a report seems to expect them, don't add data actions to "fix" it. Cloud sync works from management-plane metadata.

Frequently asked questions

Does Cloudeval Cloud sync need the Contributor role?

No. Normal Cloud sync needs */read, Microsoft.Resources/deployments/exportTemplate/action and, for topology, Microsoft.Network/networkWatchers/topology/action. Keep Contributor for disposable smoke tests in a throwaway resource group.

Is the Azure Reader role enough for a read-only architecture scanner?

Not quite, for Cloudeval. Reader grants only */read. Cloud sync also uses the deployment export action to build report inputs and the Network Watcher topology action to map network relationships.

Can Cloudeval read Key Vault secrets or storage blob contents?

No. The recommended role has no data actions, and Cloud sync uses management-plane metadata only. Don't add data actions unless a separate workflow explicitly needs them.

Should I assign the role at subscription or resource-group scope?

Use resource-group scope by default. Choose subscription scope only when the tool should discover and sync every accessible resource group in that subscription.

Why are network relationships missing after a sync?

Usually the role lacks Microsoft.Network/networkWatchers/topology/action, or it isn't assigned on the resource group that holds Network Watcher, often NetworkWatcherRG. Add the action or the assignment and sync again.

Takeaway

A read-only scanner should have a read-only role. For Cloudeval, that's Reader's */read plus two named actions and no data actions, assigned per resource group to its own service principal.

The full reference, with the complete script, is Azure Sync Permissions for Cloudeval.

Related on cloudeval.ai

Try it

Create the custom role on one non-production resource group, then connect it. Open the Cloudeval app and follow Connect an Azure environment.

About the author

Prateek Singh is the founder of Ganak AI Labs and builds Cloudeval AI.

cloudeval.ai | Docs | YouTube | GitHub | LinkedIn

Top comments (0)