top of page

Intune How to: Dynamic Registry Configuration Based on Entra ID Group Membership

  • Writer: Sebastian F. Markdanner
    Sebastian F. Markdanner
  • Nov 18, 2024
  • 11 min read

Updated: Nov 20, 2024

While I work on the next part of my Identity Governance series, I’ve got a configuration gem to share.

Advanced multi-screen setup displaying real-time registry configuration analytics for security groups in Intune.

As a firm believer in leaving servers as a footnote in our tech history, I often guide my clients on their journey to a cloud-only environment. A top priority in this journey? Migrating endpoints out of the on-prem domain—because, frankly, there’s rarely a need to keep endpoints tethered to a traditional domain anymore. This transition also tightens security, enabling server isolation and reducing our attack surface.


Table of content


The Mission Brief: Streamlining Intune Registry Configuration

Recently, I was tasked with helping a client move their endpoints from a domain-joined setup to an Entra-joined configuration. This involved combing through their existing Group Policy Objects (GPOs) to ensure a smooth migration.


This client had a long history with GPOs, resulting in a tangled web of settings: many GPOs clashed, became obsolete, or were simply inefficient for the task at hand. While I was able to discard about 90% of these GPOs—either because they weren’t needed for the Entra Joined clients or were better handled in Intune—one stubborn legacy app required extra attention. This app, an Outlook add-in for email archiving, relies on a registry setting in the HKCU hive to determine specific archive locations. Previously, each location was managed by a GPO, with each GPO scoped to a different user group.


Simply lifting these GPOs into Intune would have been a headache both for management and performance. Plus, it risked performance slowdowns and management complexity if a user were ever added to multiple groups.


The Question

Due to the above, the question I was trying to get a handle on ended up as such:

How do I manage dynamic changing registry key / values in the HKCU hive based on Entra ID Group Memberships?



Answer Unlocked

As a rule, I don’t port GPOs to Intune—I start fresh whenever possible. So I got to thinking: how could I implement a cleaner solution?


After talking through the case with some of my knowledgeable colleagues, sketching out potential workflows, and testing several ideas, I finally landed on a solution worth sharing.


The Powershell Script solution

Both scripts used for this solution are available on my GitHub


Required Permissions

To run these scripts, you’ll need an App Registration in Entra ID with the following API permissions:

  • User.Read.All

  • Group.Read.All

Installation Script

<#
.SYNOPSIS
    Fetches group membership information for the currently logged-in user from Microsoft Graph API and configures registry settings based on group memberships.

.DESCRIPTION
    This script leverages the Microsoft Graph API to retrieve the group memberships of the logged-in user within Microsoft Entra ID. 
    Based on these group memberships, the script applies specific registry values associated with each group. Additionally, the 
    user's SID is dynamically retrieved and saved to a specified file location, allowing for reuse for detection. 
    Static registry values are also applied regardless of group membership.

.NOTES
    Author: Sebastian Flæng Markdanner
    Website: https://chanceofsecurity.com
    Email: Sebastian.Markdanner@chanceofsecurity.com
    Version: 1.1
    Date: 12-11-2024
#>

# Configurable Variables. Modify as needed

$tenantId           = "YOUR_TENANT_ID"                              # Microsoft tenant ID
$clientId           = "YOUR_CLIENT_ID"                              # Microsoft client ID for API access
$clientSecret       = "YOUR_CLIENT_SECRET"                          # Secret for API authentication
$resource           = "https://graph.microsoft.com"                 # Microsoft Graph API resource URL
$graphApiUrl        = "https://graph.microsoft.com/v1.0"            # Microsoft Graph API base URL
$SIDFilePath        = "C:\ProgramData\Microsoft\<company>\SID.txt"  # Path to save the user's SID
$domain             = "@yourdomain.com"                             # Domain suffix to complete UPN
$regPath            = "Software\ExampleApp"                         # Registry path for setting values

# Group-to-FilePath Mappings based on user group membership. Modify as needed.
$GroupToFilePath = @{
    'Group01' = "D:\Example\Path"
    'Group02' = "H:\Example\Path"
    'Group03' = "D:\Example\Path\SubPath"
    'Group04' = "D:\Example"
    'Group05' = "P:\Example\Path"
}

# Static Registry Values to be Set
$StaticRegValues = @{
    "StaticKey01" = "true"
    "StaticKey02" = "true"
    "StaticKey03" = "false"
    "StaticKey04" = "false"
    "StaticKey05" = "false"
}

# Function to load the user's registry hive, set values, and unload it using reg.exe
function Set-UserRegistryValues {
    param (
        [string]$UserSID,
        [string]$FilePath
    )

    # Retrieve the user profile path dynamically based on SID
    $userProfilePath = Get-UserProfilePath -UserSID $UserSID
    if (-not $userProfilePath) {
        Write-Log "Unable to retrieve user profile path for SID: $UserSID"
        return
    }

    # Define path to the user's NTUSER.DAT file, used to load their registry hive
    $regHivePath = "$userProfilePath\NTUSER.DAT"

    # Load the user registry hive into HKEY_USERS if not already loaded
    if (-not (Test-Path "HKU\$UserSID")) {
        reg.exe load "HKU\$UserSID" $regHivePath
    }

    # Registry key path for user settings under HKEY_USERS
    $RegKey = "HKU\$UserSID\$regPath"

    # Initialize a new hashtable for registry values and add static values from $StaticRegValues
    $RegValues = @{}
    $StaticRegValues.GetEnumerator() | ForEach-Object { $RegValues[$_.Key] = $_.Value }
    
    # Add the dynamic file path based on group membership to the registry values
    $RegValues["DynamicFilePath"] = $FilePath

    # Ensure the registry key exists
    reg.exe add "$RegKey" /f

    # Create or modify each registry property using reg.exe, based on the values in $RegValues
    foreach ($item in $RegValues.GetEnumerator()) {
        $name = $item.Key
        $value = $item.Value

        # Check if the registry value exists before adding or modifying
        Write-Host "Checking if registry key: $name exists"
        $regQuery = reg.exe query "$RegKey" /v $name 2>&1

        if ($regQuery -like "*ERROR*") {
            # If the registry value does not exist, add it
            Write-Host "Adding registry key: $name with value: $value"
            reg.exe add "$RegKey" /v $name /t REG_SZ /d $value /f
        } else {
            # If the registry value exists, modify it
            Write-Host "Modifying existing registry key: $name with new value: $value"
            reg.exe add "$RegKey" /v $name /t REG_SZ /d $value /f
        }
    }

    Write-Log "Registry values applied successfully for FilePath: $FilePath"
}

# Function to retrieve the UPN (User Principal Name) of the currently logged-in user
function Get-LoggedInUserUPN {
    # Use WMI to get the currently logged-in user's information
    $loggedInUser = (Get-WmiObject -Class Win32_ComputerSystem | Select-Object -ExpandProperty UserName)
    
    if ($loggedInUser -and $loggedInUser -like "*\*") {
        # If username is in domain\username format, convert to UPN format
        $loggedInUser = $loggedInUser.Split('\')[1] + $domain
    }
    
    return $loggedInUser
}

# Function to retrieve the SID of the logged-on user
function Get-LoggedOnUserSID {
    # Retrieve all logged-on users
    $loggedOnUsers = Get-LoggedOnUser
    
    # Filter to get the active or console session user
    $activeUser = $loggedOnUsers | Where-Object { $_.IsActiveUserSession -eq $true }
    
    if ($activeUser) {
        return $activeUser.SID
    } else {
        Write-Log "No active user session found."
        return $null
    }
}

# Function to save the user's SID to a specified file path
function Save-SIDToFile {
    param (
        [string]$UserSID,
        [string]$filePath
    )

    try {
        # Extract the directory path from the file path
        $directoryPath = Split-Path -Path $filePath -Parent

        # Check if the directory exists, and create it if necessary
        if (-not (Test-Path -Path $directoryPath)) {
            Write-Log "Directory does not exist, creating: $directoryPath"
            New-Item -Path $directoryPath -ItemType Directory -Force
        }

        # Save the SID to the specified file
        Write-Log "Saving SID to file: $filePath"
        $UserSID | Out-File -FilePath $filePath -Force
        Write-Log "Successfully saved SID: $UserSID to $filePath"
    } catch {
        Write-Log "Failed to save SID to file: $($_.Exception.Message)"
    }
}

# Retrieve Microsoft Graph API token using service principal credentials
$token = Get-GraphToken -tenantId $tenantId -clientId $clientId -clientSecret $clientSecret -resource $resource

# Retrieve the UPN of the currently logged-in user
$userPrincipalName = Get-LoggedInUserUPN

if (-not $userPrincipalName) {
    Write-Log "No logged-in user found, exiting."
    exit 1
}

Write-Log "Retrieved UPN: $userPrincipalName"

# URL Encode the UPN to include in API requests
$encodedUpn = [System.Web.HttpUtility]::UrlEncode($userPrincipalName)

# Query Microsoft Graph for the user's group memberships
$headers = @{
    Authorization = "Bearer $token"
}

$graphApiUrlWithUpn = "$graphApiUrl/users/$encodedUpn/memberOf"
Write-Log "Graph API URL: $graphApiUrlWithUpn"

try {
    $graphResponse = Invoke-RestMethod -Uri $graphApiUrlWithUpn -Headers $headers -Method Get
    $userGroups = $graphResponse.value | ForEach-Object { $_.displayName }
    Write-Log "User is a member of the following groups: $($userGroups -join ', ')"
} catch {
    Write-Log "Failed to retrieve group memberships from Microsoft Graph. Error: $($_.Exception.Message)"
}

# Retrieve the SID of the logged-in user and save it to a file
$userSID = Get-LoggedOnUserSID
Write-Log "Retrieved user SID: $userSID"

# Save SID to the specified file if it was successfully retrieved
if ($userSID) {
    Save-SIDToFile -UserSID $userSID -FilePath $SIDFilePath
    Write-Log "SID saved to $SIDFilePath"
} else {
    Write-Log "No SID was found to save."
}

# Loop through the user's groups and apply the appropriate registry values
foreach ($group in $userGroups) {
    if ($GroupToFilePath.ContainsKey($group)) {
        $filePath = $GroupToFilePath[$group]
        Write-Log "Applying registry changes for group: $group, FilePath: $filePath"

        # Set the registry values for the application based on the user's SID
        Set-UserRegistryValues -UserSID $userSID -FilePath $filePath
    }
}

This script uses the Microsoft Graph API to retrieve the currently logged-in user’s group memberships within Microsoft Entra ID and configures registry settings accordingly, in the HKU Hive. The script dynamically retrieves the user’s SID, saving it for detection, and applies a set of static registry values for all users. NOTE: The script is build around PSAppDeploymentToolkit, as it reuses some of the functions and methods from the toolkit.

Detection Script

<#
.SYNOPSIS
    Detects if registry settings match the expected configuration based on the group memberships of the currently logged-in user, fetched from Microsoft Graph API.

.DESCRIPTION
    This script retrieves the group memberships of the currently logged-in user from Microsoft Entra ID using Microsoft Graph API. 
    It checks if the registry settings match the expected values for these groups, verifying the `FilePath` registry value for the user profile based on membership. 
    The script dynamically retrieves the user's SID, loading their registry hive if needed, and logs output for successful verification or mismatches. 
    A static set of registry values is also checked and compared for all users.

.NOTES
    Author: Sebastian Flæng Markdanner
    Website: https://chanceofsecurity.com
    Email: Sebastian.Markdanner@chanceofsecurity.com
    Version: 1.3
    Date: 12-11-2024
#>

function Log {
    # Logs a message with a timestamp
    [CmdletBinding()]
    param (
        [Parameter(Mandatory=$false)] [String] $message
    )

    $ts = get-date -f "yyyy/MM/dd hh:mm:ss tt"
    Write-Output "$ts $message"
}

# Configurable Variables
$tenantId       = "YOUR_TENANT_ID"                                          # Microsoft tenant ID
$clientId       = "YOUR_CLIENT_ID"                                          # Microsoft client ID for API access
$clientSecret   = "YOUR_CLIENT_SECRET"                                      # Secret for API authentication
$resource       = "https://graph.microsoft.com"                             # Microsoft Graph API resource URL
$graphApiUrl    = "https://graph.microsoft.com/v1.0"                        # Microsoft Graph API base URL
$SIDFilePath    = "C:\ProgramData\Microsoft\<company>\SID.txt"              # Path to save the user's SID
$domain         = "@yourdomain.com"                                         # Domain suffix to complete UPN
$regPath        = "Software\ExampleApp"                                     # Registry path for checking values
$logPath        = "C:\ProgramData\Microsoft\IntuneManagementExtension\Logs" # Directory for log file
$logFile        = "PS-<LOGFILEPATH>-v1.0.log"                               # Log filename

# Start logging
Start-Transcript -Path "$($logPath)\$logFile" -Append

# Group-to-FilePath mappings based on user group membership
$GroupToFilePath = @{
    'Group01' = "D:\Example\Path"
    'Group02' = "H:\Example\Path"
    'Group03' = "D:\Example\Path\SubPath"
    'Group04' = "D:\Example"
    'Group05' = "P:\Example\Path"
}

# Function to read the user's SID from a specified file
function Get-SIDFromFile {
    param ([string]$SIDFilePath)

    if (Test-Path $SIDFilePath) {
        $SID = Get-Content -Path $SIDFilePath -ErrorAction Stop
        if (-not [string]::IsNullOrEmpty($SID)) {
            return $SID.Trim()
        } else {
            Log "SID file is empty."
            exit 1
        }
    } else {
        Log "SID file not found at: $SIDFilePath"
        exit 1
    }
}

# Function to obtain an access token for Microsoft Graph API
function Get-GraphToken {
    param (
        [string]$tenantId,
        [string]$clientId,
        [string]$clientSecret,
        [string]$resource
    )

    $body = @{
        grant_type    = "client_credentials"
        client_id     = $clientId
        client_secret = $clientSecret
        scope         = "$resource/.default"
    }

    $response = Invoke-RestMethod -Method Post -Uri "https://login.microsoftonline.com/$tenantId/oauth2/v2.0/token" -ContentType "application/x-www-form-urlencoded" -Body $body
    return $response.access_token
}

# Function to retrieve the UPN (User Principal Name) of the currently logged-in user
function Get-LoggedInUserUPN {
    $loggedInUser = (Get-WmiObject -Class Win32_ComputerSystem | Select-Object -ExpandProperty UserName)
    if ($loggedInUser -and $loggedInUser -like "*\*") {
        $loggedInUser = $loggedInUser.Split('\')[1] + $domain
    }
    return $loggedInUser
}

# Function to load the user's registry hive if necessary
function Load-UserRegistryHive {
    param ([string]$UserSID)

    if (-not (Test-Path "HKU\$UserSID")) {
        $userProfilePath = (Get-WmiObject -Class Win32_UserProfile | Where-Object { $_.SID -eq $UserSID }).LocalPath
        $regHivePath = "$userProfilePath\NTUSER.DAT"

        if (Test-Path $regHivePath) {
            reg.exe load "HKU\$UserSID" $regHivePath
            Log "User registry hive loaded for SID: $UserSID"
        } else {
            Log "Could not find NTUSER.DAT for user: $UserSID"
            exit 1
        }
    } else {
        Log "User registry hive already loaded for SID: $UserSID"
    }
}

# Retrieve Microsoft Graph API token
$token = Get-GraphToken -tenantId $tenantId -clientId $clientId -clientSecret $clientSecret -resource $resource

# Retrieve the current logged-in user's UPN
$userPrincipalName = Get-LoggedInUserUPN

if (-not $userPrincipalName) {
    Log "No logged-in user found, exiting."
    exit 1
}

Log "Retrieved UPN: $userPrincipalName"

# URL Encode the UPN for use in API requests
$encodedUpn = [System.Web.HttpUtility]::UrlEncode($userPrincipalName)

# Query Microsoft Graph for group memberships of the user
$headers = @{
    Authorization = "Bearer $token"
}
$graphApiUrlWithUpn = "$graphApiUrl/users/$encodedUpn/memberOf"

try {
    $graphResponse = Invoke-RestMethod -Uri $graphApiUrlWithUpn -Headers $headers -Method Get
    $userGroups = $graphResponse.value | ForEach-Object { $_.displayName }
    Log "User is a member of $userGroups"
} catch {
    Log "Failed to retrieve group memberships from Microsoft Graph. Error: $_"
    exit 1
}

# Retrieve user's SID from the SID file
$userSID = Get-SIDFromFile -SIDFilePath $SIDFilePath

if (-not $userSID) {
    Log "No SID was found in the file, exiting."
    exit 1
}

Log "Retrieved User SID from file: $userSID"

# Load the user's registry hive if required
Load-UserRegistryHive -UserSID $userSID

# Check if the registry key for the application exists and matches expected values
try {
    $regKeyExists = Get-ItemProperty -Path "Registry::HKU\$userSID\$regPath" -ErrorAction Stop
    Log "Registry key exists: HKU\$userSID\$regPath"

    # Retrieve and verify the FilePath value from the registry
    $regFilePath = $regKeyExists.FilePath
    $expectedFilePath = $null
    $groupMatched = $false

    # Determine the expected file path based on the user's group memberships
    foreach ($group in $userGroups) {
        if ($GroupToFilePath.ContainsKey($group)) {
            $expectedFilePath = $GroupToFilePath[$group]
            $groupMatched = $true
        }
    }

    # Verify if the registry FilePath matches the expected file path
    if ($regFilePath -and $groupMatched -and $regFilePath -eq $expectedFilePath) {
        Log "Registry key and FilePath are correctly set."
        Write-Output "Success!"
    } else {
        Log "Mismatch in FilePath. Expected: $expectedFilePath, Found: $regFilePath."
        exit 1
    }

} catch {
    Log "Registry key does not exist: HKU\$userSID\$regPath. Error: $_"
    exit 1
}

# Successful validation
Log "Validation successful."
Write-Output "Success!"
Stop-Transcript
exit 0

The detection script verifies that the registry settings match the expected configuration based on group memberships. It fetches group memberships from Entra ID and checks the FilePath registry value for each user profile, dynamically retrieving the user’s SID to access their registry hive if needed. Static registry values are also checked across users.


NOTE: As this is not a RunOnce scenario, it bypasses the Active Setup registry key and instead targets the HKU Hive for registry modifications, setting values directly for the user.


Deployment Options

Deploying this script can be handled in multiple ways, specifically through:

  1. Platform Script – This would let the script run once, which doesn’t work for this solution.

  2. Remediation Script – This could work as it allows the script to run at regular intervals (as often as hourly) and verifies that the current registry key aligns with the Entra group membership. Strong contender however as I reuse logic from PSADT, which isn't a possibility for Remediation Scripts.

  3. Win32 Application – Deploying the script as a Win32 application means it runs during synchronization, which happens every 8 hours by default, but can be enforced either locally or via Intune. This approach offers full Win32 capabilities, including requirements, detection, supersedence, and dependencies, and is the only option possible for this script solution.


When packaging the script using PSAppDeploymentToolkit (PSADT), you have the option to embed the script within the main Deploy-Application.ps1 file or place it in the Files folder and call it from there.


Below is an example of how to reference the script in PSADT by adding a line to the installation phase of Deploy-Application.ps1:

PSAppDeploymentToolkit script showcasing PowerShell code for setting registry keys based on Entra ID security group memberships.

Once packaged with PSADT, the app needs to be converted to an .intunewin file using the Microsoft Win32 Content Prep Tool, readying it for Intune deployment.


Deploying via Intune: Step-by-Step

Step 1: Open Intune.microsoft.com and navigate to Apps > Windows.

Microsoft Intune admin center showing the Windows apps overview page for managing applications and installation status.

Step 2: Click “+ Add” and select “Windows app (Win32)” on the next blade.

Microsoft Intune interface for adding a new Windows app, highlighting the process to upload Intunewin files.
Intune app type selection menu with Windows app (Win32) highlighted for deploying registry configuration scripts.

Step 3: Select your .intunewin file for the application.

Uploading the Deploy-Application.intunewin file in Microsoft Intune to automate registry key configuration based on group membership.

Step 4: Configure a meaningful app name and description. Including the app version is useful for testing as it updates in the company portal when a sync is run, making it easy to ensure testing with the newest version deployed.

Intune application information page for configuring a group-based dynamic registry changer app, showing fields for app details.

Side 5: Set your install and uninstall commands. For this app, we don’t need the uninstall command.

Configuring install and uninstall commands in Intune for a PowerShell-based dynamic registry configuration application.

Step 6: Configure any necessary requirements based on your environment.

Intune application requirements page, set to target 64-bit Windows 11 (21H2) systems for the registry changer app

Step 7: Choose a custom detection script and add the provided detection script.

Intune detection rules setup using a custom PowerShell script to verify registry changes based on Entra ID group membership.

Step 8 (Optional): Configure supersedence or dependencies as needed.

Configuring supersedence and dependencies for Intune app deployment with registry settings based on security groups.

Step 9: Assign the app to the intended scope. In this case, I deployed it to all users, scoped to Entra-joined devices via a device filter.

Setting required group assignments for Entra-joined devices in Intune for registry key deployment.

Once deployed, the app will start setting registry keys based on the user’s group memberships in Entra ID.



Conclusion: A Fresh Take on Registry Changes

What We’ve Learned

Implementing dynamic registry changes based on Entra ID group memberships in a cloud-only environment isn’t always straightforward. Traditional GPO methods don’t translate seamlessly to the cloud, and this process illustrates the complexity involved. However, with a bit of PowerShell wizardry and creativity, it’s absolutely doable. This solution offers a flexible, scalable approach for managing registry changes dynamically—without the headaches of legacy GPOs


And now for a quick laugh, or at least a puff of air, here's another bad joke

How does a computer get drunk?

It takes screenshots! 😎


Try It Out!

If you’re in the process of moving away from legacy setups, this approach could save you hours of manual configuration and troubleshooting. Give it a shot in your environment, and see if it simplifies your migration process.


Stay Connected!

If you found this guide helpful, explore the rest of my blog for more insights and tutorials on cloud management and identity governance. Bookmark this page and check back often—I’m regularly sharing tips and solutions for navigating the challenges of modern IT. Got questions or suggestions? Reach out or leave a comment! Let’s make the cloud migration journey easier, one post at a time.

Comments

Rated 0 out of 5 stars.
No ratings yet

Add a rating
bottom of page