Showing posts with label AzureSecurity. Show all posts
Showing posts with label AzureSecurity. Show all posts

Sunday, July 12, 2026

Azure Managed Identity: From Beginner to Advanced

Azure Managed Identity: From Beginner to Advanced

Managing secrets is one of the most painful parts of building cloud applications. Connection strings in config files, client secrets that expire at 2 AM, certificates someone forgot to renew — we have all been there. Azure Managed Identity solves this problem by removing credentials from your code entirely.

This post takes you from the basics all the way to production best practices, with everything verified against the latest Microsoft Learn documentation.


Part 1: The Basics (Beginner)

What is a Managed Identity?

A managed identity is an automatically managed identity in Microsoft Entra ID that Azure resources can use to authenticate to any service that supports Microsoft Entra authentication — without you storing any credentials in code, config, or Key Vault.

Key points:

  • Azure creates and manages the credentials behind the scenes. You never see them, rotate them, or store them.
  • Managed identities are free — there is no extra cost with Microsoft Entra ID.
  • The feature was formerly called Managed Service Identity (MSI) — you will still see "MSI" in older docs, blogs, and even some parameter names.

The Two Types

Property System-assigned User-assigned
Creation Created as part of an Azure resource (VM, App Service, Function App, etc.) Created as a standalone Azure resource
Lifecycle Tied to the parent resource — deleted when the resource is deleted Independent — must be explicitly deleted
Sharing Cannot be shared; belongs to exactly one resource Can be assigned to multiple resources
Common use cases Workloads contained in a single resource that need an independent identity Workloads spanning multiple resources; pre-authorization scenarios; resources that get recycled frequently but need consistent permissions

Microsoft's current recommendation: User-assigned managed identities are the recommended type for most scenarios, because they can be provisioned independently of compute, pre-authorized before deployment, and shared across resources.

Three Core Concepts

Every managed identity is backed by a special service principal in Microsoft Entra ID. Three IDs matter:

  1. Client ID — a unique identifier tied to the identity's application registration. Used when your code needs to pick a specific user-assigned identity.
  2. Principal ID (Object ID) — the object ID of the service principal. Used when assigning RBAC roles.
  3. Service Principal — the Entra ID object that represents the identity in your tenant.

How Authentication Works (The Mental Model)

  1. You enable a managed identity on a resource (say, a VM).
  2. Azure creates a service principal in your Entra tenant.
  3. You grant that identity an RBAC role on a target resource (e.g., Storage Blob Data Reader on a storage account).
  4. Your code asks a local, non-routable endpoint for a token — no secrets involved.
  5. Azure returns a Microsoft Entra access token (JWT).
  6. Your code presents that token to the target service.

The magic is in step 4: the token endpoint is only reachable from inside the resource itself.


Part 2: Hands-On (Getting Started)

Enable a System-Assigned Identity

Azure portal: Resource → Identity blade → System assigned tab → set Status to On → Save.

Azure CLI:

# On a VM
az vm identity assign --name MyVM --resource-group MyRG

# On a Web App
az webapp identity assign --name MyWebApp --resource-group MyRG

PowerShell:

# On a VM
$vm = Get-AzVM -ResourceGroupName "MyRG" -Name "MyVM"
Update-AzVM -ResourceGroupName "MyRG" -VM $vm -IdentityType SystemAssigned

Create and Assign a User-Assigned Identity

# 1. Create the identity
az identity create --name MyUAMI --resource-group MyRG

# 2. Assign it to a VM
az vm identity assign --name MyVM --resource-group MyRG \
  --identities /subscriptions/<sub-id>/resourcegroups/MyRG/providers/Microsoft.ManagedIdentity/userAssignedIdentities/MyUAMI

Grant Permissions with RBAC

The identity is useless until it has permissions. Assign a role scoped as narrowly as possible:

az role assignment create \
  --assignee <principal-id-of-managed-identity> \
  --role "Storage Blob Data Reader" \
  --scope /subscriptions/<sub-id>/resourceGroups/MyRG/providers/Microsoft.Storage/storageAccounts/mystorageacct

See It Work: Request a Token Manually

From inside an Azure VM, call the Azure Instance Metadata Service (IMDS) endpoint:

curl 'http://169.254.169.254/metadata/identity/oauth2/token?api-version=2018-02-01&resource=https%3A%2F%2Fmanagement.azure.com%2F' \
  -H "Metadata: true" -s

What each piece means:

  • 169.254.169.254 — the IMDS endpoint, a well-known non-routable IP reachable only from inside the VM.
  • api-version=2018-02-01 — use this version or later.
  • resource — the audience of the token (here, Azure Resource Manager). Note the trailing slash — the value must exactly match what Entra ID expects.
  • Metadata: true header — mandatory; it mitigates server-side request forgery (SSRF) attacks.

If the VM has multiple user-assigned identities, add client_id=<client-id> (or object_id / msi_res_id) to specify which identity to use.


Part 3: Using Managed Identity in Code (Intermediate)

You should almost never call IMDS directly in real applications. Use the Azure Identity client library instead — it handles endpoint discovery, caching, and retries across all Azure hosting environments (VM, App Service, Functions, AKS, Container Apps, Arc).

C# (.NET)

using Azure.Identity;
using Azure.Storage.Blobs;

// System-assigned identity
var credential = new ManagedIdentityCredential();

// User-assigned identity (specify the client ID)
var uamiCredential = new ManagedIdentityCredential(
    ManagedIdentityId.FromUserAssignedClientId("<client-id>"));

var blobClient = new BlobServiceClient(
    new Uri("https://<storage-account>.blob.core.windows.net"),
    credential);

Python

from azure.identity import ManagedIdentityCredential
from azure.storage.blob import BlobServiceClient

# System-assigned
credential = ManagedIdentityCredential()

# User-assigned
credential = ManagedIdentityCredential(client_id="<client-id>")

blob_client = BlobServiceClient(
    account_url="https://<storage-account>.blob.core.windows.net",
    credential=credential
)

PowerShell (e.g., in Azure Automation or an Azure VM)

# Connect to Azure using the managed identity
Connect-AzAccount -Identity

# Connect using a specific user-assigned identity
Connect-AzAccount -Identity -AccountId "<client-id>"

# Get a token for a specific resource
$token = Get-AzAccessToken -ResourceUrl "https://graph.microsoft.com"

DefaultAzureCredential: Convenient, But Know the Trade-offs

DefaultAzureCredential tries a chain of authentication methods and picks whichever works at runtime. Locally it uses your Visual Studio / Azure CLI / Azure PowerShell login; in Azure it discovers the managed identity — no code changes between environments.

var credential = new DefaultAzureCredential(); // great for getting started

This is fantastic for the dev inner loop, but it has real production drawbacks (covered in Part 5).


Part 4: Advanced Scenarios

The Token Endpoint Varies by Host

Different Azure services expose the managed identity endpoint differently — another reason to use the SDK rather than raw HTTP:

Host Endpoint
Azure VMs, VM Scale Sets, AKS/ACI containers http://169.254.169.254/metadata/identity/oauth2/token (IMDS)
App Service / Functions / Container Apps IDENTITY_ENDPOINT + IDENTITY_HEADER environment variables
Azure Arc-enabled servers http://localhost:40342/metadata/identity/oauth2/token (Hybrid IMDS), with an extra challenge-token step
Service Fabric IDENTITY_ENDPOINT + IDENTITY_HEADER + IDENTITY_SERVER_THUMBPRINT

Azure Arc: Managed Identity Beyond Azure

Arc-enabled servers (on-premises or other clouds) get a managed identity too, via the Hybrid Instance Metadata Service (HIMDS). The token flow is multi-step: request a challenge token file (readable only by privileged users), then exchange it for the access token. This design ensures only higher-privileged processes on the machine can obtain tokens.

AKS: Workload Identity, Not Pod IMDS Access

On AKS, cluster-level managed identities cover the cluster-to-Azure scenario (the control plane and kubelet acting on Azure). For pod-to-Azure authentication, the modern approach is Microsoft Entra Workload ID — pods get short-lived federated tokens instead of hitting IMDS.

Related hardening: AKS now supports blocking pod access to IMDS for non-host-network pods (preview). Once enabled, pods can no longer grab node-level managed identity tokens — a real security win, since any pod reaching IMDS could otherwise obtain tokens for the node's identity.

Managed Identity as a Federated Identity Credential (Cross-Tenant)

Managed identities only work within the tenant that hosts the subscription. For cross-tenant access, configure the managed identity as a federated identity credential (FIC) on an Entra app registration. Your workload uses its managed identity token as proof to acquire tokens as the multi-tenant app — completely certificateless. Microsoft.Identity.Web calls this pattern "certificateless credentials" and recommends it as the best option for production workloads on Azure.

Token Caching

The managed identity subsystem caches tokens at the platform level, and the Azure Identity SDKs cache in-memory via MSAL. Still:

  • Reuse credential instances across clients. High-volume apps that create new credentials per request can hit HTTP 429 throttling from Entra ID.
  • Handle the case where a target service rejects a token as expired.

Important Boundaries to Remember

  • Managed identities are tenant-specific. Moving a subscription to another directory means recreating and reconfiguring identities.
  • A resource can have one system-assigned identity but multiple user-assigned identities.
  • When you switch an existing resource's identity type (e.g., system-assigned → user-assigned on AKS), components keep using the old identity until its token expires — the switchover can take hours.

Part 5: Production Best Practices

This is where the "it works on my machine" story becomes "it works reliably at 3 AM."

1. Prefer User-Assigned Identities

Microsoft's guidance is clear: user-assigned identities are more efficient in a broader range of scenarios.

  • Identities and role assignments can be created before the resources that use them, so infrastructure deployments do not fail because the deployer lacks role-assignment rights.
  • One identity can serve many resources — fewer identities and role assignments to govern.
  • Lifecycle is decoupled: recycle compute freely without losing permissions.

Use system-assigned only when a resource genuinely needs its own unique identity that should die with it.

2. Use Deterministic Credentials in Production

DefaultAzureCredential is great for development but risky in production:

  • Unpredictable behavior: the chain checks environment variables and installed tools; someone running az login on a production VM can silently change which credential your app uses — potentially elevating or reducing privileges.
  • Debugging pain: when auth fails, you must dig through logs to find which credential in the chain failed.
  • Performance overhead: sequentially trying credentials adds latency.
  • Permission mismatches: the first credential that gets a token wins, even if it has the wrong permissions.

Production pattern — pick the credential explicitly:

TokenCredential credential = builder.Environment.IsProduction()
    ? new ManagedIdentityCredential(ManagedIdentityId.FromUserAssignedClientId(clientId))
    : new DefaultAzureCredential();

3. Understand the Retry Strategy

The Azure Identity library behaves differently depending on how you use managed identity:

  • Fail-fast mode (default DefaultAzureCredential behavior): no retries on token failure. Good for the dev loop, bad for production.
  • Resilient mode: up to five retries with exponential backoff starting at 0.8 seconds. Activated by using ManagedIdentityCredential directly, using ChainedTokenCredential containing it, or setting AZURE_TOKEN_CREDENTIALS=ManagedIdentityCredential (requires Azure.Identity 1.16.0+ for the environment-variable approach).

One more reason to use ManagedIdentityCredential directly in production.

4. Apply Least Privilege — Ruthlessly

  • Grant the narrowest role at the narrowest scope (resource > resource group > subscription).
  • Prefer data-plane roles (e.g., Storage Blob Data Reader) over broad control-plane roles like Contributor.
  • Audit role assignments regularly and revoke what is unused.

5. Separate Identities per Environment

Use different managed identities for dev, staging, and production. If an identity is ever compromised or misconfigured, the blast radius stays contained to one environment.

6. Monitor and Audit

  • Managed identity CRUD operations show up in Azure Activity logs.
  • Sign-in activity appears in Microsoft Entra sign-in logs — filter by service principal sign-ins to watch your identities in action.
  • Enable SDK diagnostic logging to catch credential-chain issues early.
  • Document which permissions each identity has and why.

7. Reuse Credential Instances

Create one credential object per application and share it across all SDK clients. This maximizes the MSAL token cache and avoids Entra ID throttling.

8. Eliminate the Last Secrets

A production maturity checklist:

  • [ ] No connection strings with keys — use Entra auth for Storage, SQL, Service Bus, Cosmos DB, etc.
  • [ ] No client secrets in app registrations for Azure-hosted workloads — use managed identity + federated identity credentials instead.
  • [ ] Key Vault accessed via managed identity (RBAC), not access policies with secrets.
  • [ ] CI/CD pipelines using OIDC / workload identity federation instead of service principal secrets.
  • [ ] AKS pods using Workload ID; IMDS restriction enabled where possible.

Quick Troubleshooting Guide

Symptom Likely cause Fix
400 Bad Request from IMDS with multiple identities Multiple user-assigned identities but no client_id specified Pass client_id, object_id, or msi_res_id
Token acquired but target service returns 403 RBAC role missing, wrong scope, or role propagation delay Verify role assignment; allow a few minutes for propagation
Auth works locally, fails in Azure Local credential (CLI/VS) has broader permissions than the managed identity Grant the managed identity the required role; use deterministic credentials
Old permissions still in effect after role change Platform/SDK token cache Tokens reflect changes only after refresh — wait for expiry or restart
DefaultAzureCredential slow or flaky in production Chain probing + fail-fast mode Switch to ManagedIdentityCredential directly

Wrap-Up

Managed identity is one of those rare features that improves security and reduces operational work at the same time. The learning curve is short:

  1. Beginner: enable an identity, assign a role, watch a token flow.
  2. Intermediate: wire up the Azure Identity SDK, understand system vs. user-assigned.
  3. Advanced: federated credentials, Arc/HIMDS, AKS workload identity, cross-tenant patterns.
  4. Production: user-assigned by default, deterministic credentials, least privilege, per-environment identities, monitoring.

If your Azure workloads still hold a client secret or storage key anywhere, that is your next refactoring target.


References (Microsoft Learn)


Wednesday, June 24, 2026

Automate Azure PIM Role Activation for Entra ID + Azure Resources with PowerShell

Automate Azure PIM Role Activation for Entra ID + Azure Resources with PowerShell

If you're working in a Zero Trust security environment, your Azure roles are likely managed through Privileged Identity Management (PIM) — meaning they expire and need to be manually re-activated every few hours. Clicking through the portal every time is tedious.

This PowerShell script automates activation for both Entra ID directory roles (via Microsoft Graph) and Azure resource roles (via ARM REST API) in a single run — with automatic detection of the maximum allowed duration from your PIM policy.


What This Script Does

  • Auto-installs all required PowerShell modules if not present
  • Activates all eligible Entra ID (Microsoft Graph) PIM roles — e.g. Global Reader, Security Administrator
  • Activates all eligible Azure Resource PIM roles — e.g. Contributor, Owner on subscriptions
  • Auto-detects the maximum allowed activation duration from your PIM policy per role
  • Displays a clean summary table of all eligible roles before activation

Prerequisites

  • PowerShell 5.1+ or PowerShell 7+
  • Internet access (modules are auto-installed if missing)
  • An account with eligible PIM role assignments in Entra ID or Azure subscriptions

The script will auto-install these modules if not found:

ModulePurpose
Az.AccountsAzure authentication & context
Az.ResourcesAzure AD user lookup
Microsoft.Graph.AuthenticationGraph API authentication
Microsoft.Graph.Identity.GovernancePIM role management
Microsoft.Graph.Identity.SignInsSign-in & identity operations

The Script

# ============================================================
#  Azure PIM Role Activator - Entra ID + Azure Resources
# ============================================================

# ── Install & Import Modules ─────────────────────────────────────────────────
foreach ($module in @("Az.Accounts", "Az.Resources")) {
    if (-not (Get-Module -ListAvailable -Name $module)) {
        Write-Host "Installing $module..." -ForegroundColor Yellow
        Install-Module $module -Scope CurrentUser -Force -AllowClobber
    }
    Import-Module $module -ErrorAction Stop
}

$targetVersion = (Get-Module -ListAvailable -Name Microsoft.Graph.Authentication | Sort-Object Version -Descending | Select-Object -First 1).Version
if (-not $targetVersion) {
    Install-Module Microsoft.Graph.Authentication -Scope CurrentUser -Force -AllowClobber
    $targetVersion = (Get-Module -ListAvailable -Name Microsoft.Graph.Authentication | Sort-Object Version -Descending | Select-Object -First 1).Version
}
foreach ($module in @("Microsoft.Graph.Authentication", "Microsoft.Graph.Identity.Governance", "Microsoft.Graph.Identity.SignIns")) {
    if (-not (Get-Module -ListAvailable -Name $module | Where-Object { $_.Version -eq $targetVersion })) {
        Write-Host "Installing $module $targetVersion..." -ForegroundColor Yellow
        Install-Module $module -RequiredVersion $targetVersion -Scope CurrentUser -Force -AllowClobber
    }
    Import-Module $module -RequiredVersion $targetVersion -ErrorAction Stop
}

# ============================================================
#  PART 1 — Entra ID (Microsoft Graph) PIM Roles
# ============================================================
Write-Host "`n===== ENTRA ID ROLES =====" -ForegroundColor Cyan

Disconnect-MgGraph -ErrorAction SilentlyContinue
Connect-MgGraph -Scopes "User.Read", "RoleManagement.ReadWrite.Directory", "RoleAssignmentSchedule.ReadWrite.Directory" -NoWelcome

$user = Invoke-MgGraphRequest -Uri "https://graph.microsoft.com/v1.0/me" -Method GET
if (-not $user) { Write-Error "Failed to retrieve user."; exit }
Write-Host "Logged in as: $($user.userPrincipalName)" -ForegroundColor Green

$eligibleRoles = Get-MgRoleManagementDirectoryRoleEligibilitySchedule -Filter "principalId eq '$($user.id)'" -ExpandProperty RoleDefinition
if (-not $eligibleRoles) { Write-Warning "No eligible Entra ID PIM roles found." }

foreach ($role in $eligibleRoles) {
    try {
        $rawDuration = ((Get-MgPolicyRoleManagementPolicyAssignment `
            -Filter "scopeId eq '/' and scopeType eq 'DirectoryRole' and roleDefinitionId eq '$($role.RoleDefinitionId)'" `
            -ExpandProperty "Policy(`$expand=Rules)").Policy.Rules | Where-Object { $_.Id -eq "Expiration_EndUser_Assignment" }).AdditionalProperties["maximumDuration"]
        $hours = if ($rawDuration -match "PT(\d+)H") { [int]$Matches[1] } elseif ($rawDuration -match "P(\d+)D") { [int]$Matches[1]*24 } else { 8 }
        New-MgRoleManagementDirectoryRoleAssignmentScheduleRequest -BodyParameter @{
            Action           = "selfActivate"
            PrincipalId      = $user.id
            RoleDefinitionId = $role.RoleDefinitionId
            DirectoryScopeId = $role.DirectoryScopeId
            Justification    = "M365 Team"
            ScheduleInfo     = @{ StartDateTime = (Get-Date).ToUniversalTime(); Expiration = @{ Type = "AfterDuration"; Duration = "PT${hours}H" } }
        } | Out-Null
        Write-Host "  ✔ Activated : $($role.RoleDefinition.DisplayName) for $hours hr(s)" -ForegroundColor Green
    }
    catch { Write-Host "  ✘ Failed    : $($role.RoleDefinition.DisplayName) — $($_.Exception.Message)" -ForegroundColor Red }
}

# ============================================================
#  PART 2 — Azure Resources (ARM) PIM Roles
# ============================================================
Write-Host "`n===== AZURE RESOURCE ROLES =====" -ForegroundColor Cyan

Connect-AzAccount
$tokenObj = Get-AzAccessToken -ResourceUrl "https://management.azure.com"
$token    = [System.Runtime.InteropServices.Marshal]::PtrToStringAuto(
                [System.Runtime.InteropServices.Marshal]::SecureStringToBSTR($tokenObj.Token))
$headers  = @{ Authorization = "Bearer $token"; "Content-Type" = "application/json" }
$userId   = (Get-AzADUser -SignedIn).Id

Write-Host "Subscription : $((Get-AzContext).Subscription.Name)" -ForegroundColor Green
Write-Host "User ID      : $userId" -ForegroundColor Green

$roles = (Invoke-RestMethod `
    -Uri "https://management.azure.com/providers/Microsoft.Authorization/roleEligibilityScheduleInstances?api-version=2020-10-01&`$filter=asTarget()" `
    -Headers $headers).value

if (-not $roles) { Write-Warning "No eligible Azure resource roles found." } else {
    $roles | ForEach-Object {
        [PSCustomObject]@{
            Role     = $_.properties.expandedProperties.roleDefinition.displayName
            Resource = $_.properties.expandedProperties.scope.displayName
            Type     = $_.properties.expandedProperties.scope.type
            Status   = $_.properties.status
        }
    } | Format-Table -AutoSize
}

Write-Host "`n===== ACTIVATING ALL ROLES =====" -ForegroundColor Yellow

foreach ($role in $roles) {

    $roleName  = $role.properties.expandedProperties.roleDefinition.displayName
    $scopePath = $role.properties.scope
    $roleDefId = $role.properties.roleDefinitionId

    # ── Auto-detect max allowed hours from PIM policy assignment ─────────────
    try {
        $policyAssignments = (Invoke-RestMethod `
            -Uri "https://management.azure.com$scopePath/providers/Microsoft.Authorization/roleManagementPolicyAssignments?api-version=2020-10-01&`$filter=roleDefinitionId eq '$roleDefId'" `
            -Headers $headers).value

        $policyId = $policyAssignments[0].properties.policyId

        $rules = (Invoke-RestMethod `
            -Uri "https://management.azure.com$policyId`?api-version=2020-10-01" `
            -Headers $headers).properties.effectiveRules

        $expiryRule  = $rules | Where-Object { $_.id -eq "Expiration_EndUser_Assignment" }
        $rawDuration = $expiryRule.maximumDuration

        $hours = if ($rawDuration -match "PT(\d+)H")     { [int]$Matches[1] }
                 elseif ($rawDuration -match "P(\d+)D")  { [int]$Matches[1] * 24 }
                 else                                     { 1 }

        Write-Host "  Policy max duration for '$roleName': $rawDuration ($hours hr)" -ForegroundColor DarkGray
    }
    catch {
        $hours = 1
        Write-Host "  Could not read policy for '$roleName', defaulting to 1hr" -ForegroundColor DarkYellow
    }

    # ── Activate the role ─────────────────────────────────────────────────────
    try {
        $body = @{ properties = @{
            principalId      = $userId
            roleDefinitionId = $roleDefId
            requestType      = "SelfActivate"
            linkedRoleEligibilityScheduleId = $role.properties.roleEligibilityScheduleId
            justification    = "Self activation"
            scheduleInfo     = @{ expiration = @{ type = "AfterDuration"; duration = "PT${hours}H" } }
        }} | ConvertTo-Json -Depth 10

        Invoke-RestMethod `
            -Uri "https://management.azure.com$scopePath/providers/Microsoft.Authorization/roleAssignmentScheduleRequests/$([guid]::NewGuid())?api-version=2020-10-01" `
            -Headers $headers -Method Put -Body $body | Out-Null

        Write-Host "  ✔ Activated : $roleName → $($role.properties.expandedProperties.scope.displayName) for $hours hour(s)" -ForegroundColor Green
    }
    catch {
        $err = ($_.ErrorDetails.Message | ConvertFrom-Json -ErrorAction SilentlyContinue).error.message
        Write-Host "  ✘ Failed    : $roleName — $err" -ForegroundColor Red
    }
}

Write-Host "`nDone!" -ForegroundColor Cyan

How It Works

Module Auto-Install — The script checks for all required Az and Microsoft Graph modules and installs any missing ones before proceeding. It also pins Graph modules to the same version to avoid compatibility conflicts.

Part 1 — Entra ID Roles (Graph API) — Connects via Connect-MgGraph with the required scopes, fetches all eligible directory role assignments for the signed-in user, reads the max allowed duration from each role's PIM policy, and submits a selfActivate request via New-MgRoleManagementDirectoryRoleAssignmentScheduleRequest.

Part 2 — Azure Resource Roles (ARM REST API) — Connects via Connect-AzAccount, retrieves a bearer token (handling the SecureString conversion in newer Az module versions), queries the tenant-level PIM endpoint for eligible resource role assignments, auto-detects max duration from roleManagementPolicyAssignments, and submits activation requests via the ARM REST API.

Automate Azure PIM Role Activation with PowerShell

Automate Azure PIM Role Activation with PowerShell

Tired of manually clicking Activate in the Azure portal every time your PIM role expires?
This script automatically activates all eligible Azure resource roles — and even detects the maximum allowed duration from your PIM policy, so you never hit the "duration exceeds maximum" error again.


Prerequisites

  • Az PowerShell module installed (Install-Module Az -Scope CurrentUser -Force)
  • Eligible Azure RBAC roles assigned via PIM
  • Az.Accounts and Az.Resources modules

The Script

# ============================================================
#  Azure PIM Resource Role Activator - Full Script
# ============================================================

# ── Auto-install required modules ────────────────────────────────────────────
foreach ($module in @("Az.Accounts", "Az.Resources")) {
    if (-not (Get-Module -ListAvailable -Name $module)) {
        Write-Host "Installing $module..." -ForegroundColor Yellow
        Install-Module $module -Scope CurrentUser -Force -AllowClobber
    }
    Import-Module $module -ErrorAction Stop
}

# Or if you want to install the full Az bundle (all Az modules) instead:
# if (-not (Get-Module -ListAvailable -Name Az.Accounts)) {
#     Write-Host "Installing Az module..." -ForegroundColor Yellow
#     Install-Module Az -Scope CurrentUser -Force -AllowClobber
# }
# Import-Module Az.Accounts, Az.Resources -ErrorAction Stop

Connect-AzAccount

# Get token (handle SecureString in newer Az versions)
$tokenObj = Get-AzAccessToken -ResourceUrl "https://management.azure.com"
$token    = [System.Runtime.InteropServices.Marshal]::PtrToStringAuto(
                [System.Runtime.InteropServices.Marshal]::SecureStringToBSTR($tokenObj.Token))

$headers = @{ Authorization = "Bearer $token"; "Content-Type" = "application/json" }
$subId   = (Get-AzContext).Subscription.Id
$userId  = (Get-AzADUser -SignedIn).Id

Write-Host "Subscription : $subId" -ForegroundColor Cyan
Write-Host "User ID      : $userId" -ForegroundColor Cyan

# ── Get eligible roles (tenant-level works, subscription-level doesn't) ──────
$roles = (Invoke-RestMethod `
    -Uri "https://management.azure.com/providers/Microsoft.Authorization/roleEligibilityScheduleInstances?api-version=2020-10-01&`$filter=asTarget()" `
    -Headers $headers).value
	
$roles | ForEach-Object {
    [PSCustomObject]@{
        Role         = $_.properties.expandedProperties.roleDefinition.displayName
        Resource     = $_.properties.expandedProperties.scope.displayName
        ResourceType = $_.properties.expandedProperties.scope.type
        Scope        = $_.properties.scope
        StartTime    = $_.properties.startDateTime
        EndTime      = $_.properties.endDateTime
        Status       = $_.properties.status
    }
} | Format-Table -AutoSize
	
Write-Host "`nFound $($roles.Count) eligible role(s)" -ForegroundColor Yellow
$roles | ForEach-Object {
    Write-Host "  • $($_.properties.expandedProperties.roleDefinition.displayName) → $($_.properties.expandedProperties.scope.displayName)"
}

Write-Host "`n===== ACTIVATING ALL ROLES =====" -ForegroundColor Yellow

foreach ($role in $roles) {

    $roleName  = $role.properties.expandedProperties.roleDefinition.displayName
    $scopePath = $role.properties.scope
    $roleDefId = $role.properties.roleDefinitionId

    # ── Auto-detect max allowed hours from PIM policy assignment ─────────────
    try {
        $policyAssignments = (Invoke-RestMethod `
            -Uri "https://management.azure.com$scopePath/providers/Microsoft.Authorization/roleManagementPolicyAssignments?api-version=2020-10-01&`$filter=roleDefinitionId eq '$roleDefId'" `
            -Headers $headers).value

        $policyId = $policyAssignments[0].properties.policyId

        $rules = (Invoke-RestMethod `
            -Uri "https://management.azure.com$policyId`?api-version=2020-10-01" `
            -Headers $headers).properties.effectiveRules

        $expiryRule  = $rules | Where-Object { $_.id -eq "Expiration_EndUser_Assignment" }
        $rawDuration = $expiryRule.maximumDuration

        $hours = if ($rawDuration -match "PT(\d+)H")     { [int]$Matches[1] }
                 elseif ($rawDuration -match "P(\d+)D")  { [int]$Matches[1] * 24 }
                 else                                     { 1 }

        Write-Host "  Policy max duration for '$roleName': $rawDuration ($hours hr)" -ForegroundColor DarkGray
    }
    catch {
        $hours = 1
        Write-Host "  Could not read policy for '$roleName', defaulting to 1hr" -ForegroundColor DarkYellow
    }

    # ── Activate the role ─────────────────────────────────────────────────────
    try {
        $body = @{ properties = @{
            principalId      = $userId
            roleDefinitionId = $roleDefId
            requestType      = "SelfActivate"
            linkedRoleEligibilityScheduleId = $role.properties.roleEligibilityScheduleId
            justification    = "Self activation"
            scheduleInfo     = @{ expiration = @{ type = "AfterDuration"; duration = "PT${hours}H" } }
        }} | ConvertTo-Json -Depth 10

        Invoke-RestMethod `
            -Uri "https://management.azure.com$scopePath/providers/Microsoft.Authorization/roleAssignmentScheduleRequests/$([guid]::NewGuid())?api-version=2020-10-01" `
            -Headers $headers -Method Put -Body $body | Out-Null

        Write-Host "  ✔ Activated : $roleName → $($role.properties.expandedProperties.scope.displayName) for $hours hour(s)" -ForegroundColor Green
    }
    catch {
        $err = ($_.ErrorDetails.Message | ConvertFrom-Json -ErrorAction SilentlyContinue).error.message
        Write-Host "  ✘ Failed    : $roleName — $err" -ForegroundColor Red
    }
}

Write-Host "`nDone!" -ForegroundColor Cyan

How It Works

1. Authentication — Connects to Azure and retrieves a bearer token, handling the SecureString conversion introduced in newer Az module versions.

2. Eligible Role Discovery — Queries the tenant-level PIM endpoint to fetch all eligible role assignments for the signed-in user.

3. Auto-Duration Detection — For each role, it reads the roleManagementPolicyAssignment to find the Expiration_EndUser_Assignment rule and extracts the maximumDuration (e.g. PT4H). This prevents the "duration exceeds maximum" API error.

4. Role Activation — Submits a SelfActivate request for each eligible role using the exact maximum duration allowed by policy.



Featured Post

Build a Secure MCP Server on Azure Functions for Copilot Studio Agents (Entra ID OAuth)

Model Context Protocol (MCP) is quickly becoming the standard way to give AI agents real tools. Copilot Studio can call any remote MCP serve...

Popular posts