Skip to main content

DUDE Under the Hood: Managed Identity Setup and Permissions

The Problem
#

Most user-to-device sync scripts in Intune environments run under a service account with GroupMember.ReadWrite.All and zero guardrails. The script has the keys to every group in the tenant. It runs on a timer. Nobody reviews what it does. When it breaks, it breaks at 2 AM and empties a group that controls Conditional Access exclusions.

I built DUDE Manager to solve this properly. The introductory post covers what it does. This post covers how it does it — the architecture, the permission model, the runtime modules, and the design decisions that keep it from becoming the next incident.

Solution Overview
#

DUDE Manager separates concerns into two planes:

  • Control plane — a PowerShell WPF GUI on the operator’s workstation. Read-only Graph access. Manages configuration, deploys infrastructure, monitors execution.
  • Execution plane — an Azure Function running under a system-assigned Managed Identity. This is the only thing that writes to Entra ID groups.

The operator cannot accidentally modify group membership from the GUI. The runtime cannot be configured without the GUI. Neither plane has more permissions than it needs.

Prerequisites
#

  • PowerShell 7.4+
  • Az PowerShell modules (Az.Accounts, Az.Resources, Az.Storage, Az.Websites, Az.Functions, Az.OperationalInsights, Az.ApplicationInsights)
  • AzTable module
  • Microsoft.Graph.Authentication, Microsoft.Graph.Applications, Microsoft.Graph.Groups, Microsoft.Graph.Identity.DirectoryManagement
  • Windows 10/11 (WPF requirement for the GUI)
  • Azure subscription with Contributor + Owner on the target Resource Group (Owner revocable after initial deployment)
  • Privileged Role Administrator in Entra ID (for assigning MI permissions — also revocable post-deployment)

Implementation
#

The Two-Tier Permission Model
#

This is the part most homegrown scripts get wrong. They use one identity for everything.

GUI user permissions (delegated, read-only):

Group.Read.All                    # Validate groups exist
Application.Read.All              # Verify MI permissions
AdministrativeUnit.Read.All       # Validate admin units (optional)

Three scopes. All read-only. The GUI user’s infrastructure operations — deploying the Function App, writing to the config table — go through Azure RBAC, not Graph. This means a GUI operator with Reader on the Resource Group can monitor everything but deploy nothing.

Managed Identity permissions (application, scoped writes):

# Core — always assigned
Device.Read.All
DeviceManagementManagedDevices.Read.All
Group.Read.All
GroupMember.ReadWrite.All          # The only write scope for groups
User.Read.All

# Optional — only if features are enabled
AdministrativeUnit.ReadWrite.All   # Admin Unit sync
Machine.ReadWrite.All              # Defender for Endpoint tagging (Note: This is a WindowsDefenderATP API permission, not a Microsoft Graph permission. Grant it on the WindowsDefenderATP resource application, not on Microsoft Graph.)

The MI gets GroupMember.ReadWrite.All — which is tenant-wide. Microsoft Graph does not support resource-scoped application permissions for this. That is an accepted architectural constraint. The mitigation is the prefix allowlist, which I will get to.

The 12 Runtime Modules
#

The Azure Function runtime is not a single script. It is 12 purpose-built PowerShell modules:

ModuleResponsibility
DUDE.OrchestrationTop-level sync cycle coordinator
DUDE.ConfigReads and validates configuration from Azure Table
DUDE.AuthManaged Identity token acquisition
DUDE.TableStorageAzure Table read/write via REST + MI tokens
DUDE.GraphClientCustom Graph client with retry, backoff, jitter, pagination, and $batch
DUDE.DataFetchResolves user group membership and device ownership
DUDE.DiffEngineCalculates add/remove deltas per group
DUDE.GroupSyncExecutes group membership changes
DUDE.AdminUnitSyncSyncs users and devices into Administrative Units
DUDE.DefenderClientDefender for Endpoint API client
DUDE.DefenderSyncApplies machine tags in Defender
DUDE.LoggingStructured logging to Application Insights

Each module owns one concern. The Graph client, for example, handles pagination (@odata.nextLink), exponential backoff on 429/503/504, and batch operations — because the runtime processes thousands of objects per run, unlike the GUI which makes maybe 10 Graph calls per session.

The runtime has zero dependency on the Az PowerShell SDK. It uses REST-based table storage with MI bearer tokens. This is a deliberate design decision — the Function App should not need a 200MB module just to read a config table.

Fail-Closed Design
#

This is the core safety principle. When something is uncertain, the system stops.

Prefix allowlists (DUDE_ALLOWED_GROUP_PREFIXES):

Every sync cycle validates that every target group name starts with an allowed prefix. If a mapping references Global-Admins-Devices and your prefix list is DUDE-Prod-,DUDE-Staging-, the row is rejected. Not skipped silently — rejected with an error.

If the prefix setting itself is missing from the Function App configuration, all rows are rejected. The entire run halts. This is fail-closed, not fail-open.

The same logic applies to Admin Unit prefixes and Defender Tag prefixes, with a nuance: if the allowlist setting exists but a specific value does not match, the optional field is stripped from the row and the core group sync continues. If the allowlist setting is missing entirely, all rows with that field are rejected.

Blast radius limiter (DUDE_MAX_REMOVAL_PERCENT):

Each sync cycle enforces a maximum removal percentage per group. Default: 25%.

If a run calculates that 40% of a group’s members should be removed — because a source group was misconfigured, or an upstream directory change happened — the removals are blocked. The anomaly is logged. Nothing destructive happens.

Per-group overrides exist for small groups where 25% means “one device.”

Debug mode default:

Every new deployment starts in Debug mode. Debug mode calculates all changes — adds, removes, target groups — and logs them. No writes. You review the plan, validate intent, then switch to Production. This is not optional. It is the default.

12-Step Infrastructure Validation
#

DUDE Manager validates infrastructure across 12 sequential steps before anything runs in production:

  1. Resource Group exists
  2. Storage Account exists
  3. Config table exists
  4. Log Analytics Workspace exists
  5. Application Insights exists
  6. App Service Plan exists
  7. Function App + base files deployed
  8. Managed Identity enabled + core Graph permissions assigned
  9. Optional Graph permissions (Admin Units) if enabled
  10. Optional Defender permissions if enabled
  11. Function files deployed + SHA256 integrity verified via ARTIFACTS.json
  12. All 19 required Function App Settings present and valid

Step 11 fetches every deployed file from the Azure Function via the Kudu API and compares its SHA256 hash against the local manifest. If someone modified a file in the portal, you will know.

Security Considerations
#

The GroupMember.ReadWrite.All reality: This is tenant-wide. If the MI token were extracted via SSRF or a compromised deployment, the attacker could modify any group. Mitigations: restrict Function App network access (VNet integration, IP restrictions), use a dedicated storage account, monitor MI sign-in logs.

Storage account access split: The GUI uses Reader and Data Access (which includes listkeys — broader than ideal). The MI uses Storage Table Data Contributor (data-plane only, no key access). Deploy DUDE into a dedicated storage account to limit blast radius.

Deployment permissions are revocable: Owner on the Resource Group and Privileged Role Administrator are only needed for initial deployment. After setup, revoke both. Ongoing operations need Contributor and Reader.

Key Takeaways
#

  • Separate control plane from execution plane. The thing that configures should not be the thing that writes.
  • Fail-closed is non-negotiable for automation that touches security groups.
  • Prefix allowlists are not security theater — they are the primary runtime control when Graph permissions are tenant-wide.
  • Debug mode as default prevents the “deploy and pray” pattern.
  • SHA256 integrity verification catches drift between what you deployed and what is running.

When to Use This / When Not To
#

Use DUDE Manager when:

  • You manage Intune at scale and need user-to-device group sync
  • You care about least privilege, audit trails, and operational safety
  • You are replacing a homegrown script that has outlived its guardrails
  • You want optional Admin Unit and Defender tag enrichment

Do not use DUDE Manager when:

  • You have a handful of static device groups that rarely change
  • You do not use user-based targeting at all
  • You need something running in 5 minutes — this deploys real Azure infrastructure with real permissions

GitHub
#

DUDE Manager on GitHub