Notes on Windows endpoint management — Intune, ConfigMgr, and PowerShell
The component-level counterpart to Configuration Service Provider, which covers what a CSP is. This note covers parts: how a URI resolves to code, where that code lives, where the schema lives, and which stores hold state.
A CSP is a registration, a COM class, and whatever subsystem it fronts. There is no CSP file, folder, process, or service to find.
Ask “where is the Wi-Fi CSP?” and there is no satisfying answer — no WifiCSP.exe, no install directory, no service. The CSP exists as a registry registration that names a COM class, and the work it does happens inside whatever subsystem it fronts.
The comparison worth holding is the one Configuration Service Provider already draws — CSP as the MDM equivalent of a Group Policy client-side extension — because the two are registered along strikingly similar lines, and the differences are where the diagnostics diverge:
| CSP | Group Policy CSE | |
|---|---|---|
| Registration | Provisioning\CSPs\, keyed by URI root |
Winlogon\GPExtensions\{CSE GUID} |
| Names its code as | A COM class id, resolved onward to a DLL | A DllName value, stated directly |
| Schema | DDF XML — published, not on the device | ADMX/ADML — on the device in PolicyDefinitions |
| Addressed by | OMA-URI | Registry policy key |
| Scope split | Separate registrations per context root | Separate User/Computer halves of one GPO |
| Runs as | In-proc, inside its caller | In-proc, inside gpsvc |
| “Which file is it?” | Two lookups away, varies by build | Named in the key |
Practical consequence: “the CSP isn’t installed” is never the diagnosis. The equivalent failure is the node does not exist on this build — a versioning question, not a deployment one.
This chain is the whole mechanism, and it is the answer to what physically constitutes a CSP:
OMA-URI ./Device/Vendor/MSFT/Policy/Config/Update/ManagePreviewBuilds
│
│ longest-prefix match against the registered roots in
▼
HKLM\SOFTWARE\Microsoft\Provisioning\CSPs\
└── .\Device\Vendor\MSFT\Policy ← one key per CSP, per context root
│
│ the key names a COM class id
▼
HKLM\SOFTWARE\Classes\CLSID\{…}\InprocServer32
│
▼
%SystemRoot%\System32\<provider>.dll
Three things follow from the shape of it:
Provisioning\CSPs returns a not-found status before any CSP code runs..\Device\Vendor\MSFT\Policy and .\User\Vendor\MSFT\Policy are distinct keys. A CSP registered only under one root genuinely does not exist in the other — this is the mechanical basis for the dual-context rules in Context Roots - Device and User.Provenance Microsoft confirms that per-CSP registration keys exist in the registry, in an unexpected place — the DeviceManageability CSP page, which states that for performance it “directly reads the CSP version from the registry” and that “The
csp_versionis a value under each of the CSP registration keys.” It never gives their path.So
csp_versionis the one value name with documented backing. The location (Provisioning\CSPs, keyed by URI root) and the COM resolution step throughCLSID\{guid}\InprocServer32are practitioner knowledge — verifiable on any device with the check below, but absent from Microsoft’s documentation. Treat the mechanism as sound and the specific path as something to confirm on the build in front of you.That same page is also a reminder the registry is not a passive mirror: Microsoft notes the CSP’s own
GetPropertyimplementation had to be updated to read from the registry too, “so that both the paths return the same information.” Two independently maintained read paths, deliberately reconciled.
A CSP is loaded into whichever component is calling it. It has no process of its own:
| Caller | When |
|---|---|
omadmclient.exe |
During an OMA-DM session — see Session Lifecycle |
| The WMI bridge host | Local access via root\cimv2\mdm\dmmap, see MDM Bridge WMI Provider |
dcsvc |
The declared configuration channel |
This mirrors the task-triggered model in MDM Client Anatomy, and produces the same shift in questioning:
Because the CSP runs inside its caller, a CSP fault never appears as a crashed service. It appears as a status code attributed to the session — which is why Alerts and Status Codes is the triage surface, not Task Manager.
The DDF is published documentation, and the arrow runs the opposite way from what the word “schema” suggests:
CSP implementation ──► DDF published ──► docs · Intune's catalog · you
(the rules live here) (a description of (consumers, all off-device)
what was built)
The rules — which nodes exist, which verbs each accepts, what type each takes — are compiled into the CSP’s own code, behind the registration above. The DDF describes that behaviour after the fact. Nothing on the device ever reads one: if every DDF file vanished tomorrow, devices would behave identically.
This is the sharpest anatomical difference from Group Policy, and it runs against intuition. ADMX in %SystemRoot%\PolicyDefinitions genuinely is parsed at runtime, by gpsvc and by the policy editor. ADMX is a real schema; DDF only looks like one.
The files themselves live on each CSP’s reference page on Microsoft Learn, plus the per-release archives Microsoft publishes so a build’s added nodes can be diffed. Third-party MDM vendors read them to build their own policy UIs — and so does Intune, which is the whole of the next section.
So how does the device decide what is legal? Empirically, at request time. The status codes are the same rules the DDF documents, enforced dynamically:
| DDF declares | Enforced on device by | Failure |
|---|---|---|
| The node exists | Registration lookup, then the CSP’s own node table | 404 |
AccessType — which verbs are permitted |
The CSP rejects the verb | 405 |
DFFormat — the data type |
The CSP rejects the payload | 415 |
See Alerts and Status Codes. This is also why a node can appear in the published DDF and still return 404: the DDF describes the CSP as designed, the device has only the code it shipped with, and nothing reconciles the two.
The on-device ways to ask “what is legal here” are therefore all indirect:
root\cimv2\mdm\dmmap — its presence proves the projection exists on this build.Get against an interior node — returns its children, so the tree can be walked.The DDF governs authoring, then drops out entirely before anything is delivered:
1 Microsoft implements the CSP in a Windows build, and publishes its DDF
│
2 Intune ingests the DDF service-side ──► the settings catalog
│ name · description · DFFormat · allowed values · OMA-URI · supported builds
│
3 You pick a setting in a profile and supply only a value
│
4 Assignment compiles the profile into OMA-DM operations
│
▼ ─────────── the DDF stops here ───────────
5 Next session: omadmclient.exe dials out — scheduled poll, or WNS-nudged
│
6 Replace / Add commands arrive inside a SyncML envelope
│
7 Client matches each LocURI against Provisioning\CSPs, loads the CSP in-proc
│
8 CSP validates against its own compiled rules and returns a status
The settings catalog is Intune reading the DDF so that you do not have to. It supplies the OMA-URI and the format; you supply the value. A custom OMA-URI profile is the identical pipeline with steps 2 and 3 done by hand — which is precisely why type errors surface there and almost never in the catalog. Nothing is checking your DFFormat against the DDF, so the first thing to validate it is the CSP itself, and its answer is 415. Policy CSP makes the same point from the other side: the catalog is a presentation layer over CSPs.
The catalog also carries each setting’s supported Windows builds, so it can warn you before you assign. A custom OMA-URI carries no such guard — the version mismatch surfaces as a 404 on the device instead, which is the versioning failure Configuration Service Provider describes.
The payoff: steps 5–8 consult nothing but the device’s own registration and compiled code. Delivery does not carry a schema and the device never fetches one. Every guarantee you were given at authoring time was Intune’s, made service-side — the device re-derives its answer independently, and when the two disagree, the device wins.
HKLM\SOFTWARE\Microsoft\PolicyManager\default\{Area}\{Policy}
This is the shipped definition store: every Area and Policy the build supports, present whether or not anything has ever been configured. It is distinct from the value stores that PolicyManager Registry covers, and the distinction is easy to lose:
| Branch | Holds | Present when |
|---|---|---|
default\ |
Definitions — what this build supports | Always, shipped with the OS |
providers\{EnrollmentGUID}\ |
What one provider asked for | Something was configured |
current\ |
Merged effective values | Something is in force |
Why it earns its keep: default\ is the only dependable on-device answer to “does this policy exist on this Windows version?” — the versioning failure named in Configuration Service Provider. A policy absent from default will never apply no matter how the profile is authored, and no amount of console retrying changes that.
For Policy CSP the answer is uniform, and PolicyManager Registry covers it. For every other CSP there is no common store — each writes into the subsystem it fronts:
| CSP | State lands in |
|---|---|
Wifi, VPNv2 |
The network profile stores those services already use |
ClientCertificateInstall |
The certificate stores |
BitLocker |
Volume metadata and TPM state |
DevDetail, DeviceStatus |
Nowhere — computed on read, see CSP Categories |
What is uniform is the client’s record of what it last reported upward, held in NodeCache under Provisioning\NodeCache. That store belongs to the MDM client, not to any CSP — a distinction that matters when a value is correct on the device but stale in the console.
Warning Hand-editing values under
PolicyManagerto “fix” a policy desynchronises the device from NodeCache and from the server’s view. The device will report the state it believes it last sent, not what you typed. Read these keys freely; change them through the CSP.
# Every CSP registered on this build, by URI root
Get-ChildItem -Recurse HKLM:\SOFTWARE\Microsoft\Provisioning\CSPs |
Select-Object -ExpandProperty Name
# ONE registration's values, when you already know which one you want.
# -LiteralPath matters here: the path contains a '.' segment that PowerShell
# would otherwise normalise away.
$k = 'HKLM:\SOFTWARE\Microsoft\Provisioning\CSPs\.\Device\Vendor\MSFT\Policy'
Get-ItemProperty -LiteralPath $k
# EVERY registration that carries a class id, with the value name each one used.
# Matched on shape rather than on value name, because the names vary by build —
# so this is where a CLSID comes from, with no path known in advance.
# Enumerating also sidesteps the '.' path segment that trips hand-typed paths.
$regs = Get-ChildItem -Recurse HKLM:\SOFTWARE\Microsoft\Provisioning\CSPs | ForEach-Object {
$key = $_
(Get-ItemProperty -LiteralPath $key.PSPath).PSObject.Properties |
Where-Object { $_.Value -is [string] -and $_.Value -match '^\{[0-9A-Fa-f-]{36}\}$' } |
ForEach-Object {
[pscustomobject]@{
Registration = $key.Name -replace '.*\\CSPs\\', ''
ValueName = $_.Name
ClassId = $_.Value
}
}
}
$regs | Format-Table -AutoSize
# Resolve each class id to the DLL behind it. HKCR is not mounted by default
# in PowerShell, so go via HKLM\SOFTWARE\Classes.
$regs | ForEach-Object {
[pscustomobject]@{
Registration = $_.Registration
Dll = (Get-ItemProperty "HKLM:\SOFTWARE\Classes\CLSID\$($_.ClassId)\InprocServer32" `
-ErrorAction SilentlyContinue).'(default)'
}
}
# Does this build support the policy at all?
# The subkeys of an Area are its POLICY NAMES. Swap 'Update' for any other Area,
# or drop the Area entirely to list every Area this build knows.
# name listed -> supported by this build (says nothing about it being set)
# name absent -> no profile will ever apply it; expect a 404 or a silent no-op
# path-not-found -> the Area itself does not exist here, so check the Area name
Get-ChildItem HKLM:\SOFTWARE\Microsoft\PolicyManager\default\Update |
Select-Object PSChildName
# Is the CSP projected into WMI on this build?
# Get-CimClass asks whether the CLASS EXISTS, not whether it holds data — use
# Get-CimInstance for values, and expect to need SYSTEM rather than just elevation.
# classes listed -> the bridge projection exists for those Areas
# nothing returned -> no Area matches the wildcard on this build
# namespace error -> root\cimv2\mdm\dmmap is missing entirely, a larger finding
# Swap Config01 for Result01 to check the read-back surface instead of the write one.
Get-CimClass -Namespace root\cimv2\mdm\dmmap -ClassName MDM_Policy_Config01_* |
Select-Object CimClassName
On the registry provider, Get-Item returns the key — whose .Property member lists value names — while Get-ItemProperty returns the values. Reaching for the first when you want a class id is the easy mistake: it produces plausible-looking output containing nothing you can act on.
The last two commands are existence checks, not state checks — one against the registry definition store, one against the WMI projection. Presence proves the build supports something; it never proves anything is configured. For that, read current (PolicyManager Registry) or the Result/ branch (Policy CSP).
| Symptom | Anatomical cause | Where to look |
|---|---|---|
| Node returns not-found | No registration for that root on this build | Provisioning\CSPs, then Alerts and Status Codes |
| Node resolves, write rejected | AccessType forbids the verb |
Access Types and Scope |
| Policy never applies, no error | Area/Policy absent from PolicyManager\default |
Version mismatch, not delivery |
| Applies, then reverts | A second provider wrote after you | Compare providers\{GUID} against current |
| WMI bridge access denied | Running elevated, not as SYSTEM | MDM Bridge WMI Provider |
See also: Configuration Service Provider, MDM Client Anatomy, DDF - Device Description Framework