Most teams stand up Privileged Identity Management for Entra directory roles - Global Administrator, User Administrator - and stop there. But the blast radius in a mature Azure estate is on the resource control plane: a standing Owner on a production subscription can delete a resource group, exfiltrate Key Vault secrets, and rewrite RBAC. This guide extends JIT elevation to Azure resource roles, scopes eligibility down to individual resource groups, gates activation behind approval and ticketing, and brings in PIM for Groups for privileges that don’t map cleanly to a single Azure role.
The two PIM control planes
PIM governs two separate systems, and conflating them is the most common design error.
| PIM for Entra roles | PIM for Azure resources | |
|---|---|---|
| Governs | Directory roles (Global Admin, etc.) | Azure RBAC role assignments |
| Scope | Tenant / administrative units | Management group, subscription, RG, resource |
| Backing API | Microsoft Graph (roleManagement/directory) |
ARM (Microsoft.Authorization/*) |
| Max activation | Up to 24 hours | Up to 24 hours (per role policy) |
The resource side is what we focus on here. It maps directly onto the ARM hierarchy, which is what makes scoping powerful: you can make someone eligible for Contributor on one resource group without giving them anything on the subscription.
The mental shift: stop thinking “who is an Owner” and start thinking “who is eligible to become an Owner, for how long, with whose approval, against which ticket.” Standing assignments become the exception you justify, not the default.
Prerequisites: Microsoft Entra ID P2 (or Entra ID Governance) for every user who activates, and Owner or User Access Administrator on the target scope to manage its eligible assignments and policy.
1. Onboard subscriptions and discover what’s already there
PIM auto-discovers subscriptions once you have the right directory role, but the first practical step is inventorying the standing assignments you’re about to replace. Do not start by deleting - start by seeing.
# Every role assignment on a subscription, including inherited from the MG
az role assignment list \
--scope /subscriptions/00000000-0000-0000-0000-000000000000 \
--include-inherited \
--query "[].{principal:principalName, role:roleDefinitionName, type:principalType, scope:scope}" \
-o table
Flag every Owner, Contributor, and User Access Administrator held by a human (filter principalType == User). Those are your conversion candidates. Service principals and managed identities stay as standing assignments - PIM activation requires interactive sign-in, so workload identities cannot activate eligible roles.
In the portal, the same surface is Entra admin center -> Identity governance -> Privileged Identity Management -> Azure resources, where you select the subscription (or management group) to manage.
2. Create scoped eligible assignments
This is the core move. Instead of an active Contributor on the whole subscription, grant eligible Contributor on a single resource group. The principal sees the role in PIM but holds no permission until they activate.
RG_SCOPE="/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/rg-payments-prod"
PRINCIPAL_OID="11111111-1111-1111-1111-111111111111" # the user's object ID
# Contributor role definition ID (stable, well-known GUID)
CONTRIB="b24988ac-6180-42a0-ab88-20f7382dd24c"
az role assignment create \
--assignee-object-id "$PRINCIPAL_OID" \
--assignee-principal-type User \
--role "$CONTRIB" \
--scope "$RG_SCOPE"
Caveat:
az role assignment createcreates a standing (active) assignment, not an eligible one. The classic CLI predates PIM. To create eligible assignments you go through the ARM PIM provider - the portal, the REST API, or IaC (covered in step 6). I show the active form here only because you’ll use it for the rare standing exceptions; the default path is eligible.
The eligible-assignment ARM resource is Microsoft.Authorization/roleEligibilityScheduleRequests. You can create one with a raw ARM call:
GUID=$(uuidgen)
az rest --method put \
--url "https://management.azure.com${RG_SCOPE}/providers/Microsoft.Authorization/roleEligibilityScheduleRequests/${GUID}?api-version=2022-04-01-preview" \
--body '{
"properties": {
"principalId": "11111111-1111-1111-1111-111111111111",
"roleDefinitionId": "'"${RG_SCOPE}"'/providers/Microsoft.Authorization/roleDefinitions/b24988ac-6180-42a0-ab88-20f7382dd24c",
"requestType": "AdminAssign",
"scheduleInfo": {
"startDateTime": null,
"expiration": { "type": "AfterDuration", "duration": "P180D" }
},
"justification": "JIT eligibility for payments platform team"
}
}'
Note roleDefinitionId is fully qualified with the scope, requestType is AdminAssign (an admin granting eligibility, versus SelfActivate when the user activates), and expiration gives the eligibility a hard 180-day end date so it doesn’t live forever.
3. Tune the role policy per scope
Eligibility without guardrails is just deferred standing access. The role management policy at each scope controls what activation requires: maximum duration, MFA, justification, ticketing, and approval. Configure the high-value roles (Owner, Contributor, User Access Administrator) tightly, and lower-risk roles more loosely.
In the portal: PIM -> Azure resources -> <subscription> -> Settings -> <role> -> Edit. A defensible production baseline for Owner:
Activation
Max activation duration: 2 hours
Require MFA on activation: Yes (or "Require Authentication Context")
Require justification: Yes
Require ticket information: Yes
Require approval to activate: Yes -> approvers: "Cloud Platform Approvers" group
Assignment
Allow permanent eligible assignment: No (force an expiry on eligibility)
Expire eligible assignments after: 180 days
Allow permanent active assignment: No
Authentication Context is the modern way to require MFA: instead of the policy’s built-in “require MFA” toggle, you reference a Conditional Access authentication context (for example c1) so activation is gated by a full CA policy - phishing-resistant methods, compliant device, named location. That keeps your MFA strength definition in one place (CA) rather than split across PIM.
Owner and User Access Administrator are the roles that can grant themselves more access. Always require approval on those two, and never make the approver a member of the same team that activates.
A critical gap to know about: role management policies cannot be updated through Bicep or ARM templates. The Microsoft.Authorization/roleManagementPolicyAssignments resource is readable but the rules are not writable via standard ARM deployment. You set policy through the portal or the Authorization REST API (PATCH .../roleManagementPolicies/<id>); you automate assignments via IaC. Plan your tooling accordingly - I see teams burn a sprint trying to push policy through azapi before discovering this.
4. PIM for Groups: JIT into privileged access groups
Some privileges don’t map to one Azure role. You want a single activation that grants Contributor on three resource groups plus a Key Vault data-plane role, or you want JIT ownership of a group so someone can manage its membership only when needed. PIM for Groups solves this: a group is onboarded to PIM, principals are made eligible for membership or ownership, and the group itself holds the standing role assignments.
The pattern:
- Create a role-assignable security group (it must be
isAssignableToRoleor onboarded as a privileged access group). - Give that group the standing Azure RBAC assignments it needs (the three RGs + the Key Vault role).
- In PIM, make engineers eligible members. Activation drops them into the group for a bounded window; on expiry they fall out and lose every role the group carried.
az ad group create \
--display-name "pag-payments-operators" \
--mail-nickname "pag-payments-operators" \
--is-assignable-to-role true
Then assign the group its standing roles (Contributor on each RG, Key Vault Secrets User on the vault), and onboard it in PIM -> Groups -> <group> -> Settings, configuring the Member role with the same activation/approval rules as in step 3.
PIM for Groups activation is capped at 8 hours maximum - shorter than the 24-hour ceiling for direct Azure resource roles. For nested ownership scenarios (eligible owner of a group so you can rotate its members JIT) this is usually plenty.
This also cleans up nested ownership: rather than standing owners on dozens of access groups, make a platform team eligible-owner across them. They activate ownership only to make a membership change, and that activation is logged and approvable like any other.
5. A break-glass path that still gets logged
PIM is in the critical path of getting access, so a PIM, Graph, or Conditional Access outage cannot lock everyone out of production. The answer is not a quiet permanent Owner that bypasses everything - it’s a break-glass account that has standing access but is loud.
Design it so the emergency path is auditable:
- Two cloud-only
*.onmicrosoft.comaccounts, excluded from the Conditional Access policies that could block sign-in, but monitored so any use fires an alert. - Standing Owner at the management group (or per critical subscription) - the one deliberate exception to “no standing Owner.”
- A high-severity alert on every sign-in by these accounts and on any role activation they perform, routed to on-call, not just an inbox.
// Sentinel / Log Analytics: alert on any break-glass sign-in
let breakGlassUPNs = dynamic(["breakglass-01@contoso.onmicrosoft.com",
"breakglass-02@contoso.onmicrosoft.com"]);
SigninLogs
| where UserPrincipalName in~ (breakGlassUPNs)
| project TimeGenerated, UserPrincipalName, AppDisplayName, IPAddress,
ResultType, Location
The test that matters: tabletop a “PIM is down” scenario. If your engineers cannot reach the break-glass credential (sealed in a separate vault, two-person retrieval) and your alert does not actually page someone, you have a break-glass account that is just an unmonitored backdoor.
6. Automate eligibility with IaC and the PIM APIs
Click-ops eligibility does not survive a real estate. Two automation surfaces matter, and they cover different objects.
Azure resource roles via ARM/Bicep/Terraform. Eligible assignments are first-class ARM resources, so you can declare them. In Terraform, the azurerm_pim_eligible_role_assignment resource handles this cleanly:
data "azurerm_subscription" "prod" {
subscription_id = "00000000-0000-0000-0000-000000000000"
}
data "azurerm_role_definition" "contributor" {
name = "Contributor"
scope = data.azurerm_subscription.prod.id
}
resource "azurerm_pim_eligible_role_assignment" "payments_contrib" {
scope = "${data.azurerm_subscription.prod.id}/resourceGroups/rg-payments-prod"
role_definition_id = data.azurerm_role_definition.contributor.role_definition_id
principal_id = "11111111-1111-1111-1111-111111111111"
schedule {
expiration {
duration_days = 180
}
}
justification = "Payments platform team - JIT Contributor (managed by IaC)"
}
The Bicep/ARM equivalent is the roleEligibilityScheduleRequests resource shown in step 2. Remember the limitation from step 3: you can declare assignments in IaC, but the policy (duration, approval, MFA) is not writable through ARM templates - drive it via the REST API or portal.
PIM for Groups via Microsoft Graph. Group eligibility lives in Graph, not ARM. Make a principal an eligible member with an eligibilityScheduleRequest:
az rest --method post \
--url "https://graph.microsoft.com/v1.0/identityGovernance/privilegedAccess/group/eligibilityScheduleRequests" \
--headers "Content-Type=application/json" \
--body '{
"accessId": "member",
"principalId": "11111111-1111-1111-1111-111111111111",
"groupId": "22222222-2222-2222-2222-222222222222",
"action": "adminAssign",
"scheduleInfo": {
"startDateTime": "2026-05-21T00:00:00Z",
"expiration": { "type": "afterDuration", "duration": "P180D" }
},
"justification": "Eligible member of pag-payments-operators"
}'
Key fields: accessId is member or owner (this is how you grant nested JIT ownership), action is adminAssign, and expiration bounds the eligibility itself. The same endpoint family handles assignmentScheduleRequests for the rare standing assignment.
Source-control your eligibility. A pull request that adds someone as eligible-Owner is itself an approval artifact and an audit trail - far better than a portal change nobody can reconstruct six months later.
7. Recurring access reviews with auto-removal
Eligibility rots. People change teams, projects end, and “temporary” eligibility quietly becomes permanent. Access reviews are the garbage collector. Schedule a recurring review over the eligible assignments at each high-value scope, and configure it to auto-apply results and remove access when a reviewer doesn’t respond.
In the portal: Identity governance -> Access reviews -> New access review, scoped to “Azure resource roles” for the subscription/RG, reviewing eligible assignments.
Review: Eligible "Owner" + "Contributor" on rg-payments-prod
Reviewers: Resource owners (or named "Cloud Platform Approvers")
Recurrence: Quarterly
Auto-apply: Yes
If reviewers don't respond: Remove access
Justification required: Yes (reviewer must state why access is kept)
“If reviewers don’t respond -> Remove access” is the setting that actually shrinks standing risk. A review that defaults to keep on silence is theater. Make the safe default removal, and require a justification to retain.
For PIM for Groups, run the equivalent review over the group’s eligible members and owners. Reviews and the underlying eligibility share the same governance plane, so a removal here drops the eligibility everywhere it propagated.
8. Audit activations in Sentinel and alert on elevation
Every activation is an audit event. The goal is to make Owner/Contributor elevation visible in seconds, not discoverable in a quarterly export. Stream the Azure Activity log and Entra audit logs into Log Analytics / Microsoft Sentinel, then alert on the activations that matter.
PIM for Azure resource activations surface in the Activity log under the Microsoft.Authorization provider. This rule flags any activation of Owner or User Access Administrator:
AzureActivity
| where OperationNameValue has "roleAssignmentScheduleRequests"
| where ActivityStatusValue == "Success"
| extend props = parse_json(Properties)
| where tostring(props.requestbody) has_any ("Owner", "User Access Administrator")
| project TimeGenerated, Caller, ResourceGroup,
SubscriptionId = tostring(props.subscriptionId), OperationNameValue
For Entra-role and PIM-for-Groups activity, query the audit logs instead:
AuditLogs
| where Category == "RoleManagement"
| where ActivityDisplayName has "Add member to role completed (PIM activation)"
| extend actor = tostring(InitiatedBy.user.userPrincipalName)
| project TimeGenerated, ActivityDisplayName, actor,
target = tostring(TargetResources[0].displayName), Result
Turn these into scheduled analytics rules. The two alerts every estate should have: activation of Owner/User Access Administrator on production, and any modification of a PIM role policy (someone weakening duration or removing approval is a strong tampering signal).
Enterprise scenario
A payments platform team converted every production subscription to eligible-only Owner and Contributor, approval gated, and the auditors were happy. Two weeks later the on-call engineer hit a wall during a Sev2: activation of Contributor on rg-payments-prod sat in “pending approval” for forty minutes because the only approver group member was on a flight. The constraint they had missed: PIM approvals are synchronous and single-stage, with no built-in escalation or timeout-to-auto-approve. A break-glass account exists for full outages, but burning it for a routine activation hiccup is exactly the loud event you do not want during an incident.
The fix was twofold. First, the approver group got real depth - on-call leads from a different team plus the platform manager, so duty separation held but coverage did not depend on one person. Second, they split the role policy: incident-response Contributor on the production RGs requires approval, but a separate, tightly scoped eligible assignment for the active on-call rotation activates with MFA and justification only, no approval, capped at one hour.
# Grant the current on-call principal self-activatable (no-approval) eligibility,
# bounded to the rotation window; renewed each handoff by the pipeline.
az rest --method put \
--url "https://management.azure.com${RG_SCOPE}/providers/Microsoft.Authorization/roleEligibilityScheduleRequests/$(uuidgen)?api-version=2022-04-01-preview" \
--body '{"properties":{"principalId":"'"$ONCALL_OID"'","roleDefinitionId":"'"${RG_SCOPE}"'/providers/Microsoft.Authorization/roleDefinitions/b24988ac-6180-42a0-ab88-20f7382dd24c","requestType":"AdminAssign","scheduleInfo":{"expiration":{"type":"AfterDuration","duration":"P14D"}},"justification":"On-call rotation - JIT incident Contributor"}}'
The Sentinel rule from step 8 still fires on every activation, so the no-approval path stays visible. Approval is a control, not a goal; the goal is least standing privilege with an auditable, survivable elevation path.
Verify
Before you call it done, prove the controls actually behave:
# 1. The eligible principal holds NO standing permission yet
az role assignment list \
--assignee 11111111-1111-1111-1111-111111111111 \
--scope "$RG_SCOPE" --query "[].roleDefinitionName" -o tsv
# Expect: empty (eligibility is not an active assignment)
# 2. Eligible assignments exist at the scope
az rest --method get \
--url "https://management.azure.com${RG_SCOPE}/providers/Microsoft.Authorization/roleEligibilitySchedules?api-version=2020-10-01" \
--query "value[].{principal:properties.principalId, role:properties.roleDefinitionId}" -o table
Then the human-loop checks no script can do for you:
- Activate the role in
PIM -> My roles -> Azure resources. Confirm it demands MFA, justification, and a ticket, and routes to the approver. - After the approver consents, re-run check #1 - the role now appears as active.
- Wait past the activation window (or deactivate) and confirm the permission is gone.
- Trigger a test activation and confirm your Sentinel rule fires end to end.
Rollout checklist
Pitfalls and next steps
- Workload identities can’t activate. Service principals and managed identities have no interactive sign-in, so they cannot use eligible assignments - keep their RBAC as scoped standing assignments and govern those with reviews instead.
- Eligibility with no expiry is standing access in disguise. Always set an eligibility end date and an activation duration. Disable “permanent eligible assignment” in policy.
- Policy is not Terraformable. Don’t architect a pipeline assuming you can push duration/approval rules through ARM - you can’t. Assignments are IaC; policy is REST/portal.
- Scope creep upward. Granting eligible Owner at the subscription when the work only ever touches one RG defeats the point. Push eligibility down the ARM hierarchy as far as it will go.
- The approver paradox. If the people who activate also approve, you have no control. Separate the duties.
The end state is an estate with effectively zero standing Owners on production: access is something engineers request and earn for the duration of a task, gated by approval, bounded by time, and fully reconstructable from the audit log. Pair this with Conditional Access authentication contexts for the activation gate and Defender for Cloud for the posture it protects, and least privilege stops being an aspiration and becomes the default state of the system.