Every admin I know has the same four baseline Conditional Access policies: require MFA for all users, block legacy authentication, require a compliant device for corporate resources, and block sign-ins from risky locations. Ask them if those policies protect everyone in the tenant and they will say yes without hesitating. Now ask them to actually prove it — to list, right now, without opening PowerShell or spending an afternoon clicking through the portal, the users in their tenant who have zero Conditional Access policy applied to them at all. Most cannot answer. Not because the data doesn't exist, but because nobody has ever built the report that surfaces it.
That gap is the subject of this post. Conditional Access does not work the way most admins mentally model it. Policies are not a blanket that silently covers "everyone, forever." They target specific groups, specific applications, and specific conditions — and every one of those targeting decisions is a place a user, a guest, an app, or a service account can slip through unnoticed. Below: exactly why that happens, how to manually spot-check one suspected gap with the Conditional Access What If tool, and a complete PowerShell script that answers the real question properly — for every identity in your tenant, is there at least one enforced policy that actually applies to them?
The problem: your CA baseline has gaps you cannot see
Conditional Access targeting defaults to specific, named scopes — a group, an app, a role — not an implicit "all users, forever" the way a lot of admins assume once the policy is live. That distinction is exactly where coverage silently erodes over time. Five patterns account for almost every real-world gap I have found while auditing tenants:
A user is created in Entra ID today. Your CA policies target a dynamic or assigned security group built months ago. If provisioning doesn't add the new user to that group immediately — a common lag with HR-driven joiner workflows — the user signs in completely outside your MFA and device-compliance requirements until the next group sync.
Someone in another team registers a new enterprise application and nobody loops in security. Your "block legacy auth" and "require compliant device" policies were scoped to a specific list of cloud apps built two years ago — the new app was never added, so it inherits none of your baseline protection.
An old exclusion group — created for a one-off pilot, a vendor migration, or a "temporary" break-glass workaround — is still referenced in your MFA policy's exclude list. Nobody ever removes members from it. It has grown from 3 accounts to 40 over three years.
Emergency-access accounts are correctly excluded from CA so a bad policy change can never lock out the last admin. Correct in principle — but if that exclusion is never revisited, and the account credentials or scope quietly expand, it becomes a standing, completely unmonitored bypass.
Service principals and managed identities authenticate too — and most admins don't realise they need their own, separate Conditional Access policies. Conditional Access for workload identities is a distinct, newer capability with completely different targeting (client applications, not users). A user-scoped MFA policy does nothing for a compromised service principal.
Why it happens: how policy targeting actually works
Every Conditional Access policy evaluates a conditions.users block for user-facing policies (or conditions.clientApplications for workload identity policies) with four fields that determine who the policy actually reaches:
| Field | What it does | Where gaps hide |
|---|---|---|
includeUsers | Specific user IDs, or the special values All, None, GuestsOrExternalUsers | A policy scoped by mistake to a static list of user IDs never grows as the tenant does |
excludeUsers | User IDs always removed from coverage, regardless of include rules | Exclusions always win over inclusions — a single stale exclusion silently defeats an otherwise-correct policy |
includeGroups / excludeGroups | Expands to every current member of the referenced group(s) at evaluation time | Group membership drifts continuously; the policy's intent ("everyone in Finance") and its current effective membership quietly diverge |
includeApplications | Which cloud apps / resources the policy protects — All or a specific list | New app registrations and enterprise apps are not automatically added to an existing app-scoped policy |
The exclusion creep problem in practice
Here is the pattern I see most often on real tenants. A "Require MFA for all users" policy is built correctly: includeUsers = All. Six months later, a contractor migration needed a temporary carve-out, so someone created SG-MFA-Exclusions-Temp and added it to the policy's exclude list. The migration finished. The group was never deleted, and — because it's a security group with no owner review cadence — three more "just add them for now" requests over the following year pushed membership from 4 accounts to 31. The policy still says "MFA for all users" in its name. It has not meant that in over a year.
Group membership changes are also never retroactively re-evaluated against the design intent of a policy — Entra ID has no concept of "this group was supposed to represent X population, alert me if it drifts." It simply evaluates current membership at sign-in time, silently, forever. And critically: Entra ID ships no native report that answers "which users currently have zero applicable Conditional Access policy." The Insights and reporting workbook shows policy-by-policy application counts, not a per-user, cross-policy union. This has to be computed by walking every enabled policy's effective include/exclude set and checking every identity against it — which is exactly what the script later in this post does.
State = enabledForReportingButNotEnforced as a separate bucket, not as coverage.How to verify: spot-checking one gap with the What If tool
Before reaching for a script, the fastest way to sanity-check a single suspected gap — "does user X actually get MFA challenged, or not?" — is the built-in What If tool.
- Sign in to the Microsoft Entra admin centre as at least a Conditional Access Administrator or Security Administrator
- Browse to Entra ID › Conditional Access › Policies, then select What If from the left-hand menu
- Under User, search for and select the identity you want to test — for example a newly created starter, or an account you suspect is caught by a stale exclusion group
- Under Cloud apps or actions, choose the resource you care about — typically Office 365 or All cloud apps
- Optionally set sign-in conditions: device platform, client app type, and IP address / named location, to simulate a specific real-world sign-in scenario
- Click What If and review the two result tabs — Policies that will apply and Policies that will not apply, each with the specific reason (target, condition, or exclusion) why
That single result is the whole problem in miniature: two policies apply as expected, but the baseline MFA policy silently does not — because this test user was added to a "temporary" exclusion group eleven months ago for a project that finished in week two.
The fix: Get-ConditionalAccessCoverageGap.ps1
The script below uses the Microsoft Graph PowerShell SDK to do exactly what the portal can't: pull every Conditional Access policy, expand every referenced group into its actual current membership, and check every user and every workload identity (service principal) in the tenant against that resolved set. Anyone left with zero enforced policy applying to them is flagged red.
Free and open source. Download Get-ConditionalAccessCoverageGap.ps1 from the Imran76Awan/Daily-Tasks repository on GitHub — no sign-in required.
Here is what the first run against a test tenant looks like, before any remediation:
Connecting to Microsoft Graph... Connected as: svc-ca-audit@contoso.com Tenant: ████████-████-████-████-████████████ Retrieving Conditional Access policies... Total policies found : 9 Enforced (State=enabled) : 6 Report-only (not enforced) : 2 Disabled (ignored) : 1 No workload identity (client application) policies found — every service principal will show as RED unless you add one. This usually means Entra ID Workload Identities Premium has not been configured yet. Retrieving users... Enabled users found : 1,187 Evaluating Conditional Access coverage per user... Retrieving service principals (workload identities)... Application service principals found : 64 ================================================================ CONDITIONAL ACCESS COVERAGE GAP AUDIT - 16 Jul 2026 09:14 ================================================================ Identities scanned : 1,251 GREEN (enforced coverage) : 1,246 AMBER (report-only only) : 1 RED (zero applicable policy): 4 ---------------------------------------------------------------- Enforced coverage : 99.6% ================================================================ RED identities (no enforced Conditional Access policy applies): IdentityType DisplayName Identifier UserType ------------ ----------- ---------- -------- User Amelia H████ a.h████@contoso.com Member User Contractor B████ b████@vendor-b2b.com Guest User Ops-BreakGlass-02 bg2█████@contoso.onmicrosoft.com Member ServicePrincipal Finance-Reporting-API ████████-████-████-████-████████████ Workload AMBER identities (covered ONLY by report-only policies — not enforced): IdentityType DisplayName Identifier ReportOnlyOnly ------------ ----------- ---------- -------------- User Priya K████ p.k████@contoso.com Require phishing-resistant MFA (pilot) ACTION: build a true catch-all policy (All users / All cloud apps) with a tightly scoped break-glass exclusion group, add a separate workload identity policy for service principals, then re-run this script to confirm 0 RED.
Reading that output: Amelia H. was a starter created four days earlier and hadn't yet synced into SG-CA-Baseline-All-Staff. The Contractor account was a guest caught by an old SG-MFA-Exclusions-Temp group from a project that ended over a year ago. Ops-BreakGlass-02 was a second emergency-access account nobody remembered existed, let alone reviewed. And Finance-Reporting-API is exactly the workload-identity gap most admins never think to check — a service principal with an application-only Graph token and no Conditional Access policy of any kind covering it.
The core of the script is a group-expansion and set-comparison function — it resolves every group referenced by includeGroups / excludeGroups into a flat set of member IDs (cached, so a group referenced by five policies is only expanded once), then checks whether a given user is included and not excluded:
function Get-ExpandedGroupMemberIds { param([string[]]$GroupIds) $result = [System.Collections.Generic.HashSet[string]]::new() foreach ($groupId in $GroupIds) { if (-not $script:GroupMemberCache.ContainsKey($groupId)) { $members = Get-MgGroupMember -GroupId $groupId -All $script:GroupMemberCache[$groupId] = @($members | ForEach-Object { $_.Id }) } foreach ($id in $script:GroupMemberCache[$groupId]) { [void]$result.Add($id) } } return $result } function Test-UserCoveredByPolicy { param($Policy, [string]$UserId, [string]$UserType) $u = $Policy.Conditions.Users $includeSet = Get-ExpandedGroupMemberIds -GroupIds $u.IncludeGroups $excludeSet = Get-ExpandedGroupMemberIds -GroupIds $u.ExcludeGroups $includesAll = $u.IncludeUsers -contains 'All' $isIncluded = $includesAll -or $includeSet.Contains($UserId) -or ($u.IncludeUsers -contains $UserId) if (-not $isIncluded) { return $false } # exclude always wins — this is where stale exclusion groups silently defeat coverage if ($excludeSet.Contains($UserId) -or ($u.ExcludeUsers -contains $UserId)) { return $false } return $true }
Workload identities are checked the same way, but against Conditions.ClientApplications.IncludeServicePrincipals / ExcludeServicePrincipals instead of the user block — a completely separate targeting mechanism, which is exactly why a user-scoped MFA policy provides zero protection for a compromised service principal.
Remediation: closing the gaps the script finds
Proof it worked: re-running after remediation
After adding the catch-all policy with the correctly scoped break-glass exclusion group, adding the workload identity policy for service principals, fixing the starter's group membership, pruning the stale exclusion group, and switching the pilot MFA policy to enforced — here is the same script run again against the same tenant:
Connecting to Microsoft Graph... Connected as: svc-ca-audit@contoso.com Tenant: ████████-████-████-████-████████████ Retrieving Conditional Access policies... Total policies found : 11 Enforced (State=enabled) : 9 Report-only (not enforced) : 1 Disabled (ignored) : 1 Retrieving users... Enabled users found : 1,188 Evaluating Conditional Access coverage per user... Retrieving service principals (workload identities)... Application service principals found : 64 ================================================================ CONDITIONAL ACCESS COVERAGE GAP AUDIT - 16 Jul 2026 15:52 ================================================================ Identities scanned : 1,252 GREEN (enforced coverage) : 1,252 AMBER (report-only only) : 0 RED (zero applicable policy): 0 ---------------------------------------------------------------- Enforced coverage : 100.0% ================================================================ No zero-coverage identities found in this pass. Re-run periodically — new users, new app registrations, and group membership changes can reopen gaps.
Zero RED, zero AMBER, 100% enforced coverage — including Finance-Reporting-API, now covered by the new workload identity policy. That number will not stay at 100% by itself. New starters, new app registrations, and ordinary group membership churn will reopen gaps within weeks. Schedule this script — a monthly Azure Automation runbook or a Graph-permissioned scheduled task is enough — and treat any RED result as an incident to triage, not a report to file away.
I'll run the Conditional Access coverage script against your tenant, walk you through every gap it finds, and help you build a catch-all policy that doesn't lock anyone out — including workload identities.
References
- Conditional Access overview — Microsoft Learn
- Troubleshoot Conditional Access with the What If tool — Microsoft Learn
- Conditional Access for workload identities — Microsoft Learn
- Microsoft Graph PowerShell SDK documentation — Microsoft Learn
- Manage emergency access (break-glass) accounts in Entra ID — Microsoft Learn
Have you actually checked your tenant for this gap, or are you assuming your baseline policies cover everyone because the names say so? Run the script and find out — I'd genuinely like to know what you find, especially on the workload identity side. Drop a comment or reply on LinkedIn with your RED count before remediation.