In this section

EI3.12 Conditional Access Architecture as Code

90-120 minutes · Module 3
What you already know

The previous section built on the conditional access architecture. This section continues that progression.

CA EVALUATION Sign-in Evaluate User Check Device Check Location Grant/Block

Figure EI3.12. Conditional Access policy evaluation at sign-in.

Why conditional access needs version control

Conditional access policies are modified through the Entra admin center GUI: a point-and-click interface with no built-in version history, no approval workflow (unless you build one), and no rollback capability (until the soft delete preview). A single administrator changing one exclusion in one policy can silently create a gap that affects the entire tenant.

Consider: an administrator adds the "IT Department" group to the exclusion list of the MFA policy because IT staff are complaining about MFA prompts during a software deployment. The deployment ends, but nobody removes the exclusion. The IT Department, including the Exchange Administrator, the SharePoint Administrator, and other privileged accounts, is now permanently excluded from MFA. An AiTM attack against an IT staff member's account succeeds because MFA is not required.

Version control prevents this by maintaining a record of every policy state, enabling comparison between current and previous versions, and making changes reviewable and reversible.

Exporting policies with Microsoft Graph

The Microsoft Graph API provides full read and write access to conditional access policies. The export process uses the GET /identity/conditionalAccess/policies endpoint to retrieve all policies as JSON objects.

PowerShell export workflow:

The export requires the Microsoft Graph PowerShell module with the Policy.Read.All permission scope. The workflow connects to Graph, retrieves all conditional access policies, and saves each one as a JSON file:

Step 1: Connect to Microsoft Graph with Connect-MgGraph -Scopes "Policy.Read.All". This authenticates the session with the minimum required permission.

Step 2: Retrieve all policies with $policies = Get-MgIdentityConditionalAccessPolicy -All. This returns every conditional access policy in the tenant, including report-only and disabled policies.

Step 3: Export each policy to a JSON file. The file naming convention should include the policy display name (sanitized for file system compatibility) and the export date: "F1-Require-MFA-All-Users_2026-03-28.json". Saving each policy as a separate file makes Git diffs meaningful, you can see which specific policy changed between exports.

Step 4: Commit to Git with a descriptive message: "Weekly CA policy export — [date]. [N] policies exported." Push to the repository.

The entire export can be automated as a scheduled task (Windows Task Scheduler, Azure Automation, or a Logic App) that runs weekly. The automation script connects with a managed identity or service principal, exports the policies, commits to Git, and sends a notification if the export differs from the previous week's export.

The import/restore workflow:

If a policy needs to be restored from backup, the process is: read the JSON file from the Git repository, use New-MgIdentityConditionalAccessPolicy with the JSON body to create the policy, and verify the restored policy is evaluating correctly with the EI1.5 queries. Note that recreating a policy through the API assigns a new policy ID, if other systems reference the old policy ID, they need to be updated.

For modification rollback (restoring the previous version of a modified policy rather than recreating a deleted one), the process is: identify the previous version's JSON from the Git history, compare it with the current version to identify the differences, and update the current policy to match the previous version using Update-MgIdentityConditionalAccessPolicy.

The policy JSON contains every configuration element: display name, state (enabled, disabled, report-only), conditions (users, applications, locations, platforms, client apps, risk levels), grant controls, session controls, and metadata. This JSON is the complete, reproducible definition of the policy, if you have the JSON, you can recreate the exact policy.

// EI3.12 — Monitor conditional access policy changes in the audit log
// Deploy as a Sentinel analytics rule for real-time change detection
AuditLogs
| where TimeGenerated > ago(1h)
| where OperationName has "conditional access policy"
| extend Actor = tostring(InitiatedBy.user.userPrincipalName)
| extend PolicyName = tostring(TargetResources[0].displayName)
| extend ModifiedProperties = TargetResources[0].modifiedProperties
| project 
    TimeGenerated,
    Operation = OperationName,
    Actor,
    PolicyName,
    ModifiedProperties
| order by TimeGenerated desc
// Operations to watch:
// "Add conditional access policy" — new policy created
// "Update conditional access policy" — existing policy modified
// "Delete conditional access policy" — policy deleted
// Every change should correspond to a documented change request
// Unexpected changes = investigate immediately (potential attacker persistence)

The change management process

For production environments, conditional access changes should follow a formal process:

Propose: The security engineer writes the policy change specification, which policy, what change, why, and the expected impact. The specification includes the verification queries that will confirm the change works correctly.

Test in report-only: The change is implemented in a report-only policy (or an existing policy is duplicated as report-only with the change applied). The report-only policy evaluates for 48-72 hours. The results are analyzed using the EI1.5 queries: how many sign-ins would be affected, who would be blocked, who would pass.

Approve: A second administrator reviews the report-only results and the change specification. For changes that affect privileged users or block access, the CISO or security lead approves.

Enforce: The change is applied to the production policy. The report-only duplicate is deleted or disabled.

Verify: The EI1.5 verification queries confirm the policy is enforcing correctly in production. The coverage query confirms no new gaps were introduced.

Document: The change is recorded in the version control system (Git commit with the updated policy JSON) and in the change log.

Policy naming conventions

Consistent policy naming is essential for a maintainable architecture. When you have 15-20 policies, descriptive names prevent confusion:

The convention used in the Zero Trust framework: [Category]-[Number]: [Brief description]. Examples: "F1: Require MFA for All Users," "P1: Phishing-Resistant MFA for Privileged Roles," "R2: Block High Sign-In Risk." The category prefix (F for foundation, P for privileged, R for risk-based, BYOD, SHARED, etc.) groups related policies visually in the portal. The number provides a stable reference that does not change even if the description is updated.

Some organizations prefix with the enforcement state: "ENFORCING-F1: Require MFA" vs "REPORT-ONLY-F1: Require MFA." This makes the state immediately visible in the policy list without opening each policy. The downside is that the name must be updated when the state changes.

Avoid vague names like "MFA Policy" or "Security Policy 2", these do not convey the policy's scope, target, or purpose. A policy named "P2: Compliant Device for Privileged Roles" immediately communicates who it targets (privileged roles), what it requires (compliant device), and its priority tier (P = privileged).

Automated drift detection

The most operationally valuable automation for conditional access is automated drift detection: a scheduled process that exports the current policy state, compares it to the previous export, and alerts on any differences.

The drift detection workflow runs weekly: export all policies via Graph API, compare the export against the previous week's export (Git diff), and if any differences are found, generate a report showing which policies changed, which fields changed, and who made the change (from the audit log). Send the report to the security team for review.

The diff comparison should focus on the security-critical fields: conditions (user assignments, application targets, locations, platforms), grant controls (MFA type, device requirements), session controls (frequency, token protection), and state (enabled/disabled/report-only). Metadata changes (policy ID, creation date) can be ignored.

If the diff shows a change that does not correspond to a documented change request, investigate immediately. An undocumented conditional access change is a potential indicator of attacker persistence: an attacker with admin access who weakens a policy to create a gap for their continued access (EI0.6 Stage 4 of the identity kill chain).

Soft delete and restore (preview 2026)

Microsoft introduced conditional access policy soft delete and restore in preview during 2026. When a policy is deleted, it is moved to a "recently deleted" state for a configurable retention period (default 30 days) rather than being permanently removed. During the retention period, the policy can be restored with its full configuration.

Soft delete mitigates the risk of accidental policy deletion: a common incident that previously required manual recreation of the policy from memory or documentation. With soft delete, the policy can be restored in seconds.

However, soft delete does not replace version-controlled backups. Soft delete only covers deletion, it does not maintain a history of modifications. If a policy is modified incorrectly (an exclusion added, a condition changed, a grant control removed), soft delete does not help because the policy was not deleted, it was changed. Only a version-controlled export history shows the before-and-after of policy modifications.

Auditing policy drift

Policy drift occurs when the actual policy configuration diverges from the intended design over time, through incremental changes, temporary exceptions that become permanent, or undocumented modifications. The audit log monitoring query above detects changes as they happen. The version-controlled export detects drift when the current export is compared against the previous version.

The recommended operational cadence: export all policies weekly (automated). Compare each export against the previous version (a Git diff shows exactly what changed). Review any differences. If the difference corresponds to a documented change, acknowledge it. If the difference is undocumented, investigate, it may be unauthorized modification.

// EI3.12 — Detect conditional access policies in report-only mode
// These may be policies stuck in testing that should have been promoted
AuditLogs
| where TimeGenerated > ago(90d)
| where OperationName == "Update conditional access policy"
| extend PolicyName = tostring(TargetResources[0].displayName)
| extend ModifiedProperties = TargetResources[0].modifiedProperties
| where tostring(ModifiedProperties) has "enabledForReportingButNotEnforced"
| summarize LastChange = max(TimeGenerated) by PolicyName
| where LastChange < ago(30d)
// Policies set to report-only more than 30 days ago
// These should either be promoted to enforcing or disabled
// Stale report-only policies create the illusion of protection without enforcement
Beyond this module
EI8 (Conditional Access Validation and Troubleshooting) covers the full report-only testing methodology, the What-If simulation tool, and systematic troubleshooting for policy misconfigurations. EI13 (Detection Engineering) converts the policy change monitoring query into a production Sentinel analytics rule. EI15 (Backup, Recovery, and Resilience) covers the complete disaster recovery procedure for conditional access, including policy restoration from backup after a tenant compromise.

Environment: Your M365 developer tenant.

Exercise: If you have the Microsoft Graph PowerShell module installed, run the following to export your conditional access policies:

Connect-MgGraph -Scopes "Policy.Read.All" $policies = Get-MgIdentityConditionalAccessPolicy $policies | ConvertTo-Json -Depth 10 | Out-File "ca-policies-export.json"

Review the JSON output. Each policy includes its complete configuration, assignments, conditions, grant controls, session controls, and state. This is the policy-as-code representation that you would store in a Git repository.

If you do not have the PowerShell module, navigate to the Entra admin center → Identity → Protect & secure → Conditional Access → Policies. Click each policy and manually review its configuration. Note how the portal does not provide a single view of all policies or an export function, this is why the Graph API export is necessary for policy management at scale.

Compliance Context

Conditional access policies are the enforcement mechanism for your identity security architecture. A single incorrect change to a single policy can disable MFA for all users, open access from blocked countries, or remove the phishing-resistant requirement for administrators. These changes are not reversible through the portal without a backup. They may not be noticed until an audit or, worse, until an attacker exploits the gap. Version-controlled exports provide a complete change history, diff capability, and restoration path. The Graph API export is JSON, it is literally code. Managing it like code (version control, change review, automated backup) is the correct operational approach. Organizations that manage conditional access through the portal alone discover their gaps during incidents, not before them.

The reference above captures the operational configuration. The principle below crystallises the design decision.

Reference. CA Change Management —

Policy export: Weekly automated export via Graph API to Git repository

Change process: Propose → test (report-only 48h) → approve → enforce → verify → document

Monitoring: Sentinel analytics rule on AuditLogs for all CA policy changes

Drift detection: Weekly diff of current export against previous version

Change approval requirements: Baseline policies (F1-F4): Security lead approval Privileged policies (P1-P3): CISO approval Risk-based policies (R1-R3): Security lead approval Scenario policies: Security engineer + peer review Emergency changes: Any admin, documented within 24h, reviewed within 72h

Soft delete recovery: Enabled, 30-day retention

Version-controlled backup: Git repository, weekly exports, 1-year retention

Stale report-only review: Monthly check for policies in report-only > 30 days