[CA 4.1] Building the Framework: From Zip to Green Gate

The design paper made the case. This is the part where you actually stand up the framework in a tenant, and it is where the gap between “imported the policies” and “operates the policies” stops being rhetorical. I built the first release into a live tenant end to end before I let it earn a version number, and everything below is what that run actually looked like, including the parts that went sideways. The sideways parts are the ones you will hit too, so I have left them in rather than pretending the path was clean.

The whole build is about half a day of hands-on work. Most of the calendar time after that is the policies sitting in report-only while you watch them, which is the subject of the next article. This one gets you from a bare admin workstation to a tenant that passes the prerequisite gate with everything green.

Two shells, and it matters which is which

The framework ships two scripts and imports through a third-party tool, and they do not agree on a PowerShell version. The prerequisite and validation scripts want PowerShell 7. The import tool, Micke-K’s IntuneManagement, is a WPF application built for Windows PowerShell 5.1. Trying to run one in the other’s shell is the first hour people lose, so decide it up front: PowerShell 7 for anything named with a VCIO verb, Windows PowerShell 5.1 for the import tool. That split holds for the entire lifecycle of the framework, not just the build.

The Graph SDK modules the scripts depend on are not something you pre-stage. The prerequisite script checks for its six required modules on first run and offers to install them to your user profile, no admin rights involved. If you would rather do it by hand, a single Install-Module with the six module names to CurrentUser scope gets you there. Either way, that is the only environment setup the workstation needs beyond the two shells and a copy of the framework and the import tool, both unblocked with Unblock-File after extraction. Skipping the unblock is the most common reason the import tool half-loads, so do not skip it.

The prerequisite gate, run before you import

The prerequisite script runs twice across the build, and understanding why saves confusion. Some of what it checks can only be true after the import has created objects in the tenant. Break-glass membership is the obvious one: the group does not exist until the import makes it. So the script detects that no VCIO policies are present yet and runs in pre-import mode automatically, treating the not-yet-possible checks as informational rather than failing on them.

On the first run, with -Fix, the things you actually want green are the tenant-level plumbing: security defaults disabled, the Microsoft Intune Enrollment service principal present, and the Temporary Access Pass method enabled. The script creates the service principal and enables TAP for you. It also creates the companion App Protection policies and the managed-app assignment filters through Graph, which matters for a reason I will come back to. Break-glass and a couple of others will show as warnings at this stage, and that is correct. They are not failures. They are things that happen after import.

One war story from this step, because it will catch you if your admin accounts are modern. IntuneManagement authenticates through MSAL.PS, which opens an embedded sign-in window, and that window handles modern passwordless and passkey flows poorly. If your admin account is passkey-first, or if Entra decides to nudge a security-info registration in the middle of sign-in, you can land on an “unsupported browser” wall inside the tool with no way forward, and registering more methods does not help, because the block is the page itself and not a missing method. There are two fixes and the durable one is in the tool: turn on “Use WAM for enhanced login methods” in Settings, which routes authentication through the Windows broker instead of the embedded window. The quick one is to clear any pending registration out of band in a real browser first, at the security-info page, so the tool’s sign-in stops being sent to it. This is not the framework. It is the tool’s authentication library meeting a modern identity, and it is worth knowing before it eats an afternoon.

The import, and two things that look wrong but are not

With the tool signed in, the import is a bulk operation pointed at the framework’s Config folder, with Replace Dependency IDs checked and the Conditional Access state left at report-only or off. Everything ships report-only in the JSON regardless, so nothing enforces the moment it lands. The P2 overlay imports as a second pass against its own folder, which is why that folder carries its own migration table. The per-app templates never get bulk-imported at all, because they hold placeholder application IDs and are meant to be instantiated one at a time later.

Two things during the import will make you think something is broken, and neither is. The first is a “no migration table found” message at folder selection. The migration table exists to remap references to groups that already live in the target tenant and are not part of the import. In this framework every group a policy references ships inside the import set, so the tool resolves them live as it creates them, and the message is irrelevant to the outcome. The second is that there is no “Groups” row in the object-type list. Groups are dependency objects in IntuneManagement, not a first-class import type. They get created automatically when the Conditional Access policies that reference them import, which is also why the import order front-loads dependencies like Authentication Strengths and Named Locations ahead of Conditional Access itself.

When the import finishes you should see the full policy set, all thirty-plus, every group, the named locations with their placeholder values, and the App Protection policies. Two of the three App Protection policies, that is, which brings me to the reason the prerequisite script creates them itself.

What the import tool cannot carry

IntuneManagement imports the iOS and Android App Protection policies without complaint. It does not import the Windows one, because windowsManagedAppProtection is an object type the tool does not handle. You end up with two of three, and the missing one is always Windows. This is not documented anywhere I could find, and I only know it because the lab told me. The framework’s answer is that the prerequisite script creates the Windows Edge MAM policy directly through Graph, so the gap in the tool never becomes a gap in your tenant. Run the script with -Fix and it fills in whatever App Protection policies are missing by name.

The same lab run taught me something, though not the lesson I first took from it. I had shipped three managed-app assignment filters, one per platform, so that MAM would only ever apply to unmanaged devices. Graph rejected the Windows one at build time with a bad-request error, and my first conclusion, that Windows managed-app filters simply do not exist, was wrong. They do. Microsoft lists Windows alongside Android and iOS for app-protection-policy filters. What actually failed was the specific platform value my script used to create it. But chasing the right value turned out to be unnecessary, because the Windows MAM policy does not need the filter at all. Microsoft does not support Windows MAM on a device that is already MDM-enrolled in the same tenant. Such a device does not receive the app protection policy in the first place. So the framework ships the Windows MAM policy unfiltered, and an enrolled Windows device is never double-managed because it is never targeted by MAM to begin with. The iOS and Android filters stay, because those platforms genuinely run MAM and MDM side by side and need the unmanaged-only scoping. The lesson I actually kept is the more useful one: a live tenant will happily disprove your first explanation for a failure, which is exactly why you validate in one before you write anything down as fact.

Populating what the tool leaves empty

The import creates structure. It does not create meaning, and this is deliberate. Every group comes in empty, every named location holds a placeholder that matches nothing real, and the parameters that would tie the framework to a specific customer are left blank by design. There is no country list or IP range in the package that could lock a tenant out on import, which is a failure mode I have watched happen with baselines that ship someone else’s geography as a default.

So this is the step where you do the work the worksheet describes. Two cloud-only accounts into the break-glass group, with whatever names the customer uses, credentialed to the standard the runbook sets. A dynamic membership rule on the users group that you can reason about, reconciled against licensed headcount before you trust it. Real country and IP values replacing the placeholders. Service accounts into their group. Guest-accessible apps and any Tier 0 additions adjusted to the customer’s actual estate. The App Protection policies assigned to All Users with the unmanaged filters on iOS and Android, and unfiltered on Windows. None of this is hard. It is just the part no tool can do for you, because it is the part that is about this customer and no other.

The green gate

Now the prerequisite script runs a second time, without any special flag. It sees the VCIO policies in the tenant, switches itself to the full check set, and this time it means it. Break-glass membership of at least two accounts is a hard requirement here, not a warning, because this is the gate before anything enforces, and break-glass that is not populated at the moment enforcement begins is the difference between a controlled rollout and a lockout. When that run comes back with zero failures, the framework is standing. Structure imported, plumbing created, parameters set, break-glass verified.

Before you move on, open two policies in the portal and confirm they survived the round trip intact, because these two carry the most complex structures in the set and are the best canaries for silent corruption. The Tier 0 control-plane policy should require phishing-resistant MFA and a compliant device, both controls, joined with AND. The enrollment policy should show a sign-in frequency of every time. If those two read correctly, the whole format made it through.

What you have at this point is a tenant full of policies that do nothing yet. That is the correct state, and it is not the finish line. Nothing here is enforcing, and turning it on is its own discipline with its own order, which is where the next article picks up.


Repo: github.com/VirtualCaffeineIO/vcio-ca-framework


Conditional Access
‹ Previous: [CA 4] The VCIO CA Framework
Next: [CA 4.2] Enabling in Rings: An Order That Doesn’t Break Things