Deploy the PIM Manager hybrid worker (PIM for Active Directory) in Azure
Written by the PIM Manager team for PIM Manager 2.4.576 · 2026-10-11
The hybrid worker brings PIM Manager’s just-in-time model to your on-premises Active Directory. When a person activates a PIM group in Entra ID, the worker puts their AD admin account into the matching AD group for as long as the activation lasts, and takes it out again afterwards. It can also create and maintain the AD admin accounts, mirror PIM groups to AD groups, and make per-server groups local administrators on your servers.
This guide builds the worker as an Azure VM in its own virtual network, peered to the network where your domain controllers are reachable. For a worker on your own on-premises server with Azure Arc, see Hybrid worker with Azure Arc.
Edition: PIM for Active Directory is a Pro feature (On-premises Active Directory). The worker’s jobs do nothing without a Pro licence on the environment.
What you get
| The machine | One Windows Server 2022 Azure Edition Core VM (default size Standard_B2s, 4 GiB), Trusted Launch (Secure Boot + vTPM), platform patching with hotpatch, in its own resource group and VNet. |
| Network | No public IP on the VM, ever. The subnet’s NSG denies all inbound traffic, including from peered networks. Outbound traffic (Microsoft Graph, the PIM database, the update source, Windows Update) leaves through a NAT gateway on the subnet, and Azure’s implicit default outbound access is switched off. |
| Management | Azure run-command only (an outbound agent channel). No RDP, no WinRM, nothing listening. The local administrator password is generated, used once at creation and discarded; break-glass is the portal’s Reset password. |
| Cloud identity | The VM’s system-assigned managed identity reaches the PIM database (read/write rows, never schema), Microsoft Graph (read only) and the PIM source store (read only, for self-update). No SAS, no PAT, no client secret, no certificate on the machine. |
| AD identity | Active Directory is written only by group managed service accounts (gMSAs), one per privilege tier. Their passwords are managed by AD; nothing is stored on the server. |
| Isolation switch | One command cuts the worker’s network reach (and restores it) without deleting anything. |
| Self-update | A daily task moves the worker to the version your environment’s update ring approves. It never moves backwards and never moves without a ring answer. |
The security model in one paragraph
The worker can change membership of your Tier-0 groups, so treat it as a Tier-0 machine, like a domain controller. That is why it has no public IP, accepts no inbound connections, is managed only through Azure run-command, and holds no password, secret or certificate. Restrict who holds Virtual Machine Contributor (or any right that allows run-command) on its resource group to your Tier-0 administrators: run-command executes as SYSTEM on the worker.
Never put a worker identity (its gMSAs, their PermissionGroups, or the worker computer account) into a PIM just-in-time group. The members of those groups are the people with an active elevation, and the membership sync removes anyone else. The worker would remove itself, or worse, keep standing access that PIM exists to prevent. The AD setup script never does this.
Modules and tools
- Azure side: REST only. Every Azure step in this guide calls Azure Resource Manager, Microsoft Graph and Azure SQL over REST with your browser sign-in (authorization code with PKCE, never device code) or an Invardia support session. No Azure CLI, no Az or Microsoft Graph PowerShell modules.
- AD side: LDAP through .NET. The AD setup script uses no ActiveDirectory PowerShell module. It uses
dsacls.exefor OU rights anddjoin.exefor the offline domain join. The only PowerShell modules used are the two the product owner approved for the AD / hybrid worker side:- the Windows GroupPolicy module, only to create the servers GPO the first time it does not exist;
Add-KdsRootKey(the in-box Kds module), only when you ask the script to create a missing KDS root key (-CreateKdsRootKey).
- On the worker: the installer uses the in-box Windows scheduled-task cmdlets (
Register-ScheduledTaskand related) to create its tasks, and installs PowerShell 7 from Microsoft’s signed MSI. No other module is installed.
Who runs what
| Step | Who | Where | Rights needed |
|---|---|---|---|
| 1. Infrastructure (VM, VNet, NSG, NAT gateway, peering, SQL VNet rule) | Azure administrator | Any Windows machine with the PIM Manager release package | Contributor on the worker’s subscription; Network Contributor on every VNet you peer to (the script creates both sides); Contributor on the PIM SQL server (VNet rule) |
| 2. Peering check / isolation switch | Azure administrator | Same | Network Contributor on both sides |
| 3. Outbound path (NAT gateway) | Azure administrator | Same (normally run for you by step 1) | Network Contributor on the worker’s resource group |
| 4. Active Directory preparation | AD administrator | A domain controller, or a domain-joined host with the AD DS tools (dsacls.exe) and the Group Policy Management tools |
Domain Admins. Creating a KDS root key needs Enterprise Admins (or Domain Admins of the forest root domain). |
| 5. Domain join of the worker | Azure administrator (runs it), with the blob from the AD administrator | Azure portal, run-command on the worker VM | Virtual Machine Contributor (run-command) on the worker |
| 6. Cloud access grants | Azure / Entra administrator | Windows PowerShell 5.1 or PowerShell 7 on a machine with the release package | Privileged Role Administrator (Graph application permissions); Owner or User Access Administrator on the source storage container; a SQL Entra administrator of the PIM database |
| 7. Install and configure the worker | Azure administrator | Azure portal, run-command on the worker VM | Virtual Machine Contributor (run-command) on the worker |
| 8. Naming and switch-on | PIM SuperAdmin | PIM Manager, Settings > Hybrid AD & workloads > Hybrid Active Directory | SuperAdmin in PIM Manager |
Where the scripts come from. All six scripts are in the PIM Manager release package under
.\tools\pim-hybrid-worker\ (Pro / trial: the signed package Install-Invardia downloaded for your installation).
The Azure-side scripts (New-PimHybridWorkerInfra, Set-PimHybridWorkerEgress, Set-PimHybridWorkerPeering,
Grant-PimHybridWorkerAccess) load the engine’s REST libraries from the package, so run them from the package
folder; they do not work as single downloaded files. Initialize-PimHybridWorkerAd.ps1 and
Install-PimHybridWorker.ps1 run on their own machine (the domain controller, the worker): use the published
single files from https://invardia.com/support/pim/ (each is one self-contained file), or run them from the package
folder – copying the package’s copy alone no longer works since the next release after 2.4.580 (it loads a shared
helper from the package). From that release, Deploy-PimHybridWorker.ps1 -Target Azure runs the whole sequence and
delivers the worker install through Azure run-command for you.
Every script supports -WhatIf (prints the plan, changes nothing) except Install-PimHybridWorker.ps1, which has
-PlanOnly instead (see step 7). Every script is read-before-write and idempotent: if a step fails, fix the cause and
run the same command again.
Step 0: Decide and collect
| Decision | Your value (example) | Parameter (script) |
|---|---|---|
| Tenant and subscription for the worker | <tenant-id> / <subscription-id> |
-TenantId -SubscriptionId (all Azure scripts) |
| Region: the customer decides; usually the PIM environment’s region | westeurope |
-Location (infra; default westeurope) |
| Resource group | rg-pim-hybrid |
-ResourceGroup (pass it to every Azure script: the default is rg-pimhybridwrk) |
| VM name (max 15 characters; it is also the AD computer name) | PIMHYBRIDWRK (the default) |
-VmName (infra, egress) / -WorkerComputerName (AD) |
| VM size | Standard_B2s (default, 4 GiB). Standard_B1ms (2 GiB) works but leaves little memory. |
-VmSize |
| Worker VNet range and subnet (must not overlap anything it peers to) | 10.221.16.0/24 / 10.221.16.0/27 (defaults) |
-AddressPrefix -SubnetPrefix |
| The VNet where the domain controllers are reachable (required), and the PIM environment’s VNet (optional) | /subscriptions/<sub>/resourceGroups/rg-identity/providers/Microsoft.Network/virtualNetworks/vnet-identity, .../vnet-pim |
-PeerVnetId (infra) |
| DNS servers for the worker VNet: your domain controllers | 10.0.1.4, 10.0.1.5 |
-DnsServers (infra) |
| The PIM database server | Resource id /subscriptions/<sub>/resourceGroups/rg-pim/providers/Microsoft.Sql/servers/sql-pim-contoso and FQDN sql-pim-contoso.database.windows.net |
-SqlServerId (infra), -SqlServer (grant, install) |
| PIM database name | PimPlatform (default) |
-SqlDatabase |
| Outbound path | NAT gateway (default) or your own firewall route | -Egress NatGateway / -Egress None (infra) |
The PIM source store container (holds pim-src-<version>.tar.gz and channel.json for your environment) |
Resource id /subscriptions/<sub>/resourceGroups/rg-pim/providers/Microsoft.Storage/storageAccounts/stpimcontoso/blobServices/default/containers/pim-src and URL https://stpimcontoso.blob.core.windows.net/pim-src |
-SourceContainerId (grant), -SourceContainerUrl (install) |
| Update ring of your PIM environment | 3 (the worker must follow the same ring as the environment) |
-Ring (install) |
| OU for the worker computer account | OU=Tier0 Servers,DC=contoso,DC=local |
-ComputerOu (AD) |
| OU for the gMSAs and their groups | OU=Service Accounts,OU=Tier0,DC=contoso,DC=local |
-GmsaOu (AD) |
| OU where PIM creates and maintains AD groups | OU=PIM Groups,OU=Tier0,DC=contoso,DC=local |
-GroupsOu (AD), then the OU for new AD groups setting |
| OU(s) for AD admin accounts PIM creates | OU=Admin Accounts,OU=Tier0,DC=contoso,DC=local |
-AdminAccountsOu (AD) |
| Should PIM manage membership of protected groups (Domain Admins, Enterprise Admins, Schema Admins and their nesting)? | yes / no | -GrantProtectedGroupMembership (AD) |
| Server onboarding (per-server local administrator groups)? Which server OUs? | yes, OU=Servers,DC=contoso,DC=local |
-ServerGmsaName -ServerOu (AD), -ServerGmsaName (install) |
gMSA names (max 15 characters, pattern gMSA-<code>-L<level>-T<tier>) |
gMSA-PIM-L1-T0 (default), gMSA-PIM-L2-T1 |
-GmsaName -ServerGmsaName |
Cost (West Europe, pay-as-you-go, list prices October 2026): B2s Windows about 41 USD, disk about 2.4 USD, NAT
gateway about 33 USD plus its public IP 3.65 USD: about 80 USD/month for the default build. With -Egress None
(your existing firewall route) the NAT gateway and its IP are not created.
Outbound allow-list (only if you choose -Egress None and route through your own firewall):
login.microsoftonline.com, graph.microsoft.com, management.azure.com, your SQL server (*.database.windows.net,
port 1433), the source storage account (<account>.blob.core.windows.net), Windows Update, and, once during step 7,
api.github.com and github.com / objects.githubusercontent.com (the PowerShell 7 MSI). The worker also needs to
reach your domain controllers (LDAP, Kerberos, DNS) and, for server onboarding, your servers.
Step 1: Build the Azure infrastructure
Sign in happens on the first call: a browser window opens for an account of <tenant-id>. Plan first:
cd <release package folder>
$infra = @{
SubscriptionId = '<subscription-id>'
TenantId = '<tenant-id>'
ResourceGroup = 'rg-pim-hybrid'
Location = 'westeurope'
VmName = 'PIMHYBRIDWRK'
PeerVnetId = @('/subscriptions/<sub>/resourceGroups/rg-identity/providers/Microsoft.Network/virtualNetworks/vnet-identity',
'/subscriptions/<sub>/resourceGroups/rg-pim/providers/Microsoft.Network/virtualNetworks/vnet-pim')
DnsServers = @('10.0.1.4', '10.0.1.5')
SqlServerId = '/subscriptions/<sub>/resourceGroups/rg-pim/providers/Microsoft.Sql/servers/sql-pim-contoso'
# Egress = 'None' # only when your network already routes this subnet's outbound traffic
}
.\tools\pim-hybrid-worker\New-PimHybridWorkerInfra.ps1 @infra -WhatIf
Then build:
$out = .\tools\pim-hybrid-worker\New-PimHybridWorkerInfra.ps1 @infra
$out # vmName, resourceGroup, principalId, subnetId, vnetId
What it builds, in order:
- the resource group;
- the NSG
nsg-<vmname>with one rule,DenyAllInbound(priority 4096), which overrides Azure’s default allow VNet inbound; - the VNet
vnet-pimhybridwrkwith one subnetsnet-worker(NSG attached, service endpointsMicrosoft.SqlandMicrosoft.Storage), DNS servers = your domain controllers; - a peering in both directions to every
-PeerVnetId(namedpimhybridwrk-to-<remote>and<remote>-to-pimhybridwrk), with VNet access on, forwarded traffic off, no gateway transit; - the outbound path: it calls
Set-PimHybridWorkerEgress.ps1(step 3) to create the NAT gateway, unless-Egress None; - the NIC without a public IP configuration (it refuses to continue if an existing NIC carries one) and the VM:
system-assigned managed identity, Trusted Launch, local administrator
pimlocalwith a generated password that is discarded; - a SQL VNet rule named
pimhybridwrkon the PIM SQL server, admitting the worker subnet (when-SqlServerIdis given).
Keep $out.principalId. It is the managed identity’s object id, the input for step 6.
Domain controllers on-premises, behind a hub gateway? The peerings are created with VNet access only. If your DCs are reached through the hub’s VPN or ExpressRoute gateway, add
-UseRemoteGateways -GatewayVnetId <hub-vnet-id>(the hub must be one of-PeerVnetId), plus-AllowForwardedTrafficwhen a firewall / NVA in the hub forwards the traffic. The script first checks that the hub really has a VPN / ExpressRoute gateway, then needs allow gateway transit on the hub side of the peering: it sets that itself only on a hub-side peering it created (<hub>-to-pimhybridwrk). If the hub side belongs to someone else, it changes nothing there, prints exactly what the hub owner sets (allowGatewayTransit = true), and leaves use remote gateways off until you run it again. The same switches work later:Set-PimHybridWorkerPeering.ps1 ... -State Connected -UseRemoteGateways -GatewayVnetId <hub>. Make sure the on-premises side routes the worker range10.221.16.0/24back. The isolation switch (step 2) keeps those flags as they are.
A private-only PIM database (public network access disabled). The SQL VNet rule applies to the server’s public endpoint only. The worker then reaches SQL through its private endpoint, and the worker asks your DCs for the name. Add
-PrivateSqlDnsZoneId <id of your existing privatelink.database.windows.net zone>: the script links that zone to the worker VNet (only the standard zone name is accepted; it never creates or links a zone named after your domain;-CreatePrivateDnsZone -PrivateDnsZoneResourceGroup <rg>creates the standard zone only if it is missing). It then prints the one conditional forwarder your AD DNS administrator adds on the DCs. PIM never writes AD DNS:Add-DnsServerConditionalForwarderZone -Name 'privatelink.database.windows.net' -MasterServers <address> -ReplicationScope Forest
- DCs that are Azure VMs:
<address>=168.63.129.16(Azure DNS, reachable only from inside Azure). Azure answers from the zones linked to the VNet of the DNS server that asks, so link the zone to the DCs’ VNet too:-PrivateDnsLinkVnetId <dc-vnet-id>.- DCs on-premises (reached over VPN / ExpressRoute):
168.63.129.16does not answer outside Azure. Create an Azure DNS Private Resolver with an inbound endpoint in a VNet linked to the zone (normally the hub,-PrivateDnsLinkVnetId <hub-vnet-id>), and forward to that endpoint’s IP (-DnsForwarderIp <ip>puts it into the printed command).Check on the worker:
Resolve-DnsName <server>.database.windows.netreturns theprivatelinkalias and a private address. (Forwarding the public zonedatabase.windows.netto the same address also works; theprivatelinkzone is the narrower choice.)
Verify: the run ends with [infra] done. and prints VM PIMHYBRIDWRK (Standard_B2s) managed identity <guid>.
In the portal, the VM’s Networking page shows no public IP and the NSG rule DenyAllInbound.
Step 2: Check the peering (and know the isolation switch)
The peerings were created in step 1. Show their state:
.\tools\pim-hybrid-worker\Set-PimHybridWorkerPeering.ps1 -SubscriptionId <subscription-id> -TenantId <tenant-id> `
-ResourceGroup rg-pim-hybrid -State Show
Each side prints access=True state=Connected.
This same script is your isolation switch. -State Disconnected sets allow virtual network access to off on
both sides of every peering of the worker’s VNet. Traffic stops at once; the peerings stay, so -State Connected
restores exactly the same topology. While disconnected the worker cannot reach a domain controller: its AD jobs
fail, loudly, in Jobs rather than act. SQL and Graph still work over its outbound path, which is how it reports that.
.\tools\pim-hybrid-worker\Set-PimHybridWorkerPeering.ps1 -SubscriptionId <subscription-id> -TenantId <tenant-id> `
-ResourceGroup rg-pim-hybrid -State Disconnected # or -State Connected
Optional: -VnetName if you changed the worker VNet’s name.
Step 3: The outbound path (NAT gateway)
Step 1 already ran this for you (-Egress NatGateway, the default). Run it yourself only when:
- you built with
-Egress Noneand now want the NAT gateway after all; or - you want to verify the worker’s outbound path at any time (the script ends with a check); or
- a worker built by an older release has a public IP on its NIC: the script migrates it.
.\tools\pim-hybrid-worker\Set-PimHybridWorkerEgress.ps1 -SubscriptionId <subscription-id> -TenantId <tenant-id> `
-ResourceGroup rg-pim-hybrid -VmName PIMHYBRIDWRK -WhatIf
Remove -WhatIf to apply. Optional parameters: -Location (default: the VNet’s region), -VnetName, -SubnetName
(default snet-worker), -NatGatewayName (default natgw-<vmname>), -PublicIpName (default pip-<vmname>).
What it does, in order:
- creates the NAT gateway (Standard, idle timeout 4 minutes);
- detaches any public IP from the VM’s NICs (the address itself is kept);
- makes that same address (or a new Standard static IP
pip-<vmname>) the NAT gateway’s outbound address, so an allow-list that names the address keeps working; - points the subnet at the NAT gateway and switches Azure’s default outbound access off (the NSG and service endpoints are kept);
- verifies: no public IP on any NIC of the VM, the subnet uses the NAT gateway.
When migrating an existing worker, it has no outbound path for about a minute between steps 2 and 4; its loops fail that pass and recover by themselves.
Verify: [egress] done: PIMHYBRIDWRK has NO public IP; outbound leaves through natgw-pimhybridwrk as <ip>; inbound stays denied by the NSG.
Give that outbound IP to anyone who allow-lists the worker.
Step 4: Prepare Active Directory (AD administrator)
Download the published single file Initialize-PimHybridWorkerAd.ps1 (invardia.com/support/pim/) to a domain controller (or a domain-joined admin host with dsacls.exe and
the Group Policy Management tools) and run it in an elevated PowerShell session as a Domain Admin.
4.1 What it creates
One identity per privilege tier. Each identity is three objects in -GmsaOu:
| Object | Purpose |
|---|---|
<name> |
the gMSA (AES256 only, 30-day password interval) |
<name>-PermissionGroup |
the gMSA is its only member. Every right is granted to this group, never to the account. |
<name>-PrincipalsAllowedAccess |
the computers allowed to retrieve the gMSA’s password: the worker computer |
| Identity (default name) | Runs | Rights granted to its PermissionGroup |
|---|---|---|
gMSA-PIM-L1-T0 (-GmsaName) |
AD admin accounts, the AD group mirror, the continuous just-in-time membership sync | On -GroupsOu: read the OU, create groups, write member / displayName / description on groups below it. On each -AdminAccountsOu: create users and full control of the user objects below it. With -GrantProtectedGroupMembership: write member on AdminSDHolder, the only way to reach protected groups (adminCount=1), because SDProp resets their permissions every hour. That makes this identity, and the worker, Tier 0. |
gMSA-PIM-L2-T1 (-ServerGmsaName, optional) |
server onboarding: makes per-server groups members of each server’s local Administrators | Local administrator on the servers below each -ServerOu, through a GPO (below). It has no rights in AD itself. |
The servers GPO (only with -ServerGmsaName and -ServerOu): a GPO named
PIM for AD: <ServerGmsaName>-PermissionGroup is local administrator (servers) (or -ServerAdminGpoName) with one
Restricted Groups “member of” entry: the server identity’s PermissionGroup joins BUILTIN\Administrators on every
computer below the OU. It adds only; it removes nobody. It is linked to each -ServerOu and takes effect at each
server’s next policy refresh (gpupdate /target:computer).
What it deliberately does not do:
- enable the AD Privileged Access Management optional feature (it is irreversible for the forest). The script detects it and reports it: with the feature on, just-in-time membership carries a time-to-live equal to the time left on the activation; without it, membership is plain and the sync removes it when the activation ends;
- put any identity into a PIM just-in-time group;
- grant anything on protected user accounts (
adminCount=1).
4.2 The KDS root key
A gMSA cannot exist without a KDS root key in the forest. The script checks for one and stops if there is none,
unless you pass -CreateKdsRootKey. That runs Add-KdsRootKey -EffectiveImmediately once (a forest-wide object, needs
Enterprise Admins). The key is usable only after it has replicated, about 10 hours later. Create it, wait, then
continue; or create it yourself in advance.
4.3 Run it
Plan first:
$ad = @{
WorkerComputerName = 'PIMHYBRIDWRK' # = -VmName from step 1
ComputerOu = 'OU=Tier0 Servers,DC=contoso,DC=local'
GmsaOu = 'OU=Service Accounts,OU=Tier0,DC=contoso,DC=local'
GmsaName = 'gMSA-PIM-L1-T0' # the default
GroupsOu = 'OU=PIM Groups,OU=Tier0,DC=contoso,DC=local'
AdminAccountsOu = @('OU=Admin Accounts,OU=Tier0,DC=contoso,DC=local')
GrantProtectedGroupMembership = $true # only if PIM should manage protected groups
ServerGmsaName = 'gMSA-PIM-L2-T1' # only for server onboarding
ServerOu = @('OU=Servers,DC=contoso,DC=local')
UsedBy = 'PIM Manager hybrid worker' # written into the gMSA descriptions
ProvisionOdj = $true # create the computer account + the join blob
# CreateKdsRootKey = $true # only if 4.2 applies
# TriggerSdProp = $true # apply AdminSDHolder now, not within the hour
}
.\Initialize-PimHybridWorkerAd.ps1 @ad -WhatIf
Then run it without -WhatIf.
Parameters in short:
| Parameter | Required | Meaning |
|---|---|---|
-WorkerComputerName |
yes | The worker’s computer name (= the VM name). |
-ComputerOu |
yes | Where the computer account is created (-ProvisionOdj). |
-GmsaOu |
yes | Where the gMSAs and their two groups each are created. |
-GroupsOu |
yes | Where PIM creates and maintains AD groups. |
-GmsaName |
no | Default gMSA-PIM-L1-T0. |
-AdminAccountsOu |
no | One or more OUs for AD admin accounts PIM creates. |
-GrantProtectedGroupMembership |
no | Write member on AdminSDHolder (Tier 0). -TriggerSdProp applies it at once on the PDC emulator. |
-ServerGmsaName, -ServerOu, -ServerAdminGpoName |
no | Server onboarding identity, the OUs whose servers it administers, the GPO name. -ServerGmsaName needs -ServerOu and must differ from -GmsaName. |
-ProvisionOdj |
no | Create the computer account with djoin /provision and print the offline domain-join blob. Refuses if the computer exists, unless -ReuseComputer (that resets its password). |
-CreateKdsRootKey |
no | See 4.2. |
-UsedBy |
no | Text written into each gMSA’s description. |
Every OU you name must already exist; the script stops on a missing one.
4.4 Output that feeds later steps
- With
-ProvisionOdjthe output contains the join blob between the linesODJ-BLOB-BEGINandODJ-BLOB-END. It starts withgz:(compressed so it fits in run-command’s 4 KB output if you run this script itself through run-command on an Azure DC). Hand it to the Azure administrator over a secure channel: it carries the computer’s initial password until step 7 rotates it. - The last lines print the exact gMSA names for step 7, for example
Install-PimHybridWorker.ps1 -GmsaName 'CONTOSO\gMSA-PIM-L1-T0' -ServerGmsaName 'CONTOSO\gMSA-PIM-L2-T1', and the OU to set in PIM Manager (step 8).
Verify: in Active Directory Users and Computers, -GmsaOu holds the gMSA(s) and their -PermissionGroup /
-PrincipalsAllowedAccess groups; the worker computer account is in -ComputerOu and is a member of each
-PrincipalsAllowedAccess group; with server onboarding, Group Policy Management shows the GPO linked to each
-ServerOu.
Step 5: Join the worker to the domain (run-command)
The worker has no inbound access, so everything on it runs through Azure run-command, as SYSTEM: Azure portal > the worker VM > Operations > Run command > RunPowerShellScript.
Not yet automated: there is no PIM Manager script that sends
Install-PimHybridWorker.ps1to the VM through run-command. Use the portal as shown here. The portal’s script box takes no separate parameters, so paste the script inside a script block and pass the parameters after it.
Paste this, with the full contents of Install-PimHybridWorker.ps1 between the braces and the blob from step 4:
& {
# <paste the full contents of Install-PimHybridWorker.ps1 here>
} -Phase Join -OdjBlob 'gz:H4sIAAAA...'
What it does: writes the blob to a temporary file, runs djoin /requestODJ, deletes the file, and reboots after 15
seconds (-NoReboot skips the reboot). If the VM is already joined it does nothing.
Verify: the output ends with offline domain join staged -- rebooting to complete it. After the reboot the VM’s
computer account in AD shows a recent logon.
Step 6: Grant the worker’s cloud access
Run in Windows PowerShell 5.1 or PowerShell 7, from the release package folder (under PowerShell 7, if the SQL
connection fails at the transport level, the script repeats the SQL statements once in Windows PowerShell 5.1 and says
so; the access token is passed on that process’s standard input, never on a command line). Feed it principalId from
step 1:
powershell.exe -File .\tools\pim-hybrid-worker\Grant-PimHybridWorkerAccess.ps1 `
-SubscriptionId <subscription-id> -TenantId <tenant-id> -PrincipalId <principalId from step 1> `
-SqlServer sql-pim-contoso.database.windows.net -SqlDatabase PimPlatform `
-SourceContainerId /subscriptions/<sub>/resourceGroups/rg-pim/providers/Microsoft.Storage/storageAccounts/stpimcontoso/blobServices/default/containers/pim-src `
-WhatIf
Remove -WhatIf to apply. What the managed identity gets, least privilege:
| Where | Grant | Why |
|---|---|---|
| PIM database | A contained database user (default name PIMHYBRIDWRK, -SqlUserName) created from the identity’s application id, in db_datareader + db_datawriter. Never db_ddladmin. |
The worker changes rows (scheduler state, run history, audit), never the schema. |
| Microsoft Graph | Application permissions, read only: Group.Read.All, User.Read.All, PrivilegedAssignmentSchedule.Read.AzureADGroup |
To read the active PIM-for-Groups members that the AD membership mirrors. |
| Source store | Storage Blob Data Reader on the one container you name | The worker fetches its code and channel.json with its own identity: no SAS, no PAT. |
Mail.Send is deliberately not granted. Without it, a new AD admin account whose initial password cannot be mailed
is held by the worker and reported, never created with a password nobody receives. Add -GrantMailSend if you want
the worker to create those accounts and mail the initial password.
A new Graph application permission takes about 25 minutes to reach the identity’s tokens. The Storage Blob Data Reader role must be in place before step 7 (Azure role assignments usually apply within a few minutes).
Verify: each line reports granted or already granted, and the run ends with [grant] done.
Step 7: Install and configure the worker (run-command)
After the reboot from step 5, run this through Run command > RunPowerShellScript again, with the script pasted between the braces. Use the gMSA names step 4 printed:
& {
# <paste the full contents of Install-PimHybridWorker.ps1 here>
} -Phase Configure -GmsaName 'CONTOSO\gMSA-PIM-L1-T0' -ServerGmsaName 'CONTOSO\gMSA-PIM-L2-T1' `
-SqlServer sql-pim-contoso.database.windows.net -SqlDatabase PimPlatform -TenantId <tenant-id> `
-SourceContainerUrl https://stpimcontoso.blob.core.windows.net/pim-src -Ring 3 -PlanOnly
| Parameter | Required | Meaning |
|---|---|---|
-GmsaName |
yes | DOMAIN\name of the Tier-0 gMSA. |
-ServerGmsaName |
no | DOMAIN\name of the server-onboarding gMSA. Leave it out and the main gMSA also onboards servers. |
-SqlServer, -SqlDatabase |
yes / no | The PIM database (default PimPlatform). |
-TenantId |
yes | Your tenant. |
-SourceContainerUrl |
yes | The URL of the same container you granted in step 6. |
-Ring or -Version |
one of them | The update ring your PIM environment follows; or a fixed version (N.N.N), which pins the worker. |
-PlanOnly |
no | Every job runs with -WhatIf: it plans and logs, and changes nothing in AD. Recommended for the first run. |
-AdServer |
no | A specific domain controller to talk to (default: the domain’s locator). |
-IntervalMinutes |
no | The safety-net tick (default 5). |
-Root |
no | Install folder (default C:\PIM). |
What -Phase Configure does, in order:
- checks the VM is domain-joined, then changes the machine password (
nltest /sc_change_pwd), so the join blob is worthless from now on, and deletes run-command’s downloaded script copies from the disk; - installs PowerShell 7 from the latest official release if missing, after checking the MSI’s Microsoft Authenticode signature;
- installs and tests each gMSA on the machine (purging the computer’s Kerberos tickets first so it sees its new group membership). A worker that cannot use its gMSA fails here, not at 03:00;
- grants Log on as a batch job to the worker gMSA(s) only (added to the existing holders);
- locks down
C:\PIM: inheritance off; only SYSTEM and Administrators can write the code the gMSAs run; the gMSAs can read it and write only toC:\PIM\logs; - fetches the PIM code your ring approves (
channel.jsonin the source container) with the VM’s managed identity, checks the archive and switchesC:\PIM\app\currentto it (the previous version is kept); - registers the scheduled tasks:
| Task | Runs as | Cadence | Jobs |
|---|---|---|---|
PIM Hybrid AD Sync |
Tier-0 gMSA | continuous (5 s pause between passes), restarted within a minute if it stops | hybrid-ad-sync: active PIM-for-Groups members to AD group membership, for the critical (non-server) groups |
PIM Hybrid AD Sync Servers |
Tier-0 gMSA | continuous | hybrid-ad-sync-servers: the same for the per-server groups, in parallel batches |
PIM Hybrid AD Changes |
Tier-0 gMSA | continuous | hybrid-ad-changes: AD accounts and AD groups the moment their definitions change |
PIM Hybrid Worker |
Tier-0 gMSA | every 5 minutes (safety net) | hybrid-ad-apply (AD admin accounts) and hybrid-ad-groups (the group mirror; hourly) |
PIM Hybrid Servers |
server gMSA (only with -ServerGmsaName) |
every 5 minutes | hybrid-ad-servers (hourly): per-server groups into each server’s local Administrators, add only |
PIM Hybrid Worker Update |
SYSTEM | daily 04:30 | moves the worker to the version its ring approves |
If the fetched version predates the hybrid worker, the tasks are created disabled and the daily update enables them once the ring approves a version that has it.
Go live: when the plan-only runs look right (step 9), run the same -Phase Configure command again without
-PlanOnly.
Verify: the output contains gMSA CONTOSO\gMSA-PIM-L1-T0$ with no error, PIM <version> is current, and ends with
done: 'PIM Hybrid Worker' every 5 min (safety net) + ... continuously, as CONTOSO\gMSA-PIM-L1-T0$, plus
PLAN ONLY (-WhatIf) if you used -PlanOnly.
If the gMSA test fails with is this computer in …-PrincipalsAllowedAccess?, the computer is not yet a member (or
the KDS root key is younger than 10 hours). Fix it in AD, restart the VM, and run -Phase Configure again.
Step 8: Naming and switch-on in PIM Manager (SuperAdmin)
The naming is yours. Every knob is one setting on Settings > Hybrid AD & workloads > Hybrid Active Directory (PIM for AD), each with its own help text. Blank means derived from your tenant’s naming conventions.
| Setting | What it decides |
|---|---|
| AD group marker | Which PIM groups are mirrored to AD (the marker in their name). |
| Pattern override | A full name pattern instead of the marker. |
| Cloud admin suffix / AD admin suffix | How a cloud admin account maps to its AD admin account. The sync only ever removes accounts carrying the AD admin suffix. |
| OU for new AD groups | Set it to -GroupsOu from step 4. Blank: missing AD groups are never created, only existing ones are kept in sync. Groups the worker creates are not deleted again when you clear it. |
| Per-server group format | The name of the per-server administrator groups. |
| Shared server-admins group | An optional group added to every onboarded server’s local Administrators. |
| Never remove | Members the sync must never remove. |
| Groups per batch / Batches at once | Throughput of the server-group sync (PowerShell 7). |
Verify: the page shows one status per worker job (working / needs attention, with the last check-in).
Step 9: Verify end to end
- Jobs view in PIM Manager:
hybrid-ad-sync,hybrid-ad-apply,hybrid-ad-groups(andhybrid-ad-servers) show the worker as live (or plan-only during the first run). The main engine leaves these jobs to the worker while its heartbeat is fresh. - Live view: Jobs >
hybrid-ad-sync> Live view follows every pass: per group the current and expected time-to-live, and each add, correction and removal. - On the worker (run-command):
Get-ChildItem C:\PIM\logs | Sort-Object LastWriteTime | Select-Object -Last 5andGet-Content C:\PIM\logs\sync-<yyyyMMdd>.log -Tail 40. - Network:
Set-PimHybridWorkerPeering.ps1 ... -State Show(step 2) andSet-PimHybridWorkerEgress.ps1 ...(step 3, its final check). - A real activation: activate a mirrored PIM group for a test admin; within seconds their AD admin account is a member of the AD group; when the activation ends, it is removed.
Updates
The daily PIM Hybrid Worker Update task reads channel.json for the worker’s ring and installs the approved version
when it differs: it fetches the archive with the managed identity, switches C:\PIM\app\current, and applies the new
version’s task layout. The continuous loops restart on the new code by themselves. It never rolls back unattended
and never moves without a ring answer. To run it at once: run-command Start-ScheduledTask -TaskName 'PIM Hybrid Worker Update'.
On-premises server with Azure Arc
Built, see Hybrid worker with Azure Arc (offline-tested; not yet proven on a live Arc server). In short:
- the server is connected to Azure with Azure Arc, and the Arc machine’s managed identity takes the place of the VM’s system-assigned managed identity for the PIM database, Microsoft Graph (read only) and the source store, with the same least-privilege grants;
- the same gMSA model in AD: one identity per tier, rights on the PermissionGroups, no password, secret or certificate on the server;
- no inbound connections: it is managed through Arc’s outbound agent channel, like run-command on the Azure VM;
- the same Tier-0 rule: treat the server like a domain controller.
Use that guide’s commands (Deploy-PimHybridWorker.ps1 -Target Arc); do not adapt the Azure VM steps in this guide to an
Arc server.
Rollback and removal
| Situation | What to do |
|---|---|
| A step failed | Fix the cause and re-run the same command. Every script checks what exists first. |
| Stop the worker acting at once | Set-PimHybridWorkerPeering.ps1 ... -State Disconnected (step 2). Its AD jobs fail visibly; nothing is written to AD. -State Connected restores. |
| Back to plan-only | Re-run step 7 with -PlanOnly. |
| Pause the worker | Run-command: Disable-ScheduledTask -TaskName 'PIM Hybrid Worker', and the same for PIM Hybrid AD Sync, PIM Hybrid AD Sync Servers, PIM Hybrid AD Changes, PIM Hybrid Servers. Note: the daily update re-enables tasks when it runs. Use the peering switch for a lasting stop. |
| Remove the worker completely | Remove-PimHybridWorker.ps1 – see Remove the worker below. |
Remove the worker
Remove-PimHybridWorker.ps1 removes the worker and only what PIM’s worker scripts created. Plan first; it changes
nothing without -Apply:
.\tools\pim-hybrid-worker\Remove-PimHybridWorker.ps1 -Target Azure -SubscriptionId <subscription-id> -TenantId <tenant-id> `
-SqlServerId <sql-server-id> -SourceContainerId <source-container-id> `
-PrivateSqlDnsZoneId <zone-id, if you linked one> -AdDomain CONTOSO -GmsaOu '<gMSA OU>' -ComputerOu '<computer OU>' -GroupsOu '<groups OU>'
# ...or -StatePath <the Deploy-PimHybridWorker.ps1 state file> to reuse every value given at deploy time
.\tools\pim-hybrid-worker\Remove-PimHybridWorker.ps1 ...same... -Apply
In this order it deletes: the identity’s Graph read permissions, its Storage Blob Data Reader assignment and its
database user (dropped only when it carries the identity’s id); the SQL VNet rule pimhybridwrk (only when it names the
worker subnet); the private DNS zone’s link to the worker VNet (links to other VNets and the zone itself are kept and
listed); the peerings it created on both sides (a peering someone else made is listed, never touched); then the VM with
its NIC and OS disk, the VNet, the NAT gateway and its public IP, the NSG, and the resource group if PIM created it and
it is empty. It recognises its own resources by the tag pim-hybrid-worker = <VM name> that the build puts on them. A
worker built before the tags existed: check the plan, then add -IncludeUntagged (accepts untagged resources with
exactly the build’s names in the worker’s resource group; never the resource group itself). A VNet that holds anything
else is kept, and so are its NAT gateway and NSG.
Active Directory is never changed by the script. It prints the AD administrator’s clean-up: take back the rights
of <GmsaName>-PermissionGroup with dsacls <dn> /R (groups OU, each admin-accounts OU, AdminSDHolder if it was
granted), unlink and delete the servers GPO (Remove-GPLink / Remove-GPO), then delete the gMSAs and their two
groups and the worker VM’s computer account with dsrm. Keep the KDS root key. Groups and admin accounts the worker
created stay; remove them yourself if you no longer want them.
The KDS root key and the AD Privileged Access Management feature (if you enabled it yourself) are forest-wide; leave them.