-
Couldn't load subscription status.
- Fork 23
Invoke‐EasyPIMOrchestrator step‐by‐step guide
A safe, step-by-step plan to exercise the orchestrator and policies in a real tenant. Each step includes a minimal JSON and a preview (-WhatIf) run before applying.
EasyPIM is now split into two complementary modules:
-
EasyPIM (Core Module): Provides individual PIM management functions for backup, restore, policy configuration, and assignment management. Use this for targeted operations and building custom scripts.
-
EasyPIM.Orchestrator: Provides comprehensive configuration management through
Invoke-EasyPIMOrchestrator, policy drift detection, and end-to-end workflows. This module depends on the core EasyPIM module.
This guide focuses on the orchestrator workflows, but individual core functions can be used independently for specific tasks.
- Step 0 — Backup current policies (once)
- Step 1 — Minimal config: ProtectedUsers only
- Step 2 — Entra role policy (inline)
- Step 3 — Entra role policy (template + 🆕 template + inline override)
- Step 4 — Entra role assignments (multiple assignments per role supported)
- Step 5 — Azure role policy (inline; Scope is required)
- Step 6 — Azure role policy (template + 🆕 template + inline override)
- Step 7 — Azure assignments (1 Eligible + 1 Active)
- Step 8 — Optional: Groups (Policies + Assignments)
- Step 9 — Apply changes (remove -WhatIf)
- Step 10 — Use the Same Config from Azure Key Vault (Optional)
- Step 11 — (Optional, Destructive) Reconcile with initial mode
- Step 12 — Comprehensive policy validation (all options)
- Step 13 — Detect policy drift with Test-PIMPolicyDrift
- Step 14 — (Optional) CI/CD automation (GitHub Actions + Key Vault)
- Appendix — Tips & Safety Gates
- TenantId and SubscriptionId for the target environment
- Principal Object IDs (Users/Groups/Service Principals) to test with
-
EasyPIM modules installed and authenticated context:
-
EasyPIM(core module) - provides backup, individual role management, and policy functions -
EasyPIM.Orchestrator- provides orchestration capabilities (Invoke-EasyPIMOrchestrator)
-
- Path for your config file, e.g.,
C:\Config\pim-config.json
# Install both modules from PowerShell Gallery
Install-Module -Name EasyPIM -Scope CurrentUser
Install-Module -Name EasyPIM.Orchestrator -Scope CurrentUser
# Import modules (orchestrator automatically imports core as dependency)
Import-Module EasyPIM.OrchestratorNote: The EasyPIM.Orchestrator module depends on the core EasyPIM module and will automatically import it. You can work with individual PIM functions using the core module alone, or use the orchestrator for comprehensive configuration management.
Before running any commands, establish authenticated sessions:
# Connect to Microsoft Graph (required for Entra ID PIM operations)
Connect-MgGraph -Scopes "RoleManagement.ReadWrite.Directory"
# Connect to Azure (required for Azure resource PIM operations)
Connect-AzAccount
Set-AzContext -SubscriptionId "<your-subscription-id>"Note: The orchestrator includes automatic connection checks and will prompt if authentication is missing.
Tip: Keep one file and replace/append sections as you move through steps.
Note: This step may take up to an hour depending on the number of roles and policies in your tenant.
Note: Backup functions (
Backup-PIMEntraRolePolicy,Backup-PIMAzureResourcePolicy) are provided by the coreEasyPIMmodule.
By default,
Backup-PIMAzureResourcePolicyworks at the subscription level. If you want to back up policies at a different scope, you can use the-scopeparameter instead of-subscriptionID.
Commands
# It is recommended to specify a path for the backup file:
Backup-PIMEntraRolePolicy -tenantID $env:TenantID -path C:\Temp\pimentrapolicybackup.csv
Backup-PIMAzureResourcePolicy -tenantID $env:TenantID -subscriptionID $env:SubscriptionID -path C:\Temp\pimazureresourcepolicybackup.csvGoal: Establish a safety baseline that guarantees your break‑glass / critical principals can never be removed by later reconciliation steps (especially Step 11 initial mode). ProtectedUsers is a hard exclusion list used by cleanup logic: any assignment held by these object IDs is always preserved (reported as Protected, never Removed / WouldRemove). Start with ONLY this section so you can preview the orchestration pipeline and principal validation without risking unintended deletions.
What to include:
- Break‑glass emergency access accounts (cloud‑only preferred, strong MFA)
- Core IAM / security operations groups or service principals that must retain standing access while you transition
- Accounts required to fix the system if later steps misconfigure policies
What NOT to include (anti‑patterns):
- Large generic groups (bloats permanent access and reduces visibility)
- Expired / personal test accounts (defeats cleanup objectives)
- Every admin in the tenant (use assignments + policies instead)
Best practices:
- Keep the list short (aim for 1–5 principals).
- Use GUIDs (object IDs) not display names to avoid ambiguity.
- Revisit periodically; remove stale entries once confidence is high.
- Never run initial (destructive) mode until this list is validated in a -WhatIf preview.
Write pim-config.json
{
"ProtectedUsers": [
"00000000-0000-0000-0000-000000000001",//break glass account objectID
"00000000-0000-0000-0000-000000000002"//IAM admins Group obejctID
]
}Preview
Invoke-EasyPIMOrchestrator -ConfigFilePath "C:\Config\pim-config.json" -TenantId "<tenant-guid>" -SubscriptionId "<sub-guid>" -WhatIfWrite pim-config.json (always keep ProtectedUsers first; you can add comments for clarity):
Warning: If
ApprovalRequiredis true, you must specify at least one approver in theApproversarray.
This example above uses only a subset of available options. Refer to Step 12 for the complete list of supported options.
Preview (policies only)
Invoke-EasyPIMOrchestrator -ConfigFilePath "C:\Config\pim-config.json" -TenantId "<tenant-guid>" -SubscriptionId "<sub-guid>" -WhatIf -SkipAssignmentsWhy templates? A PolicyTemplate lets you define a reusable policy profile once (durations, requirements, approvals, notifications, auth context, limits) and then reference it by name under multiple roles. Benefits:
- DRY & consistency – one edit propagates everywhere (e.g., change ActivationRequirement in Standard template and every role using it updates next run).
- Safer iteration – you preview a single template change impact across all roles (-WhatIf) before applying.
- Clear diffs – PRs show a small change in one template block instead of many duplicated inline edits.
- Easier promotion – copy a vetted template set from test → prod without hunting per‑role tweaks.
- Guardrails – high‑risk roles point to a hardened template (HighSecurity) while low‑risk roles stay on Standard.
Override strategy (important): The current engine resolves either a Template OR an inline policy for a role; it does NOT merge a template plus per‑role overrides field‑by‑field. To “override” for a specific role you simply stop using the Template reference and replace it with a full inline policy object for that role. (Future enhancement could add partial overlay, but today it is a switch, not a merge.)
Practical pattern:
- Start with templates for 90% of roles (Standard / HighSecurity, etc.).
- If one role needs a deviation (e.g., shorter ActivationDuration), replace its
{ "Template": "Standard" }with a full inline policy object and adjust only the differing fields (you can copy the template contents as a starting point). - If later the deviation is no longer needed, revert back to the template reference to rejoin centralized management.
Example override (template → inline):
"EntraRoles": {
"Policies": {
"User Administrator": {
- "Template": "HighSecurity"
+ "ActivationDuration": "PT1H", // shortened for this role only
+ "ActivationRequirement": "MultiFactorAuthentication,Justification",
+ "ApprovalRequired": true,
+ "Approvers": [ { "id": "aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee", "description": "PIM Approver 1" } ],
+ "AuthenticationContext_Enabled": true,
+ "AuthenticationContext_Value": "c1:HighRiskOperations"
}
}
}Tip: Keep the number of distinct templates small; too many templates = implicit inline sprawl.
The orchestrator now supports combining templates with inline property overrides! This provides the best of both worlds: template consistency with targeted customization.
Template + Override Example:
{
"EntraRoles": {
"Policies": {
"Global Administrator": {
"Template": "HighSecurity", // Base template provides most settings
"ActivationDuration": "PT1H", // Override: shorter than template's PT2H
"MaximumEligibilityDuration": "P60D" // Override: shorter than template's P90D
// All other HighSecurity properties (ApprovalRequired, Approvers, etc.) remain unchanged
},
"Exchange Administrator": {
"Template": "Standard", // Base template
"ApprovalRequired": true, // Override: add approval requirement
"Approvers": [
{ "id": "aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee", "description": "Exchange Admins" }
]
}
}
}
}Benefits:
- Consistency: Most properties inherit from the template
- Flexibility: Override only the properties that need customization
- Maintainability: Template changes still propagate to non-overridden properties
- Backward Compatibility: Existing template-only and inline-only configurations continue to work
Available for all policy types: EntraRoles, AzureRoles, and GroupRoles all support template + override patterns.
Write pim-config.json
{
// Object IDs for which assignments will not be removed
"ProtectedUsers": [
"00000000-0000-0000-0000-000000000001" // Example: Breakglass account
],
"PolicyTemplates": {
// Default/normal template for most roles
"Standard": {
"ActivationDuration": "PT8H",
"ActivationRequirement": "MultiFactorAuthentication,Justification",
"ApprovalRequired": false
},
// High security template with advanced options
"HighSecurity": {
"ActivationDuration": "PT2H",
"ActivationRequirement": "MultiFactorAuthentication,Justification",
"ApprovalRequired": true,
"Approvers": [
{ "id": "aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee", "description": "PIM Approver 1" }
],
"AuthenticationContext_Enabled": true,
"AuthenticationContext_Value": "c1:HighRiskOperations",
"MaximumEligibilityDuration": "P90D",
"MaximumActiveAssignmentDuration": "P30D",
"Notifications": {
"Eligibility": {
"Alert": { "isDefaultRecipientEnabled": true, "NotificationLevel": "All", "Recipients": ["pim-alerts@contoso.com"] },
"Assignee": { "isDefaultRecipientEnabled": true, "NotificationLevel": "All", "Recipients": ["pim-assignees@contoso.com"] },
"Approvers": { "isDefaultRecipientEnabled": true, "NotificationLevel": "All", "Recipients": ["pim-approvers@contoso.com"] }
},
"Active": {
"Alert": { "isDefaultRecipientEnabled": true, "NotificationLevel": "All", "Recipients": ["pim-alerts@contoso.com"] },
"Assignee": { "isDefaultRecipientEnabled": true, "NotificationLevel": "All", "Recipients": ["pim-assignees@contoso.com"] },
"Approvers": { "isDefaultRecipientEnabled": true, "NotificationLevel": "All", "Recipients": ["pim-approvers@contoso.com"] }
},
"Activation": {
"Alert": { "isDefaultRecipientEnabled": true, "NotificationLevel": "All", "Recipients": ["pim-alerts@contoso.com"] },
"Assignee": { "isDefaultRecipientEnabled": true, "NotificationLevel": "All", "Recipients": ["pim-assignees@contoso.com"] },
"Approvers": { "isDefaultRecipientEnabled": true, "NotificationLevel": "All", "Recipients": ["pim-approvers@contoso.com"] }
}
}
}
},
"EntraRoles": {
"Policies": {
// Use Standard template for most roles
"User Administrator": { "Template": "Standard" },
// Use HighSecurity template for sensitive roles
"Privileged Role Administrator": { "Template": "HighSecurity" }
}
}
}Preview (policies only)
Invoke-EasyPIMOrchestrator -ConfigFilePath "C:\Config\pim-config.json" -TenantId "<tenant-guid>" -SubscriptionId "<sub-guid>" -WhatIf -SkipAssignmentsNote: The orchestrator supports multiple assignments per role in the Assignments block. Provide an array of assignment objects; each will be processed individually.
Note: The orchestrator supports a unified Assignments schema with an assignmentType field (Eligible or Active). This is parsed by Initialize-EasyPIMAssignments and mapped internally to legacy sections. If you prefer the legacy format, see the alternative below.
Note: principalType is optional in modern Assignments examples; the orchestrator infers the object type (User/Group/Service Principal) from the ID. It's kept only for legacy readability and can be omitted below.
Write pim-config.json
Note: This config snippet only shows the Assignments block for Step 5. Policies defined in previous steps (such as EntraRoles or PolicyTemplates) can also be present in the same config file. End the Assignments block with a comma if you are including additional keys.
{
"ProtectedUsers": [ //object ids for which the assignements will not be removed
"7a55ec4d-028e-4ff1-8ee9-93da07b6d5d5" //Breakglass account
],
"PolicyTemplates": {
// Default/normal template for most roles
"Standard": {
"ActivationDuration": "PT8H",
"ActivationRequirement": "MultiFactorAuthentication,Justification",
"ApprovalRequired": false
},
// High security template with advanced options
"HighSecurity": {
"ActivationDuration": "PT2H",
"ActivationRequirement": "MultiFactorAuthentication,Justification",
"ApprovalRequired": true,
"Approvers": [
{ "id": "2ab3f204-9c6f-409d-a9bd-6e302a0132db", "description": "IAM_approvers" }
],
"AuthenticationContext_Enabled": true,
"AuthenticationContext_Value": "c1:HighRiskOperations",
"MaximumEligibilityDuration": "P90D",
"MaximumActiveAssignmentDuration": "P30D",
"Notifications": {
"Eligibility": {
"Alert": { "isDefaultRecipientEnabled": true, "NotificationLevel": "All", "Recipients": ["pim-alerts@contoso.com"] },
"Assignee": { "isDefaultRecipientEnabled": true, "NotificationLevel": "All", "Recipients": ["pim-assignees@contoso.com"] },
"Approvers": { "isDefaultRecipientEnabled": true, "NotificationLevel": "All", "Recipients": ["pim-approvers@contoso.com"] }
},
"Active": {
"Alert": { "isDefaultRecipientEnabled": true, "NotificationLevel": "All", "Recipients": ["pim-alerts@contoso.com"] },
"Assignee": { "isDefaultRecipientEnabled": true, "NotificationLevel": "All", "Recipients": ["pim-assignees@contoso.com"] },
"Approvers": { "isDefaultRecipientEnabled": true, "NotificationLevel": "All", "Recipients": ["pim-approvers@contoso.com"] }
},
"Activation": {
"Alert": { "isDefaultRecipientEnabled": true, "NotificationLevel": "All", "Recipients": ["pim-alerts@contoso.com"] },
"Assignee": { "isDefaultRecipientEnabled": true, "NotificationLevel": "All", "Recipients": ["pim-assignees@contoso.com"] },
"Approvers": { "isDefaultRecipientEnabled": true, "NotificationLevel": "All", "Recipients": ["pim-approvers@contoso.com"] }
}
}
}
},
"EntraRoles": {
"Policies": {
// Use Standard template for most roles
"Guest Inviter": { "Template": "Standard" },
"Testrole":{"Template":"Standard"},
// Use HighSecurity template for sensitive roles
"User Administrator": { "Template": "HighSecurity" }
}
},
// Example assignments block
"Assignments": {
"EntraRoles": [
{
"roleName": "User Administrator",
"assignments": [
{
"principalId": "f8b74308-47bf-4764-a31e-634e54c36212", //UserAdmin1 (ADM)
"assignmentType": "Eligible",
"justification": "My user Admins"
// If duration is not specified, the orchestrator will use the maximum allowed by policy
}
]
},
{
"roleName": "Guest Inviter",
"assignments": [
{
"principalId": "99999999-1111-2222-3333-444444444444", //GuestOpsUser1
"assignmentType": "Eligible",
"duration": "P30D", // Option example: explicit eligibility duration override
"justification": "Guest onboarding rotation"
},
{
"principalId": "99999999-5555-6666-7777-888888888888", //GuestOpsUser2
"assignmentType": "Eligible",
"permanent": true, // Option example: permanent eligibility (subject to policy allowing it)
"justification": "Primary guest management"
}
]
}
]
}
}Preview (assignments only)
Invoke-EasyPIMOrchestrator -ConfigFilePath "C:\Config\pim-config.json" -TenantId "<tenant-guid>" -SubscriptionId "<sub-guid>" -WhatIf -SkipPoliciesLegacy alternative (same outcome using legacy sections):
{
"ProtectedUsers": ["00000000-0000-0000-0000-000000000001"],
"EntraIDRoles": [
{
"roleName": "User Administrator",
"principalId": "11111111-1111-1111-1111-111111111111",
"principalType": "User",
"justification": "Ops rotation"
}
],
"EntraIDRolesActive": [
{
"roleName": "User Administrator",
"principalId": "22222222-2222-2222-2222-222222222222",
"principalType": "User",
"duration": "PT8H",
"justification": "Break-glass"
}
]
}Multiple principals
- Unified Assignments pattern: add multiple items under assignments[] for the same role.
{
"Assignments": {
"EntraRoles": [
{
"roleName": "User Administrator",
"assignments": [
{ "principalId": "11111111-1111-1111-1111-111111111111", "principalType": "User", "assignmentType": "Eligible", "justification": "Ops rotation" },
{ "principalId": "22222222-2222-2222-2222-222222222222", "assignmentType": "Eligible", "justification": "Ops rotation" }
]
}
]
}
}- Legacy pattern: use PrincipalIds (with an S) to batch expand in one entry.
{
"EntraIDRoles": [
{
"RoleName": "User Administrator",
"PrincipalIds": [
"11111111-1111-1111-1111-111111111111",
"22222222-2222-2222-2222-222222222222"
],
"PrincipalType": "User",
"Justification": "Ops rotation"
}
]
}Goal: Introduce your first Azure Role policy while preserving everything proven in Step 5 (ProtectedUsers, Entra role policy templates & assignments). Keep ProtectedUsers first for safety.
IMPORTANT: Some Azure built‑in roles are treated as protected in the orchestrator and their policies are intentionally not changed for safety (currently: "Owner" and "User Access Administrator"). If you try to target them you will see a [PROTECTED] message and no update occurs. For the first Azure policy example, use a non‑protected role such as "Reader" or "Contributor".
Use this if you maintain a single evolving file. Comments highlight what is NEW in this step.
{
// Always first – prevents accidental removals
"ProtectedUsers": [
"00000000-0000-0000-0000-000000000001" // Breakglass account
],
// From Step 5 (abbreviated for clarity)
"PolicyTemplates": {
"Standard": { "ActivationDuration": "PT8H", "ActivationRequirement": "MultiFactorAuthentication,Justification", "ApprovalRequired": false },
"HighSecurity": { "ActivationDuration": "PT2H", "ActivationRequirement": "MultiFactorAuthentication,Justification", "ApprovalRequired": true, "Approvers": [ { "id": "2ab3f204-9c6f-409d-a9bd-6e302a0132db", "description": "IAM_approvers" } ] }
},
"EntraRoles": {
"Policies": {
"Guest Inviter": { "Template": "Standard" },
"Testrole": { "Template": "Standard" },
"User Administrator": { "Template": "HighSecurity" }
}
},
"Assignments": {
"EntraRoles": [
{
"roleName": "User Administrator",
"assignments": [
{ "principalId": "f8b74308-47bf-4764-a31e-634e54c36212", "assignmentType": "Eligible", "justification": "My user Admins" }
]
},
{
"roleName": "Guest Inviter",
"assignments": [
{ "principalId": "99999999-1111-2222-3333-444444444444", "assignmentType": "Eligible", "duration": "P30D", "justification": "Guest onboarding rotation" },
{ "principalId": "99999999-5555-6666-7777-888888888888", "assignmentType": "Eligible", "permanent": true, "justification": "Primary guest management" }
]
}
]
},
// NEW in Step 6 (inline Azure policy for a NON-PROTECTED role: Reader)
"AzureRoles": {
"Policies": {
"Reader": {
"Scope": "/subscriptions/<sub-guid>",
"ActivationDuration": "PT1H",
"ActivationRequirement": "MultiFactorAuthentication",
"ApprovalRequired": false
}
}
}
}If you prefer to patch in just the new portion (assumes the earlier sections already exist above this block in your file):
{
"AzureRoles": {
"Policies": {
"Reader": {
"Scope": "/subscriptions/<sub-guid>",
"ActivationDuration": "PT1H",
"ActivationRequirement": "MultiFactorAuthentication",
"ApprovalRequired": false
}
}
}
}Preview (policies only)
Invoke-EasyPIMOrchestrator -ConfigFilePath "C:\Config\pim-config.json" -TenantId "<tenant-guid>" -SubscriptionId "<sub-guid>" -WhatIf -SkipAssignmentsGoal: Show the SMALL change from Step 6 (inline Azure policy) to a template-based Azure policy. Everything else from Step 6 stays the same. You have TWO equivalent options:
- Convert the SAME role (Reader) from inline properties to a template reference.
- Keep the original inline Reader policy and ADD a new template-based low-impact role (e.g. Tag Contributor) — useful if you want to compare side‑by‑side once.
Below are both patterns with an explicit, minimal diff so you can “see” the change clearly.
Step 6 Azure block (before):
"AzureRoles": {
"Policies": {
"Reader": {
"Scope": "/subscriptions/<sub-guid>",
"ActivationDuration": "PT1H",
"ActivationRequirement": "MultiFactorAuthentication",
"ApprovalRequired": false
}
}
}Step 7 replacement (after):
"AzureRoles": {
"Policies": {
"Reader": {
"Scope": "/subscriptions/<sub-guid>",
"Template": "Standard" // <— inline properties replaced by a template reference
}
}
}Minimal delta (diff style):
"AzureRoles": {
"Policies": {
"Reader": {
"Scope": "/subscriptions/<sub-guid>",
- "ActivationDuration": "PT1H",
- "ActivationRequirement": "MultiFactorAuthentication",
- "ApprovalRequired": false
+ "Template": "Standard"
}
}
}If you prefer to SEE both forms once, append only the new role (using Tag Contributor which has narrower scope than full Contributor):
"AzureRoles": {
"Policies": {
"Reader": { // unchanged inline from Step 6
"Scope": "/subscriptions/<sub-guid>",
"ActivationDuration": "PT1H",
"ActivationRequirement": "MultiFactorAuthentication",
"ApprovalRequired": false
},
"Tag Contributor": { // NEW template-based (low-impact)
"Scope": "/subscriptions/<sub-guid>",
"Template": "Standard"
}
}
}Later (Step 8 or whenever ready) you can delete the inline Reader block or convert it (Option A) to keep everything template-driven.
Only unchanged sections are compressed for readability.
{
"ProtectedUsers": ["00000000-0000-0000-0000-000000000001"],
"PolicyTemplates": {
"Standard": { "ActivationDuration": "PT8H", "ActivationRequirement": "MultiFactorAuthentication,Justification", "ApprovalRequired": false },
"HighSecurity": { "ActivationDuration": "PT2H", "ActivationRequirement": "MultiFactorAuthentication,Justification", "ApprovalRequired": true }
},
"EntraRoles": { "Policies": { "Guest Inviter": { "Template": "Standard" }, "Testrole": { "Template": "Standard" }, "User Administrator": { "Template": "HighSecurity" } } },
"Assignments": { /* (same as Step 6, omitted for brevity) */ },
"AzureRoles": {
"Policies": {
"Reader": { "Scope": "/subscriptions/<sub-guid>", "Template": "Standard" }
}
}
}Pick ONE of these depending on Option A or B:
Option A (replace existing block):
{
"AzureRoles": {
"Policies": {
"Reader": { "Scope": "/subscriptions/<sub-guid>", "Template": "Standard" }
}
}
}Option B (append Tag Contributor):
{
"AzureRoles": {
"Policies": {
"Tag Contributor": { "Scope": "/subscriptions/<sub-guid>", "Template": "Standard" }
}
}
}{
"AzureRoles": {
"Policies": {
"Contributor": {
"Scope": "/subscriptions/<sub-guid>",
"Template": "HighSecurity", // Base template
"ActivationDuration": "PT30M", // Override: shorter than template
"MaximumActiveAssignmentDuration": "PT8H" // Override: limit active time
// All other HighSecurity properties (ApprovalRequired, etc.) remain from template
}
}
}
}This approach gives you the security baseline of HighSecurity template while customizing activation and assignment durations for the high-privilege Contributor role.
Preview (policies only)
Invoke-EasyPIMOrchestrator -ConfigFilePath "C:\Config\pim-config.json" -TenantId "<tenant-guid>" -SubscriptionId "<sub-guid>" -WhatIf -SkipAssignmentsGoal: Add first Azure role assignments without altering existing policies. Everything from Step 6 (Azure role policy with template + inline override) remains; we only append an Assignments.AzureRoles block.
{
"ProtectedUsers": [ "00000000-0000-0000-0000-000000000001" ],
"PolicyTemplates": { ... },
"EntraRoles": { ... },
"Assignments": { // <— NEW section (already existed for EntraRoles earlier; now adding AzureRoles)
"EntraRoles": [ ... ], // (unchanged if present)
+ "AzureRoles": [
+ {
+ "roleName": "Tag Contributor",
+ "scope": "/subscriptions/<sub-guid>",
+ "assignments": [
+ { "principalId": "33333333-3333-3333-3333-333333333333", "principalType": "Group", "assignmentType": "Eligible", "justification": "Team access" },
+ { "principalId": "44444444-4444-4444-4444-444444444444", "principalType": "User", "assignmentType": "Active", "duration": "PT8H", "justification": "Maintenance window" }
+ ]
+ }
+ ]
}
}Use this if previous sections already exist exactly as-is above in your file.
{
"Assignments": {
"AzureRoles": [
{
"roleName": "Tag Contributor",
"scope": "/subscriptions/<sub-guid>",
"assignments": [
{
"principalId": "33333333-3333-3333-3333-333333333333",
"principalType": "Group",
"assignmentType": "Eligible",
"justification": "Team access"
},
{
"principalId": "44444444-4444-4444-4444-444444444444",
"principalType": "User",
"assignmentType": "Active",
"duration": "PT8H",
"justification": "Maintenance window"
}
]
}
]
}
}If you keep a compact working file focused on assignments delta:
{
"ProtectedUsers": ["00000000-0000-0000-0000-000000000001"],
"Assignments": {
"AzureRoles": [
{
"roleName": "Tag Contributor",
"scope": "/subscriptions/<sub-guid>",
"assignments": [
{ "principalId": "33333333-3333-3333-3333-333333333333", "principalType": "Group", "assignmentType": "Eligible", "justification": "Team access" },
{ "principalId": "44444444-4444-4444-4444-444444444444", "principalType": "User", "assignmentType": "Active", "duration": "PT8H", "justification": "Maintenance window" }
]
}
]
}
}Preview (assignments only)
Invoke-EasyPIMOrchestrator -ConfigFilePath "C:\Config\pim-config.json" -TenantId "<tenant-guid>" -SubscriptionId "<sub-guid>" -WhatIf -SkipPoliciesMultiple principals
- Unified Assignments pattern: add multiple items under assignments[] for the same role/scope.
{
"Assignments": {
"AzureRoles": [
{
"roleName": "Tag Contributor",
"scope": "/subscriptions/<sub-guid>",
"assignments": [
{ "principalId": "33333333-3333-3333-3333-333333333333", "principalType": "Group", "assignmentType": "Eligible", "justification": "Team access" },
{ "principalId": "55555555-5555-5555-5555-555555555555", "principalType": "User", "assignmentType": "Eligible", "justification": "Team access" }
]
}
]
}
}- Legacy pattern: use PrincipalIds (with an S) to batch expand in one entry.
{
"AzureRoles": [
{
"RoleName": "Tag Contributor",
"Scope": "/subscriptions/<sub-guid>",
"PrincipalIds": [
"33333333-3333-3333-3333-333333333333",
"55555555-5555-5555-5555-555555555555"
],
"PrincipalType": "Group",
"Justification": "Team access"
}
]
}Group policies ARE supported (Get-PIMGroupPolicy / Set-PIMGroupPolicy). The orchestrator resolves group policies via GroupRoles.Policies (preferred) or the deprecated GroupPolicies / Policies.Groups formats. We'll DEFINE a minimal policy first, then add assignments referencing it. This mirrors the security-first approach: establish guardrails (policy) before granting access (assignments).
Heads-up: AuthenticationContext_* in a shared template is ignored for Group policies. You can leave it in the template for Entra/Azure roles, but it won’t be applied to Groups.
NOTE: In
GroupRoles.Policiesyou may use either the group GUID (treated asGroupId) or a readable display name key (treated asGroupName). The orchestrator will resolveGroupNametoGroupIdat runtime. For production/stable configs prefer GUIDs to avoid ambiguity when duplicate or renamed groups exist. Assignments still require an explicitgroupIdfield.
QUICK NOTE (Auto‑Deferral): If a Group policy targets a group that is not yet PIM‑eligible (e.g. on‑premises synced or not onboarded), the orchestrator now DEFERS that policy instead of failing. It records status
DeferredNotEligible, proceeds with the rest of the run, then automatically retries those deferred group policies after the assignment phase. The final summary prints aDEFERRED GROUP POLICIESblock showing Applied / Still Not Eligible / Failed counts. To resolve a persistentStill Not Eligiblestate: (1) ensure the group is a cloud security group (not synced or M365 type unsupported), (2) enable PIM for the group in the portal (preview blade), then re-run the orchestrator. No action needed if the group becomes eligible mid‑run; the retry will apply it.
Your config already contains (abbreviated):
{
"ProtectedUsers": ["00000000-0000-0000-0000-000000000001"],
"PolicyTemplates": { "Standard": { /* PT2H activation etc. */ }, "HighSecurity": { /* approval + MFA */ } },
"EntraRoles": { "Policies": { "User Administrator": { "Template": "HighSecurity" }, "Guest Inviter": { "Template": "Standard" } } },
"AzureRoles": { "Policies": { "Reader": { /* inline minimal */ }, "Tag Contributor": { "Template": "Standard", "Scope": "/subscriptions/<sub>" } } },
"Assignments": { /* Entra + Azure role assignments already previewed earlier */ }
}We now introduce Group policies/assignments incrementally.
Flow in this step:
- 10.1 Minimal inline policy only (no assignments) — WhatIf with -SkipAssignments
- 10.2 Add assignments referencing that policy — WhatIf with -SkipPolicies
- 10.3 (Optional) Introduce a reusable template
Write pim-config.json (policy only + ProtectedUsers – kept FIRST for visibility)
{
"ProtectedUsers": ["00000000-0000-0000-0000-000000000001"],
"GroupRoles": {
"Policies": {
"MyPilotGroup": {
"Member": {
"ActivationDuration": "PT4H",
"ActivationRequirement": ["Justification"]
}
}
}
}
}Preview (policies only)
Invoke-EasyPIMOrchestrator -ConfigFilePath "C:\Config\pim-config.json" -TenantId "<tenant-guid>" -SubscriptionId "<sub-guid>" -WhatIf -SkipAssignmentsApply (after preview) — delta is the default change mode; no special flag needed for standard incremental runs:
Invoke-EasyPIMOrchestrator -ConfigFilePath "C:\Config\pim-config.json" -TenantId "<tenant-guid>" -SubscriptionId "<sub-guid>" -SkipAssignmentsInstead of a diff (harder to copy), here are clean before / after examples plus an appendable fragment.
Before (policies only):
{
"ProtectedUsers": ["00000000-0000-0000-0000-000000000001"],
"GroupRoles": {
"Policies": {
"MyPilotGroup": {
"Member": {
"ActivationDuration": "PT4H",
"ActivationRequirement": ["Justification"]
}
}
}
}
}After (assignments added – note comma before "Assignments"):
{
"ProtectedUsers": ["00000000-0000-0000-0000-000000000001"],
"GroupRoles": {
"Policies": {
"MyPilotGroup": {
"Member": {
"ActivationDuration": "PT4H",
"ActivationRequirement": ["Justification"]
}
}
}
},
"Assignments": {
"Groups": [
{
"groupId": "aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee",
"roleName": "Member",
"assignments": [
{
"principalId": "55555555-5555-5555-5555-555555555555",
"principalType": "User",
"assignmentType": "Eligible",
"justification": "Project team"
}
]
}
]
}
}Appendable fragment (paste just above the final closing brace of your existing JSON, ensuring the preceding block ends with a comma):
"Assignments": {
"Groups": [
{
"groupId": "aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee",
"roleName": "Member",
"assignments": [
{
"principalId": "55555555-5555-5555-5555-555555555555",
"principalType": "User",
"assignmentType": "Eligible",
"justification": "Project team"
}
]
}
]
}Preview (assignments only):
Invoke-EasyPIMOrchestrator -ConfigFilePath "C:\Config\pim-config.json" -TenantId "<tenant-guid>" -SubscriptionId "<sub-guid>" -WhatIf -SkipPoliciesApply assignments (after validation):
Invoke-EasyPIMOrchestrator -ConfigFilePath "C:\Config\pim-config.json" -TenantId "<tenant-guid>" -SubscriptionId "<sub-guid>" -SkipPolicies "PolicyTemplates": {
"Standard": { ... },
+ "GroupStandard": {
+ "ActivationDuration": "PT4H",
+ "ActivationRequirement": ["Justification"],
+ "ApprovalRequired": false
+ }
}
"GroupRoles": {
"Policies": {
"MyPilotGroup": {
- "Member": { "ActivationDuration": "PT4H", "ActivationRequirement": ["Justification"] }
+ "Member": { "Template": "GroupStandard" }
}
}
}Result: cleaner reuse; future tweaks centralized.
For groups that need most template properties but with specific customizations:
{
"GroupRoles": {
"Policies": {
"HighSecurityGroup": {
"Member": {
"Template": "GroupStandard", // Base template
"ActivationDuration": "PT2H", // Override: shorter than template's PT4H
"ApprovalRequired": true // Override: add approval requirement (you'll need to set approvers too)
},
"Owner": {
"Template": "GroupStandard", // Base template
"ActivationDuration": "PT1H", // Override: even shorter for owners
"MaximumEligibilityDuration": "P30D" // Override: shorter eligibility period
}
}
}
}
}This approach lets you maintain consistency with the template while customizing security requirements for sensitive groups.
NOTE: Deprecated formats (GroupPolicies array or nested Policies.Groups) still load with a warning; migrate to GroupRoles.Policies for forward compatibility.
Safety Note: By default the orchestrator operates in delta mode. That means it will create or update the assignments/policies you declare but it will not delete existing assignments that are absent from the config. Only new (or changed) items are acted on, so there is no risk of breaking existing access at this step. Destructive cleanup requires explicitly running Step 13 with
-Mode initial(and ideally a prior-WhatIf).
Apply policies
Invoke-EasyPIMOrchestrator -ConfigFilePath "C:\Config\pim-config.json" -TenantId "<tenant-guid>" -SubscriptionId "<sub-guid>" -SkipAssignmentsApply assignments
Invoke-EasyPIMOrchestrator -ConfigFilePath "C:\Config\pim-config.json" -TenantId "<tenant-guid>" -SubscriptionId "<sub-guid>" -SkipPoliciesCentralize the orchestrator configuration by storing the exact JSON in an Azure Key Vault secret.
- Create / select Key Vault (one-time):
az keyvault create -n <kv-name> -g <resource-group>- Upload JSON (plain text file):
az keyvault secret set --vault-name <kv-name> --name EasyPIM-Config --file C:\Config\pim-config.json- Preview assignments (skip policies already applied):
Invoke-EasyPIMOrchestrator -KeyVaultName <kv-name> -SecretName EasyPIM-Config -TenantId "<tenant-guid>" -SubscriptionId "<sub-guid>" -WhatIf -SkipPolicies- Apply:
Invoke-EasyPIMOrchestrator -KeyVaultName <kv-name> -SecretName EasyPIM-Config -TenantId "<tenant-guid>" -SubscriptionId "<sub-guid>" -SkipPoliciesNotes:
- Same schema as file; no transformation.
- Rotate by overwriting secret; callers just rerun.
- Keep size within Key Vault secret limits.
- CI: assign managed identity secret get permission.
Troubleshooting:
- Truncated/invalid: ensure plain UTF-8, no BOM, not base64.
- Access denied: verify RBAC/Access Policy includes get secret.
- Parse error:
az keyvault secret show --vault-name <kv-name> --name EasyPIM-Config --query value -o tsv | ConvertFrom-Json.
Pre-Execution Backup Recommended: Step 0 only backed up policies. Before running a destructive initial reconcile you should also snapshot CURRENT assignments so you can restore or justify removals. Export (or at least list to CSV) each assignment category:
- Entra role eligible:
Get-PIMEntraRoleEligibleAssignment -tenantID <tenant>- Entra role active:
Get-PIMEntraRoleActiveAssignment -tenantID <tenant>- Azure role eligible:
Get-PIMAzureResourceEligibleAssignment -tenantID <tenant> -subscriptionID <sub>- Azure role active:
Get-PIMAzureResourceActiveAssignment -tenantID <tenant> -subscriptionID <sub>- (If used) Group eligible:
Get-PIMGroupEligibleAssignment -tenantID <tenant>- (If used) Group active:
Get-PIMGroupActiveAssignment -tenantID <tenant>Example quick export (PowerShell):
# Module updated: Get-*Assignment cmdlets now emit clean objects (no leading count string), so you can pipe directly. Get-PIMEntraRoleEligibleAssignment -tenantID $tid | Export-Csv -Path C:\Logs\EntraEligible-BeforeInitial.csv -NoTypeInformation Get-PIMEntraRoleActiveAssignment -tenantID $tid | Export-Csv -Path C:\Logs\EntraActive-BeforeInitial.csv -NoTypeInformation Get-PIMAzureResourceEligibleAssignment -tenantID $tid -subscriptionID $sub | Export-Csv -Path C:\Logs\AzureEligible-BeforeInitial.csv -NoTypeInformationKeep these artifacts with the WouldRemove export for audit / rollback.
Use this mode ONLY when you intend to remove every assignment not explicitly declared (except ProtectedUsers). Always run a -WhatIf preview first.
When you run an initial (destructive) reconcile with -WhatIf, the orchestrator enumerates everything it would delete so you can validate safely before executing. Preview this FIRST so you know the scale of change before reading further.
Illustrative sample output (truncated):
───────────────────────────────────────────────────────────────────────────────┐
│ CLEANUP OPERATIONS
├───────────────────────────────────────────────────────────────────────────────┤
│ ✅ Kept : 4
│ 🗑️ Removed : 0
│ 🛈 WouldRemove: 10
│ - AcrPull /subscriptions/<sub-guid> f53bf02e-c703-40ab-b5cb-af0d546bc2c4
│ - Key Vault Secrets Officer /subscriptions/<sub-guid>/resourceGroups/RG-PIMTEST/providers/Microsoft.KeyVault/vaults/KVPIM 9f2aacfc-8c80-41a7-ba07-121e0cb29757
│ - Storage Blob Data Owner /subscriptions/<sub-guid>/resourceGroups/cloud-shell-storage-westeurope/providers/Microsoft.Storage/storageAccounts/devsample1 e54e29a4-5c6f-47a6-a5d7-7d555f77fb41
│ - Storage Blob Data Owner /subscriptions/<sub-guid>/resourceGroups/cloud-shell-storage-westeurope/providers/Microsoft.Storage/storageAccounts/devsample2 d2a829da-a0aa-4dab-9cee-a468285d101b
│ - Storage Queue Data Contributor /subscriptions/<sub-guid>/resourceGroups/cloud-shell-storage-westeurope/providers/Microsoft.Storage/storageAccounts/devsample1 e54e29a4-5c6f-47a6-a5d7-7d555f77fb41
│ ... (+5 more)
│ ⏭️ Skipped : 8
│ 🛡️ Protected: 10
└───────────────────────────────────────────────────────────────────────────────┘
Use the "Export the Full WouldRemove List" subsection below to capture the complete set for audit before proceeding.
Preview destructive reconcile:
Invoke-EasyPIMOrchestrator -ConfigFilePath "C:\Config\pim-config.json" -TenantId "<tenant-guid>" -SubscriptionId "<sub-guid>" -Mode initial -WhatIf -SkipPoliciesYou can export the complete set of preview removals for offline review or change‑control attachment using the new -WouldRemoveExportPath parameter.
Scenarios:
- Attach the JSON to a CAB / change ticket
- Diff two consecutive preview runs
- Manually whitelist unexpected principals before executing destructive mode
Usage (directory path – auto‑generates timestamped filename):
Invoke-EasyPIMOrchestrator -ConfigFilePath "C:\Config\pim-config.json" -TenantId "<tenant-guid>" -SubscriptionId "<sub-guid>" -Mode initial -WhatIf -WouldRemoveExportPath C:\Logs\PIMPreviewResult (example):
📤 Exported WouldRemove list (10 item(s)) to: C:\Logs\PIMPreview\EasyPIM-WouldRemove-20250811T134338.json
Usage (explicit file path – extension controls format):
Invoke-EasyPIMOrchestrator -ConfigFilePath "C:\Config\pim-config.json" -TenantId "<tenant-guid>" -SubscriptionId "<sub-guid>" -Mode initial -WhatIf -WouldRemoveExportPath C:\Logs\preview.csv- If the path ends with
.csva CSV is produced; any other (or no) extension defaults to JSON. - File is written even under
-WhatIf(safe preview) so you always have an artifact. - An empty export (
[]or headers only) means no deletions are projected.
Sample JSON entry:
{
"PrincipalId": "f53bf02e-c703-40ab-b5cb-af0d546bc2c4",
"PrincipalName": "Adam Warlock",
"RoleName": "AcrPull",
"Scope": "/subscriptions/<sub-guid>",
"ResourceType": "Azure Role eligible",
"Mode": "initial-preview"
}Recommended review checklist before executing destructive apply:
- Confirm every removal candidate is truly unintended or should be purged.
- Verify no break‑glass / emergency accounts appear (if so add them to
ProtectedUsers). - Re‑run preview until the export list matches expected deltas.
- (Optional) Commit the export file to a secure audit repository.
Then proceed without -WhatIf when satisfied.
Legend / interpretation:
- Kept – Assignments declared in config (no action needed)
- Removed – Assignments actually removed in a non-
-WhatIfdestructive run (always 0 during preview) - WouldRemove – Assignments NOT in config that would be deleted if you re-run without
-WhatIf- The list shows the first few (role name, scope, principal objectId). Full list retained in memory.
- Skipped – Items intentionally ignored (e.g., unsupported type, already compliant, or safety exclusions)
- Protected – Assignments whose principals are in
ProtectedUsers(never removed)
Checklist before removing -WhatIf:
- Review every WouldRemove entry – confirm each is genuinely obsolete.
- Add any missing but still required assignments to the config (they will then move from WouldRemove → Kept on the next preview).
- Ensure all break‑glass / critical accounts are in
ProtectedUsers(they'll appear under Protected, not WouldRemove). - (Optional) Capture this preview output for audit/change record.
- Re-run the same command once more with
-WhatIfto confirm no unexpected drift just before execution.
Then execute using the destructive command (without -WhatIf) only after you are satisfied.
Delta mode note: In
deltamode nothing is deleted; such items would instead surface asWouldRemove (delta)to keep you aware of potential cleanup candidates without any risk.
-
All assignments NOT declared in your config will be REMOVED (except principals listed under
ProtectedUsers). - Verify
ProtectedUsersincludes every break‑glass / critical account before proceeding. - Review prior delta runs: investigate any
WouldRemove (delta)items you are not expecting.
Best practice: Run at least once with -WhatIf, capture the summary for audit, and (optionally) perform a fresh backup (top of Step 13) immediately before executing the destructive apply.
Execute (destructive) ONLY after validating preview:
Invoke-EasyPIMOrchestrator -ConfigFilePath "C:\Config\pim-config.json" -TenantId "<tenant-guid>" -SubscriptionId "<sub-guid>" -Mode initial -SkipPoliciesThis step validates that every major policy lever is understood and renders correctly: activation & eligibility durations, active vs eligible enablement rules, authentication context, approvers, permanent eligibility flags, and the full three‑phase notification matrix (Eligibility, Active, Activation). It also introduces a reusable template that captures all options.
Templates provide consistency and reduce repetition. Define common security patterns once, then inherit and override specific properties as needed. This pattern works across all policy types (EntraRoles, AzureRoles, GroupRoles).
How It Works:
- Template Properties: All properties from the referenced template are inherited
-
Override Properties: Any property specified alongside
"Template"overrides the template value -
Scope Preservation: For Azure roles,
Scopemust always be specified (not inherited from templates) -
Logical Consistency: When
ApprovalRequired: true, you MUST provideApproversarray
Benefits:
- Consistency: Ensure common security patterns across roles
- Flexibility: Customize individual roles without duplicating configurations
- Maintainability: Update templates to change multiple role policies at once
- Clarity: Explicit overrides make policy differences obvious
Key fields you can configure (availability varies by resource type):
-
Duration Control:
ActivationDuration,MaximumActiveAssignmentDuration,MaximumEligibilityDuration -
Security Requirements:
ActivationRequirement,ActiveAssignmentRequirement(enablement rules) -
Approval Control:
ApprovalRequired+Approvers(array of objects with id + optional description) -
Permanency Control:
AllowPermanentEligibility,AllowPermanentActiveAssignment -
Conditional Access:
AuthenticationContext_Enabled+AuthenticationContext_Value -
Communication:
Notifications(Eligibility / Active / Activation phases with Alert / Assignee / Approvers blocks)
{
"ProtectedUsers": ["00000000-0000-0000-0000-000000000001"],
"PolicyTemplates": {
"StandardSecurity": {
"ActivationDuration": "PT8H",
"ActivationRequirement": "MultiFactorAuthentication,Justification",
"ApprovalRequired": false,
"MaximumEligibilityDuration": "P365D",
"AllowPermanentEligibility": false
},
"HighSecurity": {
"ActivationDuration": "PT4H",
"ActivationRequirement": "MultiFactorAuthentication,Justification,Ticketing",
"ApprovalRequired": true,
"Approvers": [
{ "id": "aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee", "description": "Security Admin 1" },
{ "id": "ffffffff-1111-2222-3333-444444444444", "description": "Security Team Group" }
],
"MaximumEligibilityDuration": "P90D",
"AllowPermanentEligibility": false,
"AuthenticationContext_Enabled": true,
"AuthenticationContext_Value": "c1:HighRiskOperations"
},
"AllOptions": {
"ActivationDuration": "PT2H",
"ActivationRequirement": "MultiFactorAuthentication,Justification,Ticketing",
"ActiveAssignmentRequirement": "MultiFactorAuthentication,Justification",
"ApprovalRequired": true,
"Approvers": [
{ "id": "aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee", "description": "PIM Approver 1" },
{ "id": "ffffffff-1111-2222-3333-444444444444", "description": "Approver Group" }
],
"AllowPermanentEligibility": false,
"AllowPermanentActiveAssignment": false,
"MaximumEligibilityDuration": "P180D",
"MaximumActiveAssignmentDuration": "P30D",
"AuthenticationContext_Enabled": true,
"AuthenticationContext_Value": "c1:HighRiskOperations",
"Notifications": {
"Eligibility": {
"Alert": { "isDefaultRecipientEnabled": true, "NotificationLevel": "All", "Recipients": ["pim-alerts@contoso.com"] },
"Assignee": { "isDefaultRecipientEnabled": true, "NotificationLevel": "All", "Recipients": ["pim-assignees@contoso.com"] },
"Approvers": { "isDefaultRecipientEnabled": true, "NotificationLevel": "All", "Recipients": ["pim-approvers@contoso.com"] }
},
"Active": {
"Alert": { "isDefaultRecipientEnabled": true, "NotificationLevel": "All", "Recipients": ["pim-alerts@contoso.com"] },
"Assignee": { "isDefaultRecipientEnabled": true, "NotificationLevel": "All", "Recipients": ["pim-assignees@contoso.com"] },
"Approvers": { "isDefaultRecipientEnabled": true, "NotificationLevel": "All", "Recipients": ["pim-approvers@contoso.com"] }
},
"Activation": {
"Alert": { "isDefaultRecipientEnabled": true, "NotificationLevel": "All", "Recipients": ["pim-alerts@contoso.com"] },
"Assignee": { "isDefaultRecipientEnabled": true, "NotificationLevel": "All", "Recipients": ["pim-assignees@contoso.com"] },
"Approvers": { "isDefaultRecipientEnabled": true, "NotificationLevel": "All", "Recipients": ["pim-approvers@contoso.com"] }
}
}
}
},
// Template inheritance examples across all policy types
"EntraRoles": {
"Policies": {
// Template only - uses all StandardSecurity settings
"User Administrator": {
"Template": "StandardSecurity"
},
// Template + override - inherits StandardSecurity but with custom duration
"Exchange Administrator": {
"Template": "StandardSecurity",
"MaximumEligibilityDuration": "P180D"
},
// Template + approval override - adds approval to StandardSecurity template
"Security Administrator": {
"Template": "StandardSecurity",
"ApprovalRequired": true,
"Approvers": [
{ "id": "security-team@contoso.com", "description": "Security Team" }
]
},
// High security template for critical roles
"Global Administrator": {
"Template": "HighSecurity",
"MaximumEligibilityDuration": "P30D" // Even shorter for Global Admin
}
}
},
"AzureRoles": {
"Policies": {
// Template + scope override (Scope is ALWAYS required for Azure roles)
"Contributor": {
"Template": "StandardSecurity",
"Scope": "/subscriptions/<sub-guid>",
"ActivationDuration": "PT12H" // Override for longer Azure access
},
// High security for critical Azure roles
"Owner": {
"Template": "HighSecurity",
"Scope": "/subscriptions/<sub-guid>",
"MaximumEligibilityDuration": "P60D" // Even shorter for Owner role
},
// Resource group specific override
"Storage Account Contributor": {
"Template": "StandardSecurity",
"Scope": "/subscriptions/<sub-guid>/resourceGroups/rg-production",
"ApprovalRequired": true,
"Approvers": [
{ "id": "storage-admins@contoso.com", "description": "Storage Team" }
]
}
}
},
"GroupRoles": {
"Policies": {
// Template inheritance works for group roles too
"f47ac10b-58cc-4372-a567-0e02b2c3d479": {
"Owner": {
"Template": "StandardSecurity",
"ActivationRequirement": "Justification" // Remove MFA for group ownership
},
"Member": {
"Template": "StandardSecurity",
"ApprovalRequired": true,
"Approvers": [
{ "id": "group-managers@contoso.com", "description": "Group Managers" }
]
}
}
}
}
}Preview template + override policies:
Invoke-EasyPIMOrchestrator -ConfigFilePath "C:\Config\pim-config.json" -TenantId "<tenant-guid>" -SubscriptionId "<sub-guid>" -WhatIf -SkipAssignmentsFor cases where templates don't fit your needs, you can define policies inline. Use this approach sparingly to avoid configuration drift.
Entra Role Example (inline):
{
"ProtectedUsers": ["00000000-0000-0000-0000-000000000001"],
"EntraRoles": {
"Policies": {
"User Administrator": {
"ActivationDuration": "PT2H",
"ActivationRequirement": "MultiFactorAuthentication,Justification",
"ApprovalRequired": true,
"Approvers": [
{ "id": "aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee", "description": "Security Team Lead" }
],
"MaximumEligibilityDuration": "P180D"
}
}
}
}Azure Role Example (inline):
{
"AzureRoles": {
"Policies": {
"Contributor": {
"Scope": "/subscriptions/<sub-guid>",
"ActivationDuration": "PT4H",
"ActivationRequirement": "MultiFactorAuthentication,Justification",
"ApprovalRequired": true,
"Approvers": [
{ "id": "azure-admins@contoso.com", "description": "Azure Administrators" }
],
"MaximumEligibilityDuration": "P180D"
}
}
}
}Group Role Example (inline):
{
"GroupRoles": {
"Policies": {
"<group-object-id>": {
"Member": {
"ActivationDuration": "PT4H",
"ActivationRequirement": ["Justification"],
"ApprovalRequired": true,
"Approvers": [
{ "id": "group-owners@contoso.com", "description": "Group Owners" }
],
"MaximumEligibilityDuration": "P180D"
}
}
}
}
}
⚠️ Important: WhenApprovalRequired: true, you MUST include theApproversarray. The system will reject policies that require approval without specifying who can approve.
For comprehensive policy configurations that use every available feature, reference the AllOptions template shown in section 12.1. This template demonstrates all available policy fields including:
- Duration Controls: Activation, eligibility, and active assignment time limits
- Security Requirements: MFA, justification, ticketing for both activation and permanent assignment
- Approval Workflows: Approval requirements with proper approver configurations
- Conditional Access: Authentication context integration for high-risk operations
- Communication: Full notification matrix for all assignment lifecycle phases
- Permanency Controls: Flags to control permanent eligibility and active assignments
Critical Validation Rules:
-
ApprovalRequired: trueMUST includeApproversarray with at least one approver - Azure role policies MUST include
Scope(cannot be inherited from templates) -
AuthenticationContext_*settings are ignored for Group policies (use shared templates safely) -
ActivationRequirementandActiveAssignmentRequirementvalues are case-sensitive, comma-separated
Example AllOptions Template Usage:
{
"PolicyTemplates": {
"AllOptions": { /* see section 12.1 for full definition */ }
},
"EntraRoles": {
"Policies": {
"Global Administrator": {
"Template": "AllOptions",
"MaximumEligibilityDuration": "P30D" // Override for even stricter Global Admin
}
}
}
}- ActivationRequirement & ActiveAssignmentRequirement values are case‑sensitive and comma separated (avoid spaces unless inside list items array form).
- Approvers only used when ApprovalRequired = true.
- AuthenticationContext_* (if enabled) requires the referenced auth context to exist.
- AuthenticationContext_* is ignored for Group policies; you can keep it in shared templates, but it won’t be applied to Groups.
- Use Verify-PIMPolicies.ps1 or Test-PIMPolicyDrift to audit drift after applying.
- Keep templates minimal; AllOptions is illustrative — real production templates often exclude rarely used features.
Goal: Verify that live policies match your declared configuration and catch out-of-band changes. Run this after Step 12 and after any apply to ensure compliance.
Note: Test-PIMPolicyDrift is provided by the EasyPIM.Orchestrator module.
What this does:
- Compares effective Entra/Azure role policies to your JSON-defined expectations
- Highlights differences per rule (enablement, durations, notifications, approvals, authentication context)
- Works in non-destructive mode; ideal for scheduled audits
Minimal usage (file config):
Test-PIMPolicyDrift -ConfigFilePath "C:\Config\pim-config.json" -TenantId $env:TENANTID -SubscriptionId $env:SUBSCRIPTIONIDKey Vault usage:
Test-PIMPolicyDrift -KeyVaultName MyPIMVault -SecretName PIMConfig -TenantId $env:TENANTID -SubscriptionId $env:SUBSCRIPTIONIDOptions and tips:
- Use -PolicyOperations to limit domains (e.g., EntraRoles only)
- Use -OutputPath C:\Logs to export a timestamped JSON/CSV report for review
- Pair with your pipeline to fail builds on detected drift
- If Authentication Context is enabled, expect MFA to be absent from EndUser enablement by design
Next: If drift is found, re-run Step 2/3/6/7 policy previews with -WhatIf to confirm the intended state, then apply.
EasyPIM CI/CD and pipeline setup is now maintained in a dedicated repository:
👉 Please use EasyPIM-CICD-test for all GitHub Actions, Key Vault integration, and automated orchestration setup.
This repo contains:
- End-to-end pipeline examples
- OIDC setup and federated credential instructions
- Permission matrix and safety patterns
- Promotion and drift gate workflows
- Observability and failure handling best practices
All future updates, bugfixes, and advanced patterns will be published there. This guide will only reference the external repo for CI/CD automation.
For details, see: https://github.com/kayasax/EasyPIM-CICD-test
The orchestrator now ALWAYS validates all referenced principals (users, groups, service principals) and role‑assignable status for groups before any changes. If issues are detected it aborts before policies or assignments are processed.
Benefits:
- Prevents misleading "Created" outputs caused by placeholder GUIDs.
- Catches non role‑assignable groups before assignment attempts.
- Produces a concise invalid principal summary without touching assignments or policies.
If validation fails:
- Replace placeholder or removed object IDs with real ones.
- (Optional) For groups: if you plan classic privileged directory role use outside this orchestrator, you may still choose to set them role-assignable; it's not required for orchestrator processing.
- Remove or comment obsolete principals, then re-run.
If you genuinely need to ignore a transient missing ID, temporarily comment it out (future enhancement may add an override switch).
- ActivationRequirement values are case-sensitive:
None,MultiFactorAuthentication,Justification,Ticketing(combine with commas) - Azure policies require
Scope; Entra policies are directory-wide - AU-scoped Entra cleanup isn’t automatic; remove manually if needed
- Keep break-glass accounts in
ProtectedUsersat all times
- See
EasyPIM-Orchestrator-Complete-Tutorial.mdfor end-to-end context - See
Configuration-Schema.mdfor the full schema and field definitions
{ // Object IDs for which assignments will not be removed "ProtectedUsers": [ "00000000-0000-0000-0000-000000000001" // Example: Breakglass account ], "EntraRoles": { "Policies": { "User Administrator": { "ActivationDuration": "PT2H", "ActivationRequirement": "MultiFactorAuthentication,Justification", "ApprovalRequired": true, "Approvers": [ { "id": "aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee", "description": "PIM Approver 1" } ] } } } }