← Back to articles PowerShell

Azure Automation Managed Identity Permissions: The Complete Guide

Azure Automation Managed Identity Permissions: The Complete Guide

The Scenario

We were running a mid-sized Intune environment — roughly 4,200 devices across three Azure subscriptions — with a handful of Azure Automation runbooks handling device compliance reporting, dynamic group population, and a nightly Intune inventory export. Everything worked fine in the dev tenant. We promoted to production, the runbooks ran without errors, but the output was consistently empty. No failures. No exceptions. Just... nothing.

Three hours later, after staring at logs that were politely lying to us, we traced the issue to the Automation Account's System-Assigned Managed Identity. It had been created, but never properly granted Microsoft Graph API permissions. The runbooks were authenticating successfully — which is why there were no auth errors — but silently failing every API call that required a specific scope.

That experience is what prompted us to build a repeatable process for auditing and configuring Managed Identity permissions before any runbook goes near production. This article is that process, fully documented.

Why This Matters

Managed Identities are the right way to authenticate Azure Automation runbooks to Microsoft Graph and other Azure services. No stored credentials. No service account passwords rotting in a Key Vault secret that someone forgot to rotate. No client secrets expiring at 2 AM on a Saturday. Microsoft's own security guidance pushes hard toward Managed Identities for exactly these reasons.

But the security model that makes them safe also makes permission misconfigurations uniquely painful. When a traditional service account lacks permissions, you usually get a clear 403 Forbidden or an AADSTS error code you can Google. When a Managed Identity lacks Graph API application permissions, the behavior depends entirely on how the SDK or REST client handles the error — and many runbooks swallow those errors or return empty collections that look like legitimate empty results.

The official Microsoft documentation covers how to assign Managed Identity permissions, but it doesn't adequately warn you about the silent failure modes, the difference between delegated and application permissions in this context, or the fact that admin consent is a separate, mandatory step that is easy to miss.

From a compliance standpoint, over-permissioned identities are just as problematic as under-permissioned ones. If your runbook only needs DeviceManagementConfiguration.ReadWrite.All, it shouldn't also have Directory.ReadWrite.All because someone once pasted a broader permission set. Least-privilege matters, and you cannot enforce it if you can't reliably audit what permissions exist.

Root Cause Analysis

How Managed Identity Permissions Actually Work

When you enable a System-Assigned Managed Identity on an Azure Automation Account, Azure creates a service principal in your Entra ID tenant. This principal has an Object ID (also referred to as PrincipalId). The identity itself is automatically lifecycle-managed — it's created when you enable it and deleted when you delete the Automation Account.

What Azure does not do automatically is grant that service principal any Microsoft Graph API permissions. That step is entirely manual, and it's where most teams get burned.

Microsoft Graph uses OAuth 2.0 application permissions (also called app roles) for non-interactive, background workloads like runbooks. These are different from delegated permissions, which require a signed-in user. For automation, you always want application permissions — the Role type in Azure CLI terminology.

After permissions are assigned, they still require admin consent before they're active. This is a separate action. Many engineers assign the permissions and assume they're live. They're not until consent is granted.

Investigating the Current State

Before you fix anything, you need a clear picture of what's actually assigned. Here's the investigation workflow we use.

Step 1: Authenticate to Azure and set context

If you're in an environment where browser-based login is unreliable (headless servers, jump boxes, restricted proxies), device code authentication is your friend:

# Device code login — works everywhere
Connect-AzAccount -DeviceCode

# List available subscriptions
Get-AzSubscription

# Set the correct subscription context
Set-AzContext -Subscription "your-subscription-id-here"

# Verify you're in the right place
Get-AzContext | Select-Object Name, Subscription, Tenant

That last verification step matters. Especially if you manage multiple tenants or subscriptions, confirming context before running anything destructive is non-negotiable.

Step 2: Retrieve the Automation Account's Managed Identity

# Pull the Automation Account object
$aa = Get-AzAutomationAccount `
  -ResourceGroupName "your-resource-group" `
  -Name "your-automation-account-name"

# Inspect the identity
$aa.Identity

# Output example:
# Type        : SystemAssigned
# PrincipalId : a1b2c3d4-e5f6-7890-abcd-ef1234567890
# TenantId    : 99887766-5544-3322-1100-aabbccddeeff

# Store the PrincipalId for later use
$principalId = $aa.Identity.PrincipalId
Write-Output "Managed Identity Principal ID: $principalId"

If $aa.Identity returns null or the Type shows None, the Managed Identity hasn't been enabled yet. Go to the Automation Account in the Azure Portal, navigate to Identity, and toggle System Assigned to On.

Step 3: Audit currently assigned Graph permissions

This is where things get interesting. The Microsoft Graph PowerShell SDK can query this, but for a quick audit, the Azure CLI gives you a cleaner output. Install it from aka.ms/azure-cli if you haven't already.

# Login to Azure CLI (separate session from Az PowerShell)
az login

# List all permissions assigned to the Managed Identity
az ad app permission list --id $principalId --output table

# For a more detailed view including consent status
az ad app permission list-grants --id $principalId --output json

Alternatively, using the Microsoft Graph PowerShell module — which gives you more programmatic control:

# Connect with the right scopes
Connect-MgGraph -Scopes "Application.Read.All"

# Get all app role assignments for the service principal
$spAppRoles = Get-MgServicePrincipalAppRoleAssignment `
  -ServicePrincipalId $principalId

# Resolve the permission names from their GUIDs
$graphSpId = (Get-MgServicePrincipal -Filter "appId eq '00000003-0000-0000-c000-000000000000'").Id
$graphSp = Get-MgServicePrincipal -ServicePrincipalId $graphSpId -ExpandProperty AppRoles

$spAppRoles | ForEach-Object {
  $roleId = $_.AppRoleId
  $roleName = ($graphSp.AppRoles | Where-Object { $_.Id -eq $roleId }).Value
  [PSCustomObject]@{
    Permission    = $roleName
    AssignedTo    = $_.PrincipalDisplayName
    CreatedDateTime = $_.CreatedDateTime
  }
} | Format-Table -AutoSize

This gives you human-readable permission names with timestamps — much more useful for auditing than raw GUIDs.

The Solution

Assigning the Right Permissions

Before assigning anything, define exactly what your runbooks need. Here's a reference for common Intune automation scenarios:

  • DeviceManagementConfiguration.Read.All — Read device configuration policies
  • DeviceManagementConfiguration.ReadWrite.All — Create/modify configuration policies
  • DeviceManagementApps.Read.All — Read app assignments and install status
  • DeviceManagementManagedDevices.Read.All — Read managed device inventory
  • DeviceManagementManagedDevices.ReadWrite.All — Trigger remote actions, wipe devices
  • Group.Read.All — Read group membership for dynamic targeting
  • Mail.Send — Send email via Graph (for report distribution)
  • Directory.Read.All — Read directory objects, user/device attributes

Principle of least privilege: Only assign what each specific runbook needs. If you have multiple runbooks with different permission requirements, consider separate Automation Accounts or use User-Assigned Managed Identities scoped per workload.

Assigning permissions via Azure CLI:

# Microsoft Graph App ID — this never changes across tenants
$graphApiId = "00000003-0000-0000-c000-000000000000"

# Assign individual permissions (application type = Role)
az ad app permission add \
  --id $principalId \
  --api $graphApiId \
  --api-permissions DeviceManagementConfiguration.Read.All=Role

az ad app permission add \
  --id $principalId \
  --api $graphApiId \
  --api-permissions DeviceManagementManagedDevices.Read.All=Role

az ad app permission add \
  --id $principalId \
  --api $graphApiId \
  --api-permissions Group.Read.All=Role

# Grant admin consent — this step is MANDATORY
az ad app permission admin-consent --id $principalId

Assigning permissions via Microsoft Graph PowerShell (preferred for scripted pipelines):

Connect-MgGraph -Scopes "AppRoleAssignment.ReadWrite.All", "Application.Read.All"

# Get the Graph service principal
$graphSp = Get-MgServicePrincipal -Filter "appId eq '00000003-0000-0000-c000-000000000000'"

# Define permissions to assign
$permissionsToAssign = @(
  "DeviceManagementConfiguration.Read.All",
  "DeviceManagementManagedDevices.Read.All",
  "Group.Read.All"
)

foreach ($permission in $permissionsToAssign) {
  # Find the AppRole ID for this permission
  $appRole = $graphSp.AppRoles | Where-Object {
    $_.Value -eq $permission -and $_.AllowedMemberTypes -contains "Application"
  }

  if ($null -eq $appRole) {
    Write-Warning "Permission not found: $permission"
    continue
  }

  # Check if already assigned
  $existing = Get-MgServicePrincipalAppRoleAssignment `
    -ServicePrincipalId $principalId | Where-Object { $_.AppRoleId -eq $appRole.Id }

  if ($existing) {
    Write-Output "Already assigned: $permission"
    continue
  }

  # Assign the permission
  New-MgServicePrincipalAppRoleAssignment `
    -ServicePrincipalId $principalId `
    -PrincipalId $principalId `
    -ResourceId $graphSp.Id `
    -AppRoleId $appRole.Id

  Write-Output "Assigned: $permission"
}

Note that New-MgServicePrincipalAppRoleAssignment grants consent implicitly when called with Global Admin credentials — no separate admin-consent step required when using this approach.

Validating the Configuration

Never assume permissions are working until you've validated them. Add this check to your runbook onboarding process:

# Validation: confirm permissions are active
$assignments = Get-MgServicePrincipalAppRoleAssignment -ServicePrincipalId $principalId
$graphSp = Get-MgServicePrincipal -Filter "appId eq '00000003-0000-0000-c000-000000000000'" `
  -ExpandProperty AppRoles

Write-Output "=== Active Graph Permissions for Managed Identity ==="
$assignments | ForEach-Object {
  $roleId = $_.AppRoleId
  $roleName = ($graphSp.AppRoles | Where-Object { $_.Id -eq $roleId }).Value
  Write-Output "  [OK] $roleName"
}

For a deeper validation, you can also reference the Microsoft Graph permissions reference to cross-check that the permissions you've assigned match the exact scope names expected by the API endpoints your runbooks call.

Scaling Considerations

At small scale — a handful of runbooks in a single tenant — a manual permission assignment workflow is fine. At scale, things get complicated quickly.

Multiple Automation Accounts Across Subscriptions

If you're managing 10+ Automation Accounts across multiple subscriptions or tenants, manual permission management becomes error-prone. The PowerShell Graph approach above adapts well to a centralized script that iterates across accounts:

# Loop across multiple Automation Accounts
$automationAccounts = @(
  @{ RG = "rg-prod-automation"; Name = "aa-intune-reporting" },
  @{ RG = "rg-prod-automation"; Name = "aa-device-lifecycle" },
  @{ RG = "rg-dev-automation";  Name = "aa-dev-testing" }
)

foreach ($account in $automationAccounts) {
  $aa = Get-AzAutomationAccount `
    -ResourceGroupName $account.RG `
    -Name $account.Name
  
  $pid = $aa.Identity.PrincipalId
  Write-Output "Processing: $($account.Name) — Principal: $pid"
  # ... run assignment logic here
}

Throttling and API Rate Limits

When running batch permission assignments via Graph API, be aware of throttling. The AppRoleAssignment endpoints are not heavily throttled, but if you're processing dozens of accounts in a tight loop, add a Start-Sleep -Seconds 1 between assignments to avoid transient 429 responses.

User-Assigned vs System-Assigned Managed Identities

For large environments with many runbooks sharing the same permission set, User-Assigned Managed Identities are worth considering. You create and manage the identity independently of any specific Automation Account, assign permissions once, and attach the same identity to multiple accounts. This eliminates the problem of needing to re-grant permissions every time a new Automation Account is provisioned. See the Microsoft documentation on Automation Managed Identities for the configuration steps.

Permission Drift Over Time

Permissions get added during incidents and forgotten. Build a quarterly audit into your operations calendar. The Graph PowerShell validation script above takes under 30 seconds to run and should be part of any Intune automation health check.

Lessons Learned

  • Silent failures are the most expensive kind. A runbook that returns empty data looks like a legitimate empty result to anyone who doesn't know the expected output. Always build explicit permission validation into runbook startup logic — fail loudly if required scopes aren't present.
  • Admin consent is not optional and it is not automatic. This single step is responsible for a disproportionate share of "I assigned the permissions but it still doesn't work" support tickets. If you use the Azure CLI path, run az ad app permission admin-consent explicitly. If you use the Graph PowerShell path with appropriate admin credentials, consent is granted as part of the assignment.
  • Application permissions and delegated permissions are not interchangeable. Automation runbooks run without an interactive user context. They require application permissions (type: Role). If you accidentally assign delegated permissions (type: Scope), the runbook will authenticate but every API call will return a 403 because there's no user token to delegate from.
  • The Graph API ID 00000003-0000-0000-c000-000000000000 is a universal constant. It's the same across every tenant, every environment, every region. You can hardcode it safely. Knowing this saves time when writing automation — you don't need to look it up dynamically.
  • Audit what you've assigned, not just what you think you assigned. Copy-paste errors in permission names are silent — the CLI or SDK will often accept an unrecognized permission name without warning, assign nothing meaningful, and return success. Always run the validation step and confirm human-readable permission names are present in the output before closing the ticket.

🎓 Ready to go deeper?

Practice real MD-102 exam questions, get AI feedback on your weak areas, and fast-track your Intune certification.

Start Free Practice → Book a Session
Souhaiel Morhag
Souhaiel Morhag
Microsoft Endpoint & Modern Workplace Engineer

Souhaiel Morhag is a Microsoft Intune and endpoint management specialist with hands-on experience deploying and securing enterprise environments across Microsoft 365. He founded MSEndpoint.com to share practical, real-world guides for IT admins navigating Microsoft technologies — and built the MSEndpoint Academy at app.msendpoint.com/academy, a dedicated learning platform for professionals preparing for the MD-102 (Microsoft 365 Endpoint Administrator) certification. Through in-depth articles and AI-powered practice exams, Souhaiel helps IT teams move faster and certify with confidence.

Related Articles

Popular on MSEndpoint