Skip to content

Access Packages 101: How the System Works

Before diving into the step-by-step runbook, this page explains the moving parts that make up the privileged access system: what an access package actually is, how it connects to Privileged Access Groups (PAGs) and PIM, why the Terraform code is split across repositories, and why onboarding a new persona takes a three-step handshake.


1. The Problem This System Solves

In many cloud tenants, privileged access is managed in one of two flawed ways:

  1. ClickOps in the portal: Admins assign roles or PIM eligibility to people by hand. Over time, nobody remembers who holds what or why.
  2. Hardcoding human UPNs in Terraform: Listing pim_eligible_users = ["alice@example.com"] in git turns every routine access request or expiry into a pull request—causing review fatigue, rubber-stamping, and slow emergency response.

This platform separates structure and policy (managed in Terraform) from runtime human access (managed in Entra ID):

  • Terraform defines the what and the rules: Which roles are bundled together into a persona, who is allowed to request it, who must approve it, how long eligibility lasts, and how long a single activation lasts. No individual human UPN appears anywhere in Terraform.
  • Entra ID Governance handles the who at runtime: Engineers request access packages in MyAccess (myaccess.microsoft.com) and activate them on demand in PIM for Groups.

2. The Building Blocks

Every privileged persona (such as Platform Owner or Cloud Estate Security Auditor) is assembled from six connected pieces in Microsoft Entra ID and Azure:

flowchart LR
    subgraph LaneA["Lane A: azure-privileged-access-grants (Human Applied)"]
        REQ["Requestor Group<br/>(sg-priv-tier-0-requestors)"]
        APP["Approver Group<br/>(sg-priv-tier-0-approvers)"]
        PAG["Persona Group (PAG)<br/>e.g. pag-owner-mg-platform"]
        ROLES["Permissions<br/>• Entra Directory Roles<br/>• Azure RBAC Grants"]
        PAG --> ROLES
    end

    subgraph LaneB["Lane B: azure-privileged-access-packages (CI Applied)"]
        CAT["Access Package Catalog<br/>(Tier 0 Privileged Access)"]
        PKG["Access Package<br/>e.g. (tier 0) Owner & Security Admin"]
        POL["Assignment Policy<br/>• Duration (90d)<br/>• Quarterly Review"]
        PIM["PIM Activation Policy<br/>• Max 4h<br/>• MFA + Justification"]
    end

    CAT --> PKG
    PKG --> POL
    REQ -.->|"Who can request"| POL
    APP -.->|"Who approves & reviews"| POL
    PKG ==>|"Grants Eligible Member"| PAG
    PIM ==>|"Governs activation"| PAG
Component Example What it does
1. Persona Group / PAG pag-owner-mg-platform An Entra ID security group that holds all the permissions for a specific job function (both Entra ID directory roles and Azure RBAC role assignments). Humans are never assigned roles directly; the group holds the roles.
2. Access Package Catalog Tier 0 Privileged Access A folder/container in Entra ID Entitlement Management that holds the persona groups (as "catalog resources") and the access packages that grant access to them.
3. Access Package (tier 0) Owner & Security Admin at Platform Management Group The "menu item" engineers see in myaccess.microsoft.com. When granted, it binds the user to the Persona Group as an Eligible Member (never an active standing member).
4. Assignment Policy & Review 90 days + quarterly review Attached to the Access Package. Defines who can request the package, whether an approver must sign off, how many days the assignment lasts before expiring, and the recurring quarterly access review.
5. PIM Activation Policy Max 4 hours, MFA required A PIM for Groups policy attached to the Persona Group. Controls what happens when an Eligible Member clicks "Activate" to temporarily become an Active Member.
6. Governance Groups & Owners sg-priv-tier-0-requestors
sg-priv-tier-0-approvers
Break-glass owners
Role-assignable groups that control who can request or approve packages, plus the two break-glass accounts set as the sole permanent owners of every Tier 0 group.

3. Two-Stage Just-In-Time (JIT) Flow: How a Human Gets Access

Because an Access Package grants PIM Eligibility rather than active group membership, getting active permissions is a two-stage process:

sequenceDiagram
    autonumber
    actor Engineer
    participant MyAccess as MyAccess Portal<br/>(Entitlement Management)
    participant PIM as Entra PIM for Groups
    participant PAG as Persona Group (PAG)<br/>(Directory Roles + Azure RBAC)

    Note over Engineer,PAG: Stage 1 — Eligibility (Up to 90 Days)
    Engineer->>MyAccess: Request Access Package
    MyAccess->>PIM: Add Engineer as "Eligible Member" of PAG
    Note right of PIM: Zero active permissions at rest

    Note over Engineer,PAG: Stage 2 — JIT Activation (1 to 4 Hours)
    Engineer->>PIM: Activate group (MFA + Justification)
    PIM->>PAG: Temporarily add Engineer as "Active Member"
    PAG-->>Engineer: Entra Roles & Azure RBAC active for 4 hours
    PIM->>PAG: Auto-remove Active Member when timer expires
  1. Stage 1 — Request Eligibility (Entitlement Management): An engineer in sg-priv-tier-0-requestors goes to myaccess.microsoft.com and requests the access package. Once granted, Entra makes them an Eligible Member of the underlying pag-* group for up to 90 days (subject to a quarterly access review). While merely eligible, the engineer has zero active permissions.
  2. Stage 2 — Activate for a Task (PIM for Groups): When the engineer actually needs to perform privileged work, they go to Entra PIM → Groups and activate the pag-* group (completing MFA and entering a justification). Entra makes them an Active Member of the group for the configured window (typically 1–4 hours). As soon as the timer expires, Entra removes them from active membership and the permissions vanish.

4. Why the Code Is Split Across Repositories (Lane A vs. Lane B)

When you look at the codebase, a single persona is split across two deployment repositories (plus a central policy module). This split is forced by two security and technical constraints in Microsoft Entra ID:

  1. Why CI cannot create the groups and role grants (Lane A): Assigning an Entra ID directory role requires the Microsoft Graph permission RoleManagement.ReadWrite.Directory (or Privileged Role Administrator). Any identity holding that permission can grant Global Administrator to anyone. Under the Tier 0 Pipeline Design, no CI pipeline is ever allowed to hold standing Tier 0 control-plane permissions. Therefore, creating the PAG and assigning its roles must be applied by a human using a PIM-activated role (azure-privileged-access-grants).
  2. Why a human CLI cannot create the access packages and PIM policies (Lane B): Configuring Access Packages (EntitlementManagement.ReadWrite.All) and PIM for Groups policies (RoleManagementPolicy.ReadWrite.AzureADGroup) requires Microsoft Graph scopes that the Azure CLI (az login) is blocked from requesting (AADSTS65002 — Microsoft does not pre-authorize the Azure CLI first-party app for those scopes). Because Terraform's providers only support human authentication via Azure CLI, a human literally cannot apply these resources from a terminal. They must be applied by a CI service principal (azure-privileged-access-packages).
Repository Delivery Lane Applied By What It Manages
solution-priv-tier-0 Policy Module Semver tag + MR The central allow-list of Entra directory roles and Tier 0 RBAC scopes. Prevents downstream repos from inventing unauthorized grants.
azure-privileged-access-grants Lane A Human (./apply.ps1 using CI's saved plan) Creates each pag-* group, assigns its Entra directory roles and Azure RBAC roles, and manages sg-priv-tier-0-requestors and sg-priv-tier-0-approvers.
azure-privileged-access-packages Lane B (restricted) GitLab CI on merge to main (sp-pla-tf-tier-0-access-packages-rw) Creates the Tier 0 Privileged Access catalog, each persona's PIM activation policy, and each persona's Access Package + Eligible Member binding.

5. The Role-Assignable Group Catch (catalog_onboarding = true)

If you add or modify a persona that includes Entra ID directory roles, you will encounter this error if you try to create its access package right away:

group <id> is role-assignable, and Entra only lets this pipeline add it to the catalog if it owns the group (CallerNotResourceOwner otherwise). Set catalog_onboarding = true on its persona (solution-manage-pag-persona v1.4.0+) in the grants project and apply it, then re-run this pipeline.

Why this happens

  1. Role-assignable groups have extra protection in Entra ID: In Entra ID, a group can only be assigned a directory role (like Security Administrator or Global Reader) if the group is created with isAssignableToRole = true ("role-assignable"). Because anyone who can add members to a role-assignable group effectively gets those directory roles, Microsoft locks them down: a standard app with Group.ReadWrite.All is not allowed to modify them.
  2. Adding a role-assignable group to a Catalog requires Group Ownership: The CI service principal for azure-privileged-access-packages (sp-pla-tf-tier-0-access-packages-rw) has permission to manage the Access Package Catalog (EntitlementManagement.ReadWrite.All). For a normal group, that is enough to add the group to the catalog. However, for a role-assignable group, Entra ID rejects the API call with CallerNotResourceOwner unless the calling service principal is explicitly listed as an Owner of that group.
  3. Why we don't leave the CI pipeline as a permanent Owner: An Owner of a role-assignable group can add active members directly to the group at any time—completely bypassing Access Packages and PIM. If the Lane B CI pipeline remained an owner permanently, anyone who could merge code into azure-privileged-access-packages could silently add themselves to Tier 0 groups.

The 3-Step Onboarding Handshake

To satisfy Entra ID without leaving a permanent backdoor, onboarding a role-assignable persona uses a temporary ownership flag:

sequenceDiagram
    autonumber
    participant Grants as 1. Grants Repo (Lane A)
    participant Entra as Microsoft Entra ID
    participant Packages as 2. Packages Repo (Lane B)

    Grants->>Entra: Create PAG with catalog_onboarding = true<br/>(Adds Packages CI SP as temporary group owner)
    Packages->>Entra: Pre-check verifies CI SP owns the group
    Packages->>Entra: Add PAG to Catalog & bind Access Package
    Grants->>Entra: Remove catalog_onboarding = true<br/>(Revokes Packages CI SP ownership — only break-glass owners remain)
  1. Turn on temporary ownership (Lane A — Grants): In azure-privileged-access-grants, set catalog_onboarding = true on the persona's solution-manage-pag-persona module block and apply. Under the hood, the module adds sp-pla-tf-tier-0-access-packages-rw as a group owner alongside the break-glass accounts.
  2. Onboard to the catalog (Lane B — Packages): In azure-privileged-access-packages, merge the new access package and PIM policy. Because the CI pipeline now owns the group, its pre-check passes and Entra allows it to add the group to the Tier 0 Privileged Access catalog.
  3. Turn off temporary ownership (Lane A — Grants): Back in azure-privileged-access-grants, remove catalog_onboarding = true and apply again. The CI pipeline is removed from the group's owners. The access package continues to work indefinitely because Entra's internal Entitlement Management service manages memberships once a group is in the catalog.

Automated reminders in CI

You don't have to memorize this order—the terraform-azuread-access-package module checks it automatically on every terraform plan:

  • Before onboarding: If the group isn't in the catalog yet and catalog_onboarding = true hasn't been applied in grants, the MR plan fails with instructions so a broken apply never reaches main.
  • After onboarding: Once the group is in the catalog, every plan emits a warning ("is in the catalog but this pipeline still owns it") until you remove catalog_onboarding = true in grants.

What about RBAC-only personas?

If a persona only grants Azure RBAC roles (such as pag-reader-mg-platform) and has no directory_role_template_ids, its group is not role-assignable (isAssignableToRole = false). Entra allows the packages pipeline to add non-role-assignable groups to the catalog without owning them, so catalog_onboarding is not needed.


6. Built-In Security Guardrails

Together, these components enforce a strict set of guardrails across the system:

  • Zero humans in Terraform for package eligibility: No user UPNs appear in azure-privileged-access-packages. Terraform builds the machinery (sg-priv-tier-0-requestors in Lane A gates who may ask); humans request access through MyAccess.
  • Break-glass group owners only: Every PAG's permanent group_owners are the two break-glass accounts (local.tier_0_group_owner_ids)—never the engineer who ran apply and never a day-to-day account, because a group owner can add active members directly without PIM.
  • Locked requestor and approver groups: sg-priv-tier-0-requestors and sg-priv-tier-0-approvers are created as role-assignable groups in azure-privileged-access-grants (Lane A). The Lane B packages pipeline can read their IDs to attach them to policies, but cannot change who is inside them.
  • Eligible Member by default (with drift detection): Because the azuread Terraform provider doesn't yet support EligibleMember bindings natively (Issue #3266), the terraform-azuread-access-package module binds the group as Eligible Member via the Microsoft Graph API and runs a continuous check block that warns if an active Member binding ever appears.
  • Automatic expiry & quarterly reviews: Access package assignments expire after 90 days (1 day for Emergency Tenant Recovery), and a quarterly access review automatically revokes any assignment not re-confirmed within 14 days.

7. Attribute-Based Auto-Assignment & Group-Gated Requests

Instead of manually maintaining a list of who gets access—or who is allowed to request access—you can drive Access Packages from user profile attributes (such as jobTitle = "Platform Engineer" or department = "Cloud Platform", synced from on-prem AD DS).

There are two ways to use profile data and groups with Access Packages, plus a few important Entra ID rules about nested groups and PIM eligibility:

flowchart LR
    ATTR["User Profile Attribute<br/>jobTitle == 'Platform Engineer'"]

    subgraph Direct["Pattern A: Auto-Assign the Package"]
        AUTO_POL["Automatic Assignment Policy<br/>(No MyAccess request needed)"]
        STANDING["Direct Group Membership<br/>(Standing Active Member)"]
        AUTO_POL --> STANDING
    end

    subgraph Gatekeeper["Pattern B: Unlock Ability to Request Package B"]
        GRP_A["Group A (Requestor Group)<br/>Dynamic Group or Auto-Assigned by Package A"]
        PKG_B["Access Package B<br/>(Shows up in MyAccess to request)"]
        PIM_B["Eligible Member of PAG<br/>→ 4h PIM Activation"]
        GRP_A -->|"Allows requesting"| PKG_B --> PIM_B
    end

    ATTR ==>|"Option 1: Skip request"| AUTO_POL
    ATTR ==>|"Option 2: Gatekeeper"| GRP_A

Pattern A: Auto-Assigning a Package from Profile Attributes (No Request Needed)

In Entra Entitlement Management, an Access Package can have an Automatic Assignment Policy instead of a request-based policy. You define a dynamic rule against the user's profile attributes, for example:

(user.jobTitle -eq "Platform Engineer") and (user.accountEnabled -eq true)
  • What happens: As soon as a user's profile in Entra ID has jobTitle = "Platform Engineer", Entra automatically grants them the Access Package without them ever opening MyAccess. The moment their job title changes in HR / AD DS, Entra automatically removes the package assignment.
  • The Catch (Active Member vs. PIM Eligible Member): Entra ID's Automatic Assignment Policies currently only support adding users as an active standing Member of a group—they cannot bind users as a PIM Eligible Member, and they do not support approval stages or 90-day time-boxed expirations.
  • Best for: Baseline birthright access (VPN groups, DevOps tools, standard Tier 2 reader access) or populating a Requestor Group (see Pattern B below).
  • Not used for: Direct Tier 0 PAG membership, because Tier 0 requires a deliberate request + PIM JIT activation (Eligible Member).

Pattern B: "If You're in Group A, You Can Request Access Package B" (Gatekeeper Pattern)

For privileged roles where you still want the user to request Access Package B in MyAccess (so they get 90-day Eligible Member status + 4-hour PIM activation), you use their profile attributes to automatically place them into Group A (the Requestor Group), which unlocks the ability to request Access Package B.

Why Nested Groups Don't Work Here

It is tempting to create a dynamic group grp-platform-engineers (Group A) and nest it inside sg-priv-tier-0-requestors (Group B). Entra ID blocks this in two places:

  1. Role-assignable groups forbid nesting: In this platform, sg-priv-tier-0-requestors has assignable_to_role = true so that only a Privileged Role Administrator can change who is in it. Microsoft Entra ID does not allow nesting a group inside a role-assignable group, and does not allow dynamic membership rules on role-assignable groups.
  2. Entitlement Management checks direct membership: Even on normal groups, Access Package requestor policies evaluate direct members of the configured requestor group, not transitive nested groups.

Two Clean Ways to Do It Instead (Without Nesting)

Approach How it works Security trade-off
1. Dynamic Requestor Group
(Best for Tier 1 / Tier 2 packages)
Create Group A (sg-platform-engineer-requestors) as a Dynamic Security Group with the rule user.jobTitle -eq "Platform Engineer". Point Access Package B's requestor_group_object_id directly at Group A. Anyone with jobTitle = "Platform Engineer" is automatically placed into Group A and can immediately see and request Access Package B in MyAccess. Dynamic groups cannot be role-assignable (assignable_to_role = false). An identity with Groups Administrator or Group.ReadWrite.All could alter the dynamic rule, so this is best for Tier 1/2 packages rather than Tier 0.
2. Chained Access Packages
(Package A auto-assigns Group A → unlocks requesting Package B)
Create Access Package A ("Baseline: Platform Engineer") with an Automatic Assignment Policy (user.jobTitle -eq "Platform Engineer"). Package A's resource is Group A (the requestor group). When a user's profile matches, Package A automatically makes them a direct member of Group A. Because they are now in Group A, Access Package B appears in their MyAccess portal ready to request. Keeps everything inside Entitlement Management with full audit trails of why the user was added to the requestor group, and works even when Group A is a role-assignable group (once onboarded to the catalog).

Next Steps