
The scariest object in your tenant is the one nobody remembers touching. A Conditional Access policy someone edited by hand at 11pm before a launch, saved, and never wrote down. It works. Probably. Until the day it doesn’t, and you’re reverse-engineering the original intent from a JSON diff in the audit log while a director asks why finance can’t reach Exchange.
On 29 July 2025, Microsoft did something about that. The Microsoft Graph Bicep extension went generally available for a first slice of Entra resources. Conditional Access policies — plus app registrations, service principals, and groups — can now live in a file, in a repo, reviewed in a pull request, and deployed by a pipeline. Declarative. Diffable. The same tooling you already point at your Azure resources.
That’s the good news, and it’s genuinely good. The part the announcement is quieter about: the first time you deploy one, a few things will surprise you. Let me walk you through it in the order you’ll hit them.
The day it shipped: what GA actually covers
GA scope is deliberately narrow. Four resource types made the cut:
Microsoft.Graph/conditionalAccessPoliciesMicrosoft.Graph/applications(app registrations)Microsoft.Graph/servicePrincipalsMicrosoft.Graph/groups
Everything else is either still in preview under the same extension or not modelled at all yet. Users, administrative units, named locations, authentication methods — check the current resource list before you assume one is there. Think of GA as four models you can actually buy off the lot, not the whole showroom. The concept car still exists in preview; it’s just not something I’d put in a production pipeline this quarter.
One more framing point before the code, because it trips people: these are tenant resources, not resource-group resources. There’s no resource group holding your Conditional Access policy. So the deployment scope is the tenant, and your Bicep has to say so.
Minute one: the file
Here’s a real policy — block legacy authentication, the single highest-value CA rule most tenants still haven’t fully enforced. Note that I’ve set it to report-only. More on why in a moment.
Two lines are doing quiet heavy lifting. targetScope = 'tenant' tells Bicep this isn’t a subscription deployment. And extension microsoftGraphV1_0 pulls in the Graph resource types — you register that extension in bicepconfig.json and pin it to the current published version rather than floating on latest. Pin it. A silent extension bump changing behaviour under your pipeline is exactly the kind of Tuesday you don’t want.
The state value enabledForReportingButNotEnforced is report-only mode. It’s the equivalent of a fire drill: you find out who would have been locked out without actually locking anyone out. Ship report-only, read the sign-in logs for a week, then flip state to 'enabled' in a second, one-line pull request. That PR is now your change record, which is the entire point of doing this.
Minute two: the deploy
Tenant-scoped deployment has its own az CLI command. Run the what-if first — always.
Drop --what-if to actually deploy. The --location is required even though a Conditional Access policy has no region — the tenant deployment itself needs a home for its metadata. Mildly annoying, entirely harmless.
Then: the three things that break
One — idempotency keys on a name, not an ID. This is the elegant bit and the dangerous bit in the same breath. Bicep decides whether to create a new object or update an existing one by matching on a key property. For Conditional Access policies, that key is the displayName. Redeploy the file above and it recognises the existing policy and updates it in place — clean, idempotent, exactly what you want. But rename CA001-Block-Legacy-Authentication to CA001-Block-Legacy-Auth and Bicep doesn’t see an edit. It sees a stranger, and it creates a second policy. Now you have two, both enforcing, and a confused help desk. It’s a hotel that keys rooms by the name on the door instead of the number: repaint the sign and housekeeping books you a new room. For applications and groups, the extension uses a uniqueName property you declare explicitly for the same purpose — set it once, never change it.
Two — what-if tells you less than you’d like. What-if is excellent for Azure resource deployments and noticeably thinner for Graph resources. It’ll tell you a policy is being created or modified; don’t expect a crisp property-by-property diff of every nested condition the way you’d get for a storage account. Treat what-if as a smoke alarm, not a full inspection. The report-only state is your real safety net here — that’s the layer that actually protects users if the template does something you didn’t intend.
Three — subscription Owner means nothing here. The identity running the deployment needs directory permissions, not Azure RBAC. To write a Conditional Access policy it needs the Conditional Access Administrator (or Security Administrator) role and the corresponding Graph permission; app registrations want Application Administrator. Your pipeline’s service principal being Owner on the subscription buys you exactly nothing at tenant scope. The first deployment from a fresh pipeline identity fails on authorization roughly every time, and people burn an afternoon on it. Assign the directory role up front and skip the afternoon.
Confirm it landed
After deploy, verify in the admin center under Protection > Conditional Access > Policies — your policy should show with an On (report-only) toggle. Or read it back with Graph PowerShell, which is entirely read-only here and a good pipeline post-check:
-All handles pagination for you — don’t hand-roll a loop over @odata.nextLink when the cmdlet does it.
And here’s the boundary worth stating plainly: Bicep does not retire your Graph PowerShell. Declarative IaC is superb for the desired state of standing objects — the policies and groups that should always look a certain way. It’s the wrong tool for imperative, one-off, data-driven work: onboarding a batch of guests, remediating stale sign-ins, exporting a report. Those stay in PowerShell against reviewed input. You’ll run both, for different jobs, and that’s correct.
The next date on the calendar
Microsoft hasn’t published a firm date for the preview resource types graduating to GA, so I won’t invent one — watch the Graph Bicep extension release notes for named locations and the rest of the Conditional Access supporting cast, because a CA policy that can reference an IaC-managed named location is where this gets properly complete.
But the date that actually matters isn’t on Microsoft’s calendar. It’s the one you set: the day your Conditional Access policies stop being tribal knowledge edited at 11pm, and start being a file someone has to review before it ships. That’s the whole win. Open the pull request.