Server-side Chrome policy delivery for Entra IDβonly (Azure AD joined) devices β delivering up-to-date Chrome policies without the Intune Settings Catalog lag and the limitations of ADMX import.
Managing Chrome policies through Intune is slow and constrained:
- Settings Catalog lag β Chrome settings exposed via the Intune Settings Catalog typically trail the latest Chrome release by ~2 months, so newly added policies aren't available when you need them.
- ADMX import limits β Importing Chrome's ADMX as a custom template is the workaround, but it has hard limitations:
- Max 20 ADMX files can be imported per tenant
- Policy duplication β imported policies are duplicated in the update catalog, cluttering the experience
- No clean removal β an old ADMX cannot be deleted while it is still referenced by a custom template policy
- Operational friction β keeping templates current means re-importing and reconciling assignments on every Chrome version bump.
Chrome Policy Manager implements a server-side policy resolution engine that delivers Chrome policies directly to device registries via Intune Proactive Remediation scripts, decoupling policy delivery from the Settings Catalog and ADMX import constraints.
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β ARCHITECTURE β
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ€
β β
β ββββββββββββββββ SignalR βββββββββββββββββ APIM βββββββββ β
β β Admin UI βββββrealtimeβββ REST API ββββmTLSββββΆβ Intuneβ β
β β (Blazor βββ REST ββββββΆβ (.NET 10) β β Remed.β β
β β Server) β β Min. API β β (PS1) β β
β ββββββββββββββββ ββββ¬ββββ¬ββββ¬βββββ βββββββββ β
β β² PolicyStatusHub β β β β
β β CommandStatusHub β β ββββββββββββββ β
β β ββββββ΄ββββ βββββββ΄βββββββ β
β ββββββ΄ββββββββ β SQL DB ββ β MS Graph β β
β β Event Grid βββpublish(MI)ββ€ (PE) ββ β (delta + β β
β β topic β βββββββββββ β webhooks) β β
β β policy-statusβββwebhookβββββββββββββββ ββββββββββββββ β
β βββββββββββββββ β β
β βββββββββββ΄βββββββββββ β
β β Service Bus β cpm-commands β
β β (MI only, β cpm-command-status β
β β disableLocalAuth)β device-reports β
β βββββββββββ¬βββββββββββ β
β β trigger β
β βββββββββββ΄βββββββββββ β
β β Worker β privileged Graph β
β β (Azure Functions) β actions (push remed., β
β β β membership sync) β
β ββββββββββββββββββββββ β
β β
β Cross-cutting: Key Vault Β· App Configuration Β· App Insights β
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
| Feature | Description |
|---|---|
| ADMX Catalog Ingestion | Parse Chrome ADMX/ADML templates β browse 700+ policies with descriptions, types, categories |
| Multi-Version ADMX Registry | Multiple Chrome ADMX template versions coexist with lifecycle states (Staged β Active β Retired); pick which ADMX version each PolicySet version is authored against, with no 20-template cap and clean version deletion |
| PolicySet Versioning | Immutable versions (Draft β Active β Archived) with hash-based change detection |
| Group-Based Targeting | Assign policies to Entra ID security groups with priority-based conflict resolution |
| Mandatory & Recommended | Support both Chrome policy scopes per assignment |
| Machine & User Targeting | Apply policies to the device (HKLM) or per logged-on user (HKCU) β see the User-Level Policies Admin Guide |
| Effective Policy Resolution | Server resolves device β groups β assignments β merged settings (lower priority wins) |
| Device Observability | Real-time compliance dashboard, offline detection, error tracking |
| Device Log Ingestion | Centralized log collection from detection/remediation scripts with level filtering |
| Policy Validation | Schema-based validation of policy values against known Chrome policy types |
| Intune Delivery | Proactive Remediation hourly check β detect drift β apply policies via registry |
| Inline Remediation | Detection script optionally remediates drift without waiting for Intune remediation cycle |
| Push Remediation Trigger | Optional Windows-style push: cycle assignment group devices and invoke on-demand remediation commands (batched) |
| Audit Trail | Full audit logging for all policy changes and device interactions |
Compliance overview at a glance β total devices, compliant/error/offline counters, and the most recent device reports with policy hash, Chrome version and last contact.
Browse the ingested Chrome ADMX catalog (722 policies) with search, category/scope filters, type badges and an "update available" banner when Google ships a newer template. The ADMX template registry lists every imported version with its status (Staged / Active / Retired) and policy counts β activate, retire or delete versions inline, and import a new version without losing the previous ones.
Per-policy editor with inline description, value guidance, target PolicySet selection and a registry write preview before adding.
Immutable, hash-tracked versions per PolicySet β inspect the resolved Chrome policy keys (JSON) for any version, and choose which Chrome ADMX template version each one is authored against (defaults to the Active template, snapshotted on the version).
Assign a policy version to an Entra ID security group with priority-based conflict resolution, Mandatory/Recommended scope, and optional Windows-style push remediation.
All assignments with group, scope, priority, target policy version, enabled state and push-remediation toggle.
Per-device observability β compliance status, policy hash, Chrome/OS versions, hardware model, script version, keys written and last contact.
Manage the trusted CA chain (Root/Subordinate) used to validate device client certificates on /api/devices/*, persisted to Azure App Configuration and consumed by the API.
ChromePolicyManager/
βββ src/
β βββ Server/
β β βββ ChromePolicyManager.Api/ # REST API (.NET 10 Minimal API)
β β β βββ Data/ # EF Core DbContext + models
β β β βββ Endpoints/ # Policy, Assignment, Device, Catalog, Monitoring, Webhook, Command, EventGrid, Config
β β β βββ Hubs/ # SignalR: PolicyStatusHub, CommandStatusHub (realtime portal)
β β β βββ Middleware/ # APIM Gateway authentication middleware
β β β βββ Models/ # PolicySet, Version, Assignment, CatalogEntry, DeviceLog
β β β βββ Services/ # AdmxParser, EffectivePolicy, Graph, Reporting, Validator,
β β β # CommandPublisher, EventGridEventPublisher, PushRemediation
β β βββ ChromePolicyManager.Admin/ # Blazor Server Admin UI (MudBlazor)
β β β βββ Components/Pages/ # Dashboard, Catalog, Policies, Assignments, Devices
β β βββ ChromePolicyManager.Worker/ # Azure Functions worker (Service Bus-triggered)
β β βββ Functions/ # CommandHandlerFunction (cpm-commands trigger)
β β βββ Services/ # PrivilegedGraphActions, StatusPublisher
β βββ Shared/
β β βββ ChromePolicyManager.Contracts/ # Shared contracts: PrivilegedCommand, CommandStatus,
β β # PolicyStatusEvents, QueueNames
β βββ Client/
β βββ Detect-ChromePolicy.ps1 # Intune detection script (supports inline remediation)
β βββ Remediate-ChromePolicy.ps1 # Intune remediation script
βββ infra/
β βββ Deploy.ps1 # Unified deploy: infra (Bicep) + code (API + Admin)
β βββ Deploy-Infrastructure.ps1 # Imperative az-CLI provisioning (alternative)
β βββ main.bicep # Infrastructure-as-Code (Bicep), dev/prod SKU tiers
β βββ main.dev.bicepparam # Dev parameters (cost-optimized SKUs)
β βββ main.prod.bicepparam # Prod parameters (production-grade SKUs)
β βββ apim/ # API Management policies
βββ tools/ # ADMX template downloads (gitignored)
- .NET 10 SDK
- Azure subscription (with Intune license for remediation)
azCLI authenticatedghCLI (optional, for repo operations)
The unified Deploy.ps1 script provisions the Azure infrastructure (Bicep) and builds/deploys the API + Admin code in one pass. SKUs are sized automatically per environment.
cd infra
# Dev (cost-optimized SKUs)
.\Deploy.ps1 -EnvironmentName dev -ClientId <api-app-id> -ClientSecret (Read-Host -AsSecureString)
# Prod (production-grade SKUs, Service Bus enabled)
.\Deploy.ps1 -EnvironmentName prod -ClientId <api-app-id> -ClientSecret (Read-Host -AsSecureString)
# Preview infra changes without applying (Bicep what-if)
.\Deploy.ps1 -EnvironmentName prod -SkipCode -WhatIfUseful switches: -SkipInfra (deploy code only), -SkipCode (deploy infra only), -SubscriptionId, -Location.
This creates: Resource Group, VNet (App Service VNet integration), SQL Server (Private Endpoint, Entra-only auth), App Service Plan + Web Apps (API + Admin), Key Vault, App Configuration, API Management, Application Insights, and (prod) Service Bus with Private Endpoint.
| Resource | dev |
prod |
|---|---|---|
| App Service Plan | B1 (Basic) | P1v3 Γ2 (PremiumV3) |
| SQL Database | Basic (5 DTU) | S2 (Standard, 50 DTU) |
| API Management | off (direct API) | Standard |
| App Configuration | Free | Standard |
| Service Bus | off by default | Standard (Private Endpoint) |
| Log Analytics retention | 30 days | 90 days |
The tier is selected by the skuTier Bicep parameter (defaults from environmentName) and the matching parameter files: main.dev.bicepparam / main.prod.bicepparam.
APIM in dev: APIM is the most expensive fixed-cost resource (~β¬45/month). It is off by default in dev (
deployApim=false). Devices then call the backend directly β setCPM_USE_DIRECT_API=trueon the device and leaveApimGateway:ClientIdunset so the backend middleware allows direct access. This skips mTLS, rate limiting and edge JWT validation, so device endpoints are unauthenticated in dev β acceptable for development, not for prod.
Note:
Deploy-Infrastructure.ps1(imperative az-CLI variant) remains available for step-by-step provisioning without Bicep.
Download the Chrome ADMX templates and upload via the Admin UI or API:
# Via API (multipart upload). Add -F "activate=true" to make this the Active
# template immediately; otherwise it is imported as Staged and coexists with
# the current Active version until you activate it from the registry.
curl -X POST https://your-api.azurewebsites.net/api/catalog/import \
-F "admxZip=@policy_templates.zip" \
-F "version=136.0"Use the Admin UI at https://your-admin.azurewebsites.net/catalog to:
- Browse the catalog β filter by category/type β view descriptions
- Select policies and configure values
- Create PolicySets (e.g., "Security Baseline", "User Experience")
- Add versions with specific settings
- Assign to Entra ID groups with priority
The deployment script automatically creates a Proactive Remediation in Intune that:
- Runs hourly on targeted devices
- Detects drift by comparing local policy hash vs server hash
- Remediates by writing Chrome registry policies directly to
HKLM:\SOFTWARE\Policies\Google\Chrome - Supports inline remediation in the detection script (configurable via
$EnableInlineRemediation) for faster drift correction without waiting for Intune's remediation cycle
| Method | Endpoint | Description |
|---|---|---|
GET |
/api/catalog |
Browse policy catalog (filter: ?category=&search=&dataType=&recommended=&version=; version selects which ADMX template, defaults to Active) |
GET |
/api/catalog/{id} |
Get full details for a single catalog entry |
GET |
/api/catalog/categories |
List available categories (?version= to target a specific ADMX template) |
GET |
/api/catalog/stats |
Import statistics |
GET |
/api/catalog/templates |
List imported ADMX template versions (status, counts, imported date) |
POST |
/api/catalog/templates/{id}/activate |
Make a version the Active template (demotes the previous Active to Retired) |
POST |
/api/catalog/templates/{id}/retire |
Retire a non-active version |
DELETE |
/api/catalog/templates/{id} |
Delete a version (blocked while Active or referenced by a Draft policy version) |
POST |
/api/catalog/import |
Import ADMX zip (multipart/form-data; activate to set Active after import) |
POST |
/api/catalog/import-from-url |
Download and import ADMX templates from a URL (?activate=) |
POST |
/api/catalog/import-local |
Import ADMX from a local server path |
| Method | Endpoint | Description |
|---|---|---|
GET |
/api/policies |
List all PolicySets with versions |
GET |
/api/policies/{id} |
Get a single PolicySet with its versions |
POST |
/api/policies |
Create new PolicySet |
POST |
/api/policies/{id}/versions |
Add version with settings JSON |
POST |
/api/policies/versions/{id}/promote |
Promote Draft β Active |
POST |
/api/policies/{id}/rollback/{versionId} |
Rollback to previous version |
POST |
/api/policies/{id}/add-setting |
Add a single setting to the current draft |
GET |
/api/policies/{id}/draft-settings |
Get settings from the current draft version |
| Method | Endpoint | Description |
|---|---|---|
GET |
/api/assignments |
List all assignments |
POST |
/api/assignments |
Create group assignment (priority + scope + optional push remediation) |
PUT |
/api/assignments/{id}/priority |
Update assignment priority |
PUT |
/api/assignments/{id}/push-remediation |
Enable/disable push remediation for assignment (optional immediate trigger) |
POST |
/api/assignments/{id}/push-remediation/trigger |
Manually trigger push remediation command dispatch |
DELETE |
/api/assignments/{id} |
Remove assignment |
GET |
/api/groups/search |
Search Entra ID groups (for assignment UI) |
| Method | Endpoint | Description |
|---|---|---|
GET |
/api/devices/{id}/effective-policy |
Resolve effective policy for device (supports ETag/304) |
POST |
/api/devices/{id}/report |
Device reports compliance status (202 Accepted) |
GET |
/api/devices/{id}/history |
Get device compliance history |
POST |
/api/devices/{id}/logs |
Batch ingest device logs (detection/remediation) |
GET |
/api/devices/{id}/logs |
Query device logs (filter by level, script type) |
| Method | Endpoint | Description |
|---|---|---|
GET |
/api/monitoring/dashboard |
Compliance dashboard data |
GET |
/api/monitoring/offline-devices |
Devices offline >24 hours |
GET |
/api/monitoring/error-devices |
Devices with recent errors |
| Method | Endpoint | Description |
|---|---|---|
POST |
/api/webhooks/group-change |
Microsoft Graph change notification receiver |
GET |
/api/webhooks/changed-groups |
List groups with pending membership changes |
POST |
/api/webhooks/acknowledge/{groupId} |
Acknowledge a group change (clear dirty flag) |
| Method | Endpoint | Description |
|---|---|---|
GET |
/health |
Health check |
Client Device β API: "What policies apply to me?" (GET /devices/{id}/effective-policy)
β
βΌ
MS Graph: devices/{id}/memberOf β [Group1, Group2, ...]
β
βΌ
Match groups β Active PolicyAssignments
β
βΌ
Sort by Priority (ascending: lower = higher priority)
β
βΌ
Merge settings (first-writer-wins per key, separated by scope)
β
βΌ
Return: { mandatory: {...}, recommended: {...}, hash: "abc123" }
Instead of relying on Intune's ADMX ingestion pipeline β with the Settings Catalog lag and ADMX import limits described above β Chrome Policy Manager writes Chrome policies directly to the registry keys Chrome already reads. New policies are available the moment the catalog is imported into CPM, independent of the Intune release cadence.
- Chrome's
PolicyLoaderWinreadsHKLM\SOFTWARE\Policies\Google\Chrome(and theHKCUequivalent) unconditionally - Registry-based policy loading does not require Active Directory, a domain join, or Group Policy
- Chrome polls the registry every 15 minutes (
kReloadInterval = base::Minutes(15)) - Entra IDβonly devices get
FULLY_TRUSTEDmanagement authority β no policy filtering
PolicyLoaderWinloads machine policies fromHKLM\SOFTWARE\Policies\Google\Chromeand user policies from theHKCUhive- The registry policy provider is always active on Windows, regardless of join state β so writing the keys directly is sufficient for Chrome to apply them
Device-facing endpoints are protected by Azure API Management acting as a security gateway. The backend API never receives unauthenticated device traffic.
ββββββββββββββββ mTLS client cert ββββββββββββββββ Managed Identity ββββββββββββββββ
β Device β ββββββββββββββββββββΆ β APIM β βββββββββββββββββββββΆ β Backend API β
β (PowerShell) β (issued by the β Gateway β β (.NET 10) β
ββββββββββββββββ customer PKI) ββββββββββββββββ ββββββββββββββββ
β
βββββββ΄βββββββ
β Validates β
β client cert β
β vs trusted β
β CA chain β
β Rate limits β
β Strips β
β spoofable β
β headers β
ββββββββββββββ
Security flow:
- Device presents a client certificate issued by the customer's PKI β deployed to the device via Intune (SCEP/PKCS) β over mTLS
- APIM validates the client certificate against the trusted CA chain configured in CPM (Root + Subordinate CAs)
- APIM rate-limits per device identity (30 calls/hour/device)
- APIM strips any client-supplied identity headers (anti-spoofing)
- APIM sets a trusted
X-Forwarded-Device-Idderived from the validated certificate - APIM authenticates to the backend using its managed identity (no shared secrets)
- Backend middleware verifies the request originates from APIM and re-validates the client certificate against the configured CA chain
Separation of concerns:
| Layer | Responsibility |
|---|---|
| APIM | Device mTLS (client certificate), rate limiting, DDoS protection, request logging |
| Backend | Business logic, policy resolution, database, Graph API |
| Admin UI | Direct Entra ID JWT auth (doesn't go through APIM) |
- Entra ID authentication for Admin UI (interactive login)
- Managed Identity for all Azure resource access (no stored credentials)
- Key Vault for secrets (connection strings, Graph client secret)
- Entra-only SQL auth (no SQL passwords β MCAPS compliant)
- Audit logging for all policy changes and device interactions
- CORS restricted to Admin UI origin only
- Service Bus for async device report processing (202 Accepted pattern)
- Backend restriction: device endpoints reject calls not originating from APIM
The solution is designed to handle large-scale enterprise environments (100,000+ devices) with minimal infrastructure cost. Three key optimizations make this possible:
The GET /devices/{id}/effective-policy endpoint returns an ETag header containing the policy hash. On subsequent requests, the client sends If-None-Match with its cached hash:
Client β API: GET /effective-policy (If-None-Match: "abc123")
API β Client: 304 Not Modified β No body, minimal compute
Only when policy actually changes:
Client β API: GET /effective-policy (If-None-Match: "abc123")
API β Client: 200 OK + full payload (ETag: "def456")
Impact: At 100k devices/hour with ~90% steady state β only ~10k full responses/hour carry a payload.
Instead of calling Microsoft Graph for every device check-in (which would hit throttling limits at scale), the API subscribes to real-time webhook notifications for group membership changes:
βββββββββββββββ Webhook: "Group X changed" βββββββββββββββ
β Microsoft β βββββββββββββββββββββββββββββββββββΆ β CPM API β
β Graph β β (marks β
βββββββββββββββ β group as β
β dirty) β
ββββββββ¬βββββββ
β
Device check-in: βΌ
- Device in Group X β Graph call (real-time, fresh data)
- Device in Group Y (unchanged) β use cached membership
Implementation:
GroupChangeNotificationService(BackgroundService) maintains subscriptions for all groups used in policy assignments- Subscriptions auto-renew before the 4230-minute Graph limit (~3 days)
/api/webhooks/group-changereceives notifications and marks affected groupsWebhookEndpoints.HasGroupChanged()allows the effective policy resolver to skip Graph calls for unchanged groups
Impact: Reduces Graph API calls from 100,000/hour to ~50-100/hour (only devices in groups that actually changed), while maintaining zero-latency reactivity β policy changes propagate within minutes, not hours like Intune.
Upgraded from Basic (5 DTU) to Standard S2 to handle sustained write throughput:
- 100k device reports/hour = ~28 writes/sec sustained
- S2 provides 50 DTU β comfortable headroom for reads + writes + indexes
| Metric | Without optimizations | With optimizations |
|---|---|---|
| Graph API calls/hour | 100,000 (throttled) | 50-100 |
| Full policy responses/hour | 100,000 | ~10,000 |
| Network bandwidth/hour | ~500 MB | ~50 MB |
| SQL write pressure | 100k full reports | 100k lightweight + 10k full |
| Reactivity | N/A (was polling) | Real-time (webhook push) |
The default Bicep deployment uses development-friendly SKUs. For production scale, upgrade to:
| Component | Default (Bicep) | Recommended (100k+) | Monthly Cost (est.) |
|---|---|---|---|
| App Service | B1 | S2 or P1v3 | β¬70-140 |
| Azure SQL | Basic (5 DTU) | S2 (50 DTU) | β¬60-150 |
| Service Bus | Basic | Standard | β¬10 |
| API Management | Developer | Consumption or Standard | β¬50-300 |
| App Configuration | Free | Standard | β¬35 |
| Total | ~β¬225-635/month |
| Component | Technology |
|---|---|
| API | .NET 10, Minimal API, Entity Framework Core 10 |
| Admin UI | Blazor Server, MudBlazor 8 |
| Database | Azure SQL (Entra-only auth), SQLite fallback for development |
| Auth | Microsoft Identity Web 3.x, MSAL, mTLS client certificates (customer PKI) |
| Group Resolution | Microsoft Graph SDK 5.x + Change Notifications |
| Messaging | Azure Service Bus (async device reports) |
| Config | Azure App Configuration |
| Secrets | Azure Key Vault |
| Observability | Azure Monitor OpenTelemetry |
| Hosting | Azure App Service (B1 default, scale to S2/P1v3) |
| API Gateway | Azure API Management (Developer SKU default) |
| Client | PowerShell 5.1 (Intune Proactive Remediation) |
| Policy Catalog | Chrome ADMX/ADML parser (700+ policies) |
| Validation | ChromePolicyValidator (schema validation for known policy types) |
- Fork the repo
- Create a feature branch (
git checkout -b feature/amazing-feature) - Commit your changes
- Push to the branch
- Open a Pull Request
This project is licensed under the MIT License - see the LICENSE file for details.
chrome-policy, intune, entra-id, azure-ad-joined, admx, group-policy-workaround, chrome-enterprise, mdm, proactive-remediation, browser-management, endpoint-management, registry-policy, blazor, dotnet, azure
Built to solve a real-world enterprise pain point β Chrome policy delivery on modern cloud-only managed devices where ADMX-based Settings Catalog fails silently.







