---
title: Deploying a Conditional Access Baseline to Any Tenant
description: A repeatable Conditional Access baseline, the order that stops you locking every administrator out, and how to deploy the same set across a fleet of tenants.
---

[Microsoft 365 Security Blog](https://www.datatechs.co.uk/blog)

# [Deploying a Conditional Access Baseline to Any Tenant](https://www.datatechs.co.uk/blog/deploying-a-conditional-access-baseline-to-any-tenant)

 Written by [Marcus Harris](https://www.datatechs.co.uk/blog/author/marcus-harris) | Sep 11, 2026, 8:00:00 AM

Every MSP I know has a Conditional Access baseline. Most of them exist as a document somebody wrote once, a set of screenshots, and a memory of roughly what was done on the last tenant.

That works until you have twenty tenants and no way to answer the question "is this client on the current baseline". Here is the set I deploy, the order that matters, and how to get it repeatable.

## Build the escape hatch first

Before a single policy exists, two break glass accounts and an exclusion group. Not after. Not as step three.

Conditional Access is the most reliable way to lock yourself and every administrator out of a tenant, and the recovery path when it happens is a support ticket and a wait.

Steps one and two are not preparation. They are the only thing standing between you and a lockout.

```
Connect-MgGraph -Scopes "User.ReadWrite.All","Group.ReadWrite.All","Policy.ReadWrite.ConditionalAccess"

# Exclusion group first
$grp = New-MgGroup -DisplayName "CA-Exclusions-BreakGlass" `
  -MailEnabled:$false -SecurityEnabled:$true `
  -MailNickname "ca-exclusions-breakglass"

# Two accounts, cloud only, no licence
1..2 | ForEach-Object {
  $upn = "bg-admin-0$_@$((Get-MgOrganization).VerifiedDomains |
          Where-Object IsDefault | Select-Object -Expand Name)"
  $pw  = -join ((33..126) | Get-Random -Count 32 | ForEach-Object {[char]$_})
  $u = New-MgUser -UserPrincipalName $upn -DisplayName "Break Glass 0$_" `
        -MailNickname "bg-admin-0$_" -AccountEnabled `
        -PasswordProfile @{ Password = $pw; ForceChangePasswordNextSignIn = $false }
  New-MgGroupMember -GroupId $grp.Id -DirectoryObjectId $u.Id
  "$upn  $pw"    # capture these, then put them in the vault
}
```

Those passwords go into a password vault with emergency-only access, not a spreadsheet and not a chat message. Set them never to expire. Then exclude that group from every policy you create from here on.

## The baseline

Sixteen policies, of which eleven work on Business Premium and five need Entra ID P2. Deploy the eleven everywhere and treat the P2 five as an upsell conversation rather than a gap.

**Runs on P1 and Business Premium**

- **Block legacy authentication.** The single highest value policy in the set. Nothing else matters much while this is off.
- **Require MFA for administrators.** All privileged roles, no exceptions beyond break glass.
- **Require MFA for all users.** Once legacy auth is blocked, this actually means something.
- **Block access from unsupported countries.** A named location set that matches where the client operates.
- **Require MFA for device registration.** Closes a common gap in joining rogue devices.
- **Block or restrict device code flow.** An increasingly popular phishing route and rarely needed.
- **Require approved app on mobile.** Pairs with app protection policies for BYOD.
- **Require compliant device for admin portals.** Where you have Intune coverage.
- **Session controls on unmanaged devices.** Sign-in frequency and no persistent browser session.
- **Block service account sign-in outside expected conditions.** Named locations and specific applications.
- **Require MFA to enrol in Intune.**

**Needs Entra ID P2**

- Sign-in risk policy, high risk
- Sign-in risk policy, medium risk
- User risk policies
- Privileged Identity Management activation gates
- Authentication context for the highest value applications

The value of writing this down as a fixed list is not the list itself. It is that "which of the sixteen is this tenant missing" is a question you can answer in a report.

## Report-only is not optional

Every policy goes in as report-only and stays there for a week or two while you watch what it would have done.

**Where to read the results**entra.microsoft.com › Protection › Conditional Access › Insights & reporting

```
$body = @{
  displayName = "CA01 - Block legacy authentication"
  state       = "enabledForReportingButNotEnforced"
  conditions  = @{
    users        = @{
      includeUsers  = @("All")
      excludeGroups = @($grp.Id)
    }
    applications = @{ includeApplications = @("All") }
    clientAppTypes = @("exchangeActiveSync","other")
  }
  grantControls = @{
    operator        = "OR"
    builtInControls = @("block")
  }
}

New-MgIdentityConditionalAccessPolicy -BodyParameter $body
```

Switch `state` to `enabled` when the report-only data is clean. Note the exclusion group is in the policy from the moment it is created, not added later.

Two policies deserve to stay in report-only longer than the rest: anything requiring a compliant device, and anything using named locations. Both depend on data that takes time to become accurate, and both fail closed in ways users notice immediately.

## Making it repeatable across a fleet

The point of a baseline is deploying it without thinking. Export the policies from a reference tenant, strip the tenant-specific identifiers, and treat the result as source you version.

```
# Export from your reference tenant
Get-MgIdentityConditionalAccessPolicy -All |
  Where-Object DisplayName -like "CA*" |
  ForEach-Object {
      $_ | ConvertTo-Json -Depth 12 |
      Out-File ".\baseline\$($_.DisplayName).json"
  }
```

On import, the fields you have to rewrite per tenant are the exclusion group id, any named location ids, and any group or role object ids in the include and exclude blocks. Everything else is portable.

Put the JSON in a repository. When you change the baseline, you change it once and you can see which tenants are behind.

## Auditing what is actually deployed

The report a client will actually read, and the one that tells you which tenant needs attention:

```
$expected = Get-ChildItem .\baseline\*.json |
            Select-Object -ExpandProperty BaseName

$actual = (Get-MgIdentityConditionalAccessPolicy -All)

[pscustomobject]@{
  Tenant       = (Get-MgOrganization).DisplayName
  Deployed     = $actual.Count
  Missing      = ($expected | Where-Object { $_ -notin $actual.DisplayName }) -join ", "
  ReportOnly   = ($actual | Where-Object State -eq "enabledForReportingButNotEnforced").DisplayName -join ", "
  Disabled     = ($actual | Where-Object State -eq "disabled").DisplayName -join ", "
}
```

The `Disabled` column is the one to watch. Policies get switched off to troubleshoot something and switched back on later, except when they are not.

## What goes wrong

**The exclusion group is empty.** Somebody created it, excluded it from everything, and never added the break glass accounts. Verify membership, do not assume it.

**Report-only ran but nobody read it.** The data is only useful if someone looks at the workbook before enabling.

**A new mailbox misses the policy set.** Usually because the policy targets a group rather than All Users and nobody added the new starter.

**Legacy auth gets re-enabled.** Set the authentication policy as the organisation default, or it will apply only to the accounts that existed when you ran it.

If you deploy nothing else from this list, deploy the break glass accounts and the legacy auth block. Those two carry most of the value, and the first one is what lets you recover from the rest.

[View full post](https://www.datatechs.co.uk/blog/deploying-a-conditional-access-baseline-to-any-tenant)

```json
{
  "@context" : "http://schema.org",
  "@type" : "BlogPosting",
  "author" : {
    "@type" : "Person",
    "name" : "Marcus Harris"
  },
  "dateModified" : "2026-09-18T08:34:58.888Z",
  "datePublished" : "2026-09-11T08:00:00Z",
  "headline" : "Deploying a Conditional Access Baseline to Any Tenant",
  "image" : {
    "@type" : "ImageObject",
    "height" : 941,
    "url" : "https://148777188.fs1.hubspotusercontent-eu1.net/hubfs/148777188/datatechs-blog-ca-baseline.png",
    "width" : 1672
  },
  "mainEntityOfPage" : "https://www.datatechs.co.uk/blog/deploying-a-conditional-access-baseline-to-any-tenant",
  "publisher" : {
    "@type" : "Organization",
    "logo" : {
      "@type" : "ImageObject",
      "height" : 60,
      "url" : "/hs/hsstatic/content_shared_assets/static-1.4092/img/default-amp-logo.png",
      "width" : 60
    },
    "name" : "blog"
  }
}
```