$ cd ../writing

2026-09-25 · 14 min

Privileged access with PIM

Eligible instead of member, the activation policy and what it asks of the person activating, the three PIM systems that share a name, and why the approval step cannot be automated, only made faster.

Part 2 ended with the problem a group layout does not solve, standing privileged access. Everyone in the production Contributor security group holds write access to production every hour of every day, whether they are working or asleep. Privileged Identity Management (PIM) is the Entra feature that turns those members into eligible members. They hold the right to ask for access, with a clock on it, instead of the access itself.

This piece goes from why to how. When PIM is worth what it costs, the words it uses, which of the three PIM systems a group estate needs, what a policy asks of a person, how an activation and an approval happen, and where automation stops. A pipeline can decide who may ask and what asking costs. It cannot activate and it cannot approve.

When PIM is worth it

This is an enterprise control. A tenant with three engineers who all hold Owner and all sit in one room does not need it. The signs that you do:

  • An auditor asks who had write access to production last quarter, and the only answer is a membership list that says who has it today.
  • Access made for an incident is still there a year later. Nobody was asked to remove it, so nobody did.
  • A laptop is stolen, or a session token is phished. With standing access the attacker has production the moment they have the device. With eligible access they have the right to ask. Asking means MFA, a written justification, and an approver who sees a request at three in the morning from an account whose owner is asleep.

PIM changes what a grant is. Access becomes an activation. It is time-bound, logged with a justification and a ticket number, and approved by someone who is not the requester. Nobody is a member at rest.

The cost is real. Every eligible user and every approver needs a Microsoft Entra ID P2 or Entra ID Governance licence. The person fixing production at two in the morning waits for MFA, a justification field and an approver who has to be awake. Budget for both before switching it on.

Eligible and active, member and owner

PIM has two kinds of assignment.

  • Active is membership. The access works now, until an end date. A permanent active assignment is what every group had before PIM.
  • Eligible is the right to activate. Nothing works until the person asks, passes the policy, and receives a time-bound active assignment in return.

In PIM for Groups every security group has two roles, member and owner, and each role has its own policy.

The owner role is the one to take seriously. Part 2 named the gap. A group owner adds members with no approval and no expiry, so an owner of a privileged group is an administrator of everything the group grants. An eligible owner closes it. Ownership for eight hours, on request, in the log.

A worked example. The group acme-az-prod-a-contributor carries Contributor on subscription A.

  • Eligible member, the team group acme-az-platform-team. A group can be eligible, not only a person. Anyone in the team can activate, and the team group stays as Part 2 built it.
  • Eligible owners, four named people. No group here. Ownership is an individual responsibility, and the approver should see a name.
  • Eligibility expires after a year. Both lists get re-decided, not carried forward by default.

Any security group can be onboarded, and Microsoft 365 groups too. The exceptions are dynamic groups and groups synchronised from on-premises. The group does not need to be role-assignable. Onboarding happens on the first eligibility or policy write, and there is no way back. Once PIM knows a group, it stays known.

Three systems, one name

PIM is three products that share a portal blade. Each has its own policies, its own approval queue and its own API. A setting changed in one does nothing in the others.

  • PIM for Entra roles governs directory roles such as Groups Administrator and User Administrator. Requests live under roleManagement/directory in Graph.
  • PIM for Azure resources governs RBAC on management groups, subscriptions and resource groups. Requests are ARM resources under the Microsoft.Authorization provider, and every scope and role pair has its own policy.
  • PIM for Groups governs membership and ownership of a group. Requests live under identityGovernance/privilegedAccess/group.

The first and third are easy to tell apart once you have set one up. The directory role from Part 2 is the example. The pipeline’s service principal holds Groups Administrator on the unit as a permanent active assignment, because a pipeline cannot activate.

The people who operate PIM hold the same role on the same unit eligibly, through a group, and activate it for up to eight hours with a justification. Same role, same scope, two kinds of assignment, and the humans are the ones with the clock.

The one to place is PIM for Azure resources. It puts eligibility directly on an RBAC assignment. A person is eligible for Contributor on subscription A, no group involved, and the policy lives on that role at that scope. It fits where a group would be one object too many. A break-glass Owner on the root management group for two named people. A subscription no team owns yet.

On an estate where roles are carried by groups it is redundant. Activating the group membership is the activation, and one policy per group replaces one per role per scope.

It still shows up, because the Access control blade offers “Eligible” as an assignment type and someone will pick it. That assignment then lives in a different queue with a different policy from the group’s, which is worth knowing before an auditor finds it.

The rest of this piece is PIM for Groups.

The policy

The policy is what a person pays to get in. It is set per group and per role.

The default policy

The moment a group is onboarded, Microsoft writes a policy for both roles. The documentation does not list its values, so read it before you trust it. A freshly onboarded group shows this for the member role:

activation max   8 hours
on activation    justification, no MFA
ticket           not required
approval         not required
eligible expiry  1 year, no permanent
active expiry    6 months, no permanent

These values are Microsoft’s, not yours. They are acceptable for a non-production Reader group. They are not acceptable for a group that writes to production, and a group keeps them until someone writes a policy.

A production policy

  • Eight hours maximum per activation. Long enough for a working day, short enough that access does not survive a shift change.
  • MFA on activation, even when the sign-in already had it.
  • Justification and a ticket number. The ticket field is information only. Microsoft does not check it against any ticketing system. The approver does.
  • Approval by a group, not by a named person. A single approver is unavailable at some point, on leave or in another time zone, and the request expires while they are.
  • No permanent assignments. Eligibility expires after a year, an active assignment after six months.
locals {
  group_id    = "00000000-0000-0000-0000-0000000000a1"
  approver_id = "00000000-0000-0000-0000-0000000000b2"
}

resource "azuread_group_role_management_policy" "member" {
  group_id = local.group_id
  role_id  = "member"

  activation_rules {
    maximum_duration                   = "PT8H"
    require_multifactor_authentication = true
    require_justification              = true
    require_ticket_info                = true
    require_approval                   = true

    approval_stage {
      primary_approver {
        object_id = local.approver_id
        type      = "groupMembers"
      }
    }
  }

  eligible_assignment_rules {
    expiration_required = true
    expire_after        = "P365D"
  }

  active_assignment_rules {
    expiration_required                = true
    expire_after                       = "P180D"
    require_multifactor_authentication = true
    require_justification              = true
  }
}

Eligibility is a second resource, one per principal per role:

locals {
  team_id = "00000000-0000-0000-0000-0000000000c3"
}

resource "azuread_privileged_access_group_eligibility_schedule" "team" {
  group_id        = local.group_id
  principal_id    = local.team_id
  assignment_type = "member"
  duration        = "P365D"
  justification   = "Platform team, prod write"
}

Two things about the policy resource

  • You never create it. The default policy exists from onboarding, so the resource imports on first use. Treat it as configuration of something that exists, not a thing you own.
  • Durations are fixed choices. Activation runs from PT30M to P1D in half-hour steps. Expiry is one of P15D, P30D, P90D, P180D, P365D. Anything else fails at plan.

Outside production

PIM earns its friction on one role there, the owner. An eligible owner is the escalation path for a team that already holds standing membership of its own groups. The team approves its own escalations, with MFA and a justification and no ticket.

Putting the member role behind PIM there buys nothing the directory does not already do. Where access has to be requested at all, an access package with an expiry fits better than PIM. Both beat manual assignment, which is where people get forgotten.

Who may apply it

The permission behind the resource is tenant-wide, and that decides who runs the code.

  • The group permission has a boundary. Part 2 gave the pipeline’s service principal Groups Administrator scoped to one administrative unit. It can touch the groups in that unit and nothing else.
  • The PIM permissions do not. RoleManagementPolicy.ReadWrite.AzureADGroup and PrivilegedEligibilitySchedule.ReadWrite.AzureADGroup are Graph application permissions. An application permission applies to every group in the tenant, and neither can be scoped to a unit.

In a tenant that belongs to one company, grant them to the pipeline and keep PIM in the same Terraform as the groups. One path, one plan, one review. That is the better setup.

In a tenant shared by several companies the request is a different one. It asks for every-group PIM write for a pipeline that one of the tenants runs, and the tenant administrator is right to hesitate.

When the grant is refused, the fallback is a script in the same repository, run under a user account by the few people who already hold Groups Administrator on the unit. A user’s delegated call is bounded by the user’s role in a way the application permission is not.

The script does what the Terraform would have done. It reads the same group configuration, reports the gap between config and PIM, and writes the missing eligibility and policies. A group that matches no rule stops the run and is reported, so a new production service never receives a default nobody reviewed.

Terraform still holds a check for the other direction. A plan that would give a team standing production membership fails, with the reason in the message. This works, but it is two tools for one job, which is why the single-company grant is preferable.

Activating

An activation request carries a start time, and the start can be in the future. Someone who has to deploy in tonight’s maintenance window files the request in the afternoon:

group     acme-az-prod-a-contributor
role      member
start     22:00 today
duration  4 hours
ticket    ACME-2210
reason    maintenance window, rollout

The approver decides at three in the afternoon. The membership appears at 22:00 and is gone at 02:00, with nobody awake to remove it. This is the part of PIM that makes a night deployment less privileged than a day one, not more.

Approving

An approval request waits 24 hours, and that window is not configurable. If nobody decides, the request expires and the person asks again. The first approver to answer resolves it, for everyone. An approver cannot approve their own request. An approver needs a licence, the same as the person asking.

For PIM for Groups there are no default approvers. If the policy requires approval and names nobody, requests wait for nobody. PIM for Entra roles behaves differently. When a policy requires approval and names no approver, active Privileged Role Administrators and Global Administrators are the approvers. When a policy does not require approval, activation is immediate in both.

Where automation stops

An application can do the administrative half of PIM. It can grant eligibility and write the policy above, from configuration, in a pull request. It cannot do the other half, and Microsoft says so in two places.

You can create eligible role assignments for users, but you can’t create eligible role assignments for applications, service principals, or managed identities because they can’t perform the activation steps.

Eligible and time-bound role assignments in Azure RBAC, Microsoft Learn

Approvers can’t approve their own role activation requests. Additionally, service principals aren’t allowed to approve requests.

Approve activation requests for group members and owners, Microsoft Learn

Every approve endpoint in Graph lists application permissions as not supported. Activation and approval are delegated operations. A person, signed in, every time.

That decides what a command-line approval tool can be. It has to run under a user token, and the first attempt to get one fails:

$ az login --scope https://graph.microsoft.com/PrivilegedAccess.ReadWrite.AzureADGroup

AADSTS65002: Consent between first party application '04b07795-...' and first party resource '00000003-...' must be configured via preauthorization. Applications owned and operated by Microsoft must get approval from the API owner before requesting tokens for that API.

The Azure CLI is a Microsoft-owned application. Which Graph permissions it may request is fixed by Microsoft, and no consent granted in your own tenant adds one. The token has to come from a client that is allowed to ask. Microsoft Graph Command Line Tools, another first-party application, carries the PIM scopes once a Global Administrator consents to the delegated permissions the approval calls need:

  • PrivilegedAssignmentSchedule.ReadWrite.AzureADGroup
  • PrivilegedAccess.ReadWrite.AzureADGroup
  • PrivilegedAssignmentSchedule.Remove.AzureADGroup

A token carries only the scopes that were requested. Leave one out and the next call returns 403 PermissionScopeNotGranted naming exactly the permission that is missing, which is how this list was assembled.

The approval itself is one call:

PATCH /identityGovernance/privilegedAccess/group/assignmentApprovals/{id}/stages/{stageId}

{ "reviewResult": "Approve", "justification": "ACME-1143" }

A 409 means a colleague was faster. A delegated token also means the audit log records every scripted decision as one the signed-in person made. “A human reviewed this” and “a human’s script reviewed this” are different controls.

The approval flow

PIM sends the approver an email. That email is where the flow starts, not the API. Reading requests over Graph would need an app registration, admin consent and a client secret that expires and breaks the flow quietly. PIM has no change notifications, so both routes poll anyway.

PIM email
  -> n8n, every minute
  -> Slack message
  -> approve or deny, on the command line

n8n reads the mailbox, parses the HTML table in the email, and posts a message with who, which group, the reason, the ticket and the start time, with a link to the portal.

The email lacks two fields, the duration and the end time. It also reports the role as Member for every request, owner included, because the privilege is in the group name. So the message answers “someone wants production” and cannot answer “for how long, and as what”.

The last step is a small script of mine. It exists because I did not want to leave the terminal for the portal, and because I automate anything I do twice. It reads the request from Graph under my own token and shows the fields the email lacks.

It also flags what an approver would want to notice. Production with no ticket reference, owner requested, longer than eight hours, a start outside business hours. Then it approves or denies, and records the requester’s ticket number as the reason, because “approved” tells nobody anything six months later.

Nothing in it approves on its own. The whole chain could close without me, and every piece for that exists. I have not connected them, because every automatic approval would still carry my name in the log, and I am not yet willing to sign for decisions I did not look at. Semi-automation is the line I am comfortable with today. It may move.

What you get, what stays manual

  • Nobody is a member of a production group at rest. The membership list is empty between activations.
  • Every hour of privileged access has a name, a ticket, an approver and an end time in the audit log.
  • Eligibility expires, so the list of who may ask is re-decided every year instead of inherited.
  • The policy is configuration in a repository, reviewed like any other change, even where the pipeline may not apply it.

What stays manual is the decision. A person approves, on a user token, with a licence. That is by design and by Microsoft’s rule, not a gap waiting for a feature.

The other thing PIM does not reach is a group it cannot onboard, a dynamic group or one synchronised from on-premises. Access there still expires only when a person removes it. A ledger with a date beside every grant is the control, and whoever reads the ledger is the expiry.

The ladder, from the top

Three pieces, three rungs.

  • One rule. Permissions inherit downward and nothing lower takes them back. That is why an estate accumulates access instead of failing, and it was the whole of the first piece.
  • Groups carry the roles. The configuration creates them, the directory fills them, and no person holds a role directly. The second piece.
  • Nobody is a member at rest. The last standing membership becomes the right to ask, with a clock and an approver. This one.

The rungs are not a roadmap. Ten people in one room are right to stop at the rule. Several teams and a shared production are right to stop at the groups. The clock is for the estate where “who can write to production right now” needs an answer shorter than the membership list, and where somebody is willing to be woken up to approve.


Part 3 of three. Part 1 is Azure identity, from startup to enterprise, Part 2 is Security groups at scale.

azureidentityrbacgovernance

read next