Managing Conditional Access (CA) in a large enterprise tenant has historically been fraught with peril. What begins as a clean set of baseline access rules invariably turns into policy sprawl: dozens of overlapping policies, conflicting session controls, emergency exception group clutter, and the constant dread of inadvertently locking out the entire IT organization during a routine policy edit.
Microsoft's introduction of Conditional Access Policy Plans in Entra ID (previewed in late 2026 for Entra ID P1/P2 tenants) directly solves this structural flaw. Rather than managing 30 to 50 fragmented individual policies, administrators can organize their zero-trust enforcement rules into unified, lifecycle-managed Policy Plans.
In this guide, we break down how Policy Plans work, how to establish governance ownership, how to simulate aggregate impact in Report-Only mode without blind spots, and how to automate the entire lifecycle using the Microsoft Graph PowerShell SDK v2.
The Enterprise Challenge: Policy Sprawl vs. Zero Trust
In traditional Conditional Access management, individual policies operate independently. When an administrator creates a new MFA rule or restricts external collaborative tenants, evaluating the aggregate risk requires manual cross-referencing:
- Colliding Grant Controls: Policy A requires phishing-resistant FIDO2 passkeys for administrators, while Policy B requires compliant devices, and Policy C blocks legacy authentication. If conditions overlap unexpectedly, users face conflicting prompts or inexplicable access denials.
- Orphaned Exception Groups: Emergency "Break-Glass" accounts and service principals end up hardcoded across dozens of policies, leading to incomplete exclusions and compliance audit failures.
- High-Risk Rollouts: Testing a single policy in "Report-Only" mode fails to reveal how it interacts with other already-enforced policies.
Architecture of a Conditional Access Policy Plan
A Policy Plan functions as an overarching governance wrapper. Instead of treating each policy as an isolated JSON payload, the plan introduces:
- Assigned Governance Ownership: A specific identity (User, Service Principal, or Security Group) is designated as the plan owner. Unauthorized modifications to policies within the plan trigger immediate governance audit alerts.
- Unified Scope & Personas: Policies are grouped by identity tier:
- Plan-Workforce-Baseline: All corporate employees accessing cloud apps.
- Plan-Privileged-Access: Global Admins, Security Admins, and sensitive roles requiring PIM and FIDO2.
- Plan-External-Collaboration: B2B guests, cross-tenant synchronization, and shared channels.
- Plan-Workload-Identities: Service principals, managed identities, and OAuth apps.
- Plan-Level Global Exceptions: Break-glass accounts and critical directory synchronization services can be defined once at the plan level, automatically shielding every policy within the group from inadvertent lockout configurations.
Step-by-Step: Managing Policy Plans via Microsoft Graph PowerShell
To automate policy plans across dev, test, and production tenants, we utilize the Microsoft.Graph.Identity.SignIns PowerShell module.
Prerequisites & Least-Privilege Permissions
- PowerShell Version: PowerShell 7.2+ recommended (PowerShell 5.1 fully supported).
- Microsoft Graph App-Only Permissions:
- Policy.ReadWrite.ConditionalAccess
- Policy.Read.All
- Directory.Read.All
- Role Requirement: Conditional Access Administrator or Global Administrator.
1. Connecting via Certificate Authentication
Never use interactive credentials or client secrets in automation runbooks. Authenticate using your enterprise certificate:
# Authenticate to Microsoft Graph via App-Only Certificate
$TenantId = "ab85c7a1-366d-44ba-acc6-924fa0950045"
$ClientId = "c841ae33-4093-4385-8a90-6a530761f9b9"
$CertThumbprint = "BEAC429DE404B46FF030811C0C9DE00DE701C027"
Connect-MgGraph -TenantId $TenantId -ClientId $ClientId -CertificateThumbprint $CertThumbprint -NoWelcome
2. Auditing Existing Policies for Plan Categorization
Before assembling plans, run an inventory script to detect unassigned policies, conflicting states, and legacy basic auth gaps:
# Audit all Conditional Access policies and group by operational state
$Policies = Get-MgIdentityConditionalAccessPolicy -All
$AuditReport = foreach ($p in $Policies) {
[PSCustomObject]@{
DisplayName = $p.DisplayName
State = $p.State
IncludeUsers = $p.Conditions.Users.IncludeUsers -join "; "
ExcludeUsers = $p.Conditions.Users.ExcludeUsers -join "; "
IncludeApps = $p.Conditions.Applications.IncludeApplications -join "; "
GrantControls = $p.GrantControls.BuiltInControls -join "; "
ModifiedDate = $p.ModifiedDateTime
}
}
$AuditReport | Format-Table -AutoSize
3. Creating a Staged Policy Plan Definition
The following script registers a new policy plan, assigns a designated security owner, and links our core baseline policies:
# Script: New-EntraCAPolicyPlan.ps1
# Description: Provisions a unified Conditional Access Policy Plan for Privileged Tier identities
$PlanDefinition = @{
displayName = "Plan-Privileged-Tier-Security"
description = "Enforces Phishing-Resistant MFA, Compliant Workstations, and Session Throttling for Admin Roles"
ownerId = "c142386f-63d7-4b1c-b2b5-ec5e9c02ab43" # SecOps Governance Group / SPN
stagingState = "ReportOnly"
tags = @("Tier0", "PrivilegedIdentity", "ZeroTrustBaseline")
# Universal Break-Glass Exclusion (Shields all child policies)
globalExclusions = @{
users = @("[email protected]", "[email protected]")
roles = @("Company Administrator") # Evaluated against emergency accounts
}
}
Write-Host "Creating Entra ID Conditional Access Policy Plan: $($PlanDefinition.displayName)..." -ForegroundColor Cyan
# Invoke the Graph Identity Governance endpoint
$PlanJson = $PlanDefinition | ConvertTo-Json -Depth 5
$NewPlan = Invoke-MgGraphRequest -Method POST -Uri "https://graph.microsoft.com/beta/identity/conditionalAccess/policyPlans" -Body $PlanJson
Write-Host "[+] Policy Plan Created Successfully! ID: $($NewPlan.id)" -ForegroundColor Green
4. Running a Batch "What-If" Diagnostic Before Enforcement
The true power of policy plans lies in aggregate simulation. Rather than evaluating a single policy in the What-If GUI, we test a user authentication against the entire policy plan:
# Simulate aggregate impact of Policy Plan against a target admin user
$SimParams = @{
userId = "[email protected]"
userPrincipalName = "[email protected]"
targetAppId = "00000002-0000-0ff1-ce00-000000000000" # Exchange Online
ipAddress = "203.0.113.45" # Untrusted external IP
devicePlatform = "windows"
clientAppType = "browser"
}
$SimulationUri = "https://graph.microsoft.com/beta/identity/conditionalAccess/policyPlans/$($NewPlan.id)/simulate"
$SimResult = Invoke-MgGraphRequest -Method POST -Uri $SimulationUri -Body ($SimParams | ConvertTo-Json)
# Output evaluation findings
Write-Host "`n=== SIMULATION RESULTS FOR $($SimParams.userPrincipalName) ===" -ForegroundColor Yellow
foreach ($rule in $SimResult.evaluatedPolicies) {
Write-Host "Policy: $($rule.policyName)"
Write-Host " - Result: $($rule.result)" -ForegroundColor ($rule.result -eq "Grant" ? "Green" : "Red")
Write-Host " - Enforced Controls: $($rule.enforcedControls -join ', ')"
}
Safe Staged Rollout Strategy (Canary to Production)
Never switch an entire policy plan from ReportOnly to Enabled in one sweep. Follow this battle-tested 4-phase promotion lifecycle:
| Phase | Duration | Scope | Monitoring Focus |
|---|---|---|---|
| Phase 1: Silent Baseline | 14 Days | 100% of Target Users (ReportOnly) | Review Entra ID Sign-In Logs (ResultType eq 53003). Identify valid business workflows that would have failed. |
| Phase 2: Canary Pilot | 7 Days | 5% (IT Security & Volunteer Admins) (Enabled) | Hardware token compatibility, biometric fallback, fast-track support tickets. |
| Phase 3: Cohort Expansion | 14 Days | 50% Departmental Waves (Enabled) | Monitor hybrid device compliance sync and external collaboration partner impact. |
| Phase 4: Full Enforcement | Standing | 100% Target Users (Enabled) | Automated alert on any policy plan exclusion bypass or emergency role activation. |
Promoting the Plan via PowerShell
Once telemetry confirms zero unintended lockouts during the Report-Only window:
# Transition the Policy Plan to full production enforcement
$UpdatePayload = @{
stagingState = "Enabled"
changeTicket = "CHG-2026-99481"
} | ConvertTo-Json
Invoke-MgGraphRequest -Method PATCH `
-Uri "https://graph.microsoft.com/beta/identity/conditionalAccess/policyPlans/$($NewPlan.id)" `
-Body $UpdatePayload
Write-Host "[+] Policy Plan promoted to Enabled state across tenant." -ForegroundColor Green
Best Practices & Hard-Earned Lessons
- Mandatory Break-Glass Exclusions: Always configure at least two cloud-only emergency access accounts (
Global Administrator) with long, random passphrases stored in separate physical safes and exempt from all CA policies. - Never Mix User Persona Tiers: Keep frontline workers, standard enterprise knowledge workers, privileged administrators, and workload identities in separate plans.
- Monitor with Log Analytics (KQL): Stream Entra ID Sign-in logs to Azure Log Analytics or Microsoft Sentinel. Set up alert queries for
ConditionalAccessStatus == "failure"on high-volume production service accounts. - Automate Drift Detection: Schedule a nightly GitHub Action or Azure Automation runbook using
Get-MgIdentityConditionalAccessPolicyto detect if an administrator manually modified an underlying policy outside of the approved Policy Plan repository.
Summary
Conditional Access Policy Plans bring enterprise-grade software development lifecycle (SDLC) disciplines to zero-trust identity management. By scoping rules by persona, enforcing global exclusions, running aggregate batch What-If simulations, and staging promotions through code, organizations can secure their Microsoft 365 environments without risking operational downtime.