Skip to content
InvardiaInvardia
Why InvardiaProductsFeaturesPricingCommunity
Docs
All documentation
Product documentationPIM ManagerSecurityInsight
SettingsPIM Manager settingsSecurityInsight settingsEntra Policy Suite settings
ScriptsPIM Manager scriptsSecurityInsight scriptsEntra Policy Suite scriptsInvardia scripts
Error codesPIM Manager error codesSecurityInsight error codes
API and MCPPIM Manager APIPIM Manager MCP toolsSecurityInsight APISecurityInsight MCP tools
SecurityPIM Manager security packSecurityInsight security packEntra Policy Suite security pack
Network and designPIM Manager network guidesPIM Manager command wizardNetwork egressDesign your installationCalculate your cost
SupportSetup scriptsRemote support access
Search the docs
SupportAboutContactBook a demoCustomer portalDesign your installationFree Pro trial

PIM Manager guides

  1. PIM Manager on a private network: your options for DNS, routing, segmentation and policy
  2. Install PIM Manager in private (internal) mode from scratch
  3. Convert a public (external) PIM Manager environment to private (internal)
  4. Give PIM Manager your own address (custom host name)
  5. Deploy the PIM Manager hybrid worker (PIM for Active Directory) in Azure
  6. Deploy the PIM Manager hybrid worker (PIM for Active Directory) on your own server with Azure Arc

Command wizard

Convert a public (external) PIM Manager environment to private (internal)

Written by the PIM Manager team for PIM Manager 2.4.579 · 2026-10-10

The one-command way

Convert-PimToPrivate.ps1 runs the steps of this guide for you, in order, by calling the same scripts the manual steps below use. Each step is checked before the next one starts. Run it from the PIM Manager release package folder, preferably on the machine that ran the original install: the install’s saved parameters (the deploy profile) are read from there.

1. Look at the plan. Without -Apply the script signs in to nothing and changes nothing. It prints every step with the exact command it would run, and names any value it still needs:

.\tools\setup\Convert-PimToPrivate.ps1 -TenantId <tenant-id> -SubscriptionId <subscription-id> -ResourceGroup rg-pim `
    -HubVnetId /subscriptions/<hub-subscription-id>/resourceGroups/rg-connectivity/providers/Microsoft.Network/virtualNetworks/vnet-hub `
    -PrivateEndpointSubnetAddressPrefix 10.50.1.0/26 [-SkipPeering] [-HostName pim.contoso.local -KeyVaultName kv-contoso-certs -KeyVaultCertName pim-contoso-local] [-SqlPrivate]

No deploy profile on this machine? Add the values the plan asks for (-Location, -VnetName, -SubnetName, -VnetAddressPrefix, -SubnetAddressPrefix, -AcrName, -LogAnalyticsName, -SqlServerFqdn), or pass the file Invardia’s wizard regenerated with -DeployProfilePath.

2. Run it by adding -Apply. You sign in once (browser sign-in, or your Invardia support session for this tenant), and every step reuses that session. A session for another tenant is refused.

3. Resume after a stop or a failure. The script keeps its progress per tenant and resource group in %LOCALAPPDATA%\pim\convert\. Continue with the same three ids plus -Resume -Apply. Steps that are already done are skipped, and the first run’s parameters are reused:

.\tools\setup\Convert-PimToPrivate.ps1 -TenantId <tenant-id> -SubscriptionId <subscription-id> -ResourceGroup rg-pim -Resume -Apply

Where it stops for your network team (exit code 10). The progress is saved at both points:

Stop When What to do, then resume with
1. The hub side With -SkipPeering (the hub VNet belongs to another team), before anything changes Give the hub owner the script that the printed New-PimHubPrepareScript.ps1 command writes (Step 1 below). They run it and confirm their side. Then add -HubSideDone.
2. Check from inside After the private environment is up, before any public endpoint is closed. Skipped when the machine running the script can reach the private address: the install check then runs right there. From a machine inside the network, check Step 5 below and run the printed Confirm-PimInstall.ps1 command. Then add -ConfirmedFromInside.

Route. The default is -Route Redeploy (Route B below). It is the route that gives the new identities everything (Graph permissions, Azure roles and database users) in your one session, and it does not need Windows. Use -Route Rebuild (Route A) if the apps were changed by hand after the install and you want to keep those changes.

Closing the public endpoints. After the check from inside, the script turns the registry’s public network access off (Route Redeploy; Route Rebuild does it inside the rebuild with -LockRegistry). It refuses, and changes nothing, until the private build path is complete: a Premium registry with an approved private endpoint, the build pool provisioned and running in your VNet, and the updater job pointed at that pool (PIM_ACR_AGENT_POOL). Fix what it names and resume. With -SqlPrivate, the very last change turns the SQL server’s public network access off with Set-PimSqlPublicAccess.ps1 (Step 7), which refuses unless the server’s private endpoint is approved. You can add -SqlPrivate to a finished conversion later: -Resume -Apply -SqlPrivate.

The safety copy (Step 2) always runs first: a capture of the live apps, plus the database’s point-in-time restore point. If it fails, the script stops before anything is changed. If any later step fails, the script prints the rollback for that point (see Rollback at the end). The steps below remain the reference for what each step does.

What changes, and what does not

The internal/external setting of a Container Apps environment is fixed when the environment is created. Azure has no update for it. So the conversion deletes and recreates the Container Apps part only: the PIM Manager app, the jobs, and the environment itself. Everything that holds data stays: the SQL database (all your configuration and history), the registry and its images, the VNet, the managed identities, Log Analytics, and any Key Vault or storage. The conversion script records this keep-list before it starts and checks it again at the end.

Before (public) After (private)
PIM Manager address ca-pim-manager.<name-A>.<region>.azurecontainerapps.io, public ca-pim-manager.<name-B>.<region>.azurecontainerapps.io, the new default domain, on a private static IP. A custom name (Step 6) keeps one stable address.
Who can reach it Anyone on the internet, through Entra sign-in Your VNet and the networks peered to it, through Entra sign-in
Registry Usually Basic Premium, private endpoint, public access off, build pool
SQL Public endpoint plus the PIM subnet’s VNet rule Private endpoint, public access off (Step 7; automatic with -SqlPrivate, or Set-PimSqlPublicAccess.ps1 -Disable)
Data — Unchanged

Downtime: the PIM Manager and the engine are unavailable from the delete until the restore. Deleting a VNet-integrated environment typically takes 15–30 minutes (the script waits up to 45, -DeleteTimeoutSeconds). Plan a maintenance window of about 1–2 hours. Privileged access that is already active stays active: Entra keeps the assignments. Only changes and new activations wait.

This is the same procedure the Architecture and Security deck describes as “export, verify, rebuild, import”. The current scripts keep the database in place, so no import is needed. The export (Step 2) is your safety copy.

Read first: Network options.

Where the commands run from: every .\tools\setup\... command in this guide is a package script: run it from the PIM Manager release package folder (Pro / trial: the signed package Install-Invardia downloaded for your installation; Community: the public repository at the release tag). Grant-PimEnginePermissions.ps1 and New-PimHubPrepareScript.ps1 are also published as standalone files at https://invardia.com/support/pim/. Every script has its documentation page at https://invardia.com/docs/pim/scripts/<Script>/ (Network options, section 6).


Choose the route

Route A: Rebuild-PimEnvInternal.ps1 (recommended when the environment was tuned after install) Route B: remove and redeploy (recommended when you still have the install parameters)
How Captures the live definition of the PIM Manager and every job. Deletes, recreates the environment internal-only on the same subnet and workspace, restores the apps from the capture, and diffs them. Repairs what is keyed to the old identities, re-attaches Entra sign-in, and publishes the private DNS zone for the new domain. Remove-PimContainerStack.ps1 deletes the Container Apps stack. Then Invoke-PimDeployAll.ps1 -Exposure internal builds it again from your parameters: the original install’s deploy profile, or the guided install’s answers.
Keeps hand-made changes to the apps (environment variables, scale, secrets) Yes, from the capture No. Only what the parameters say.
New managed identities get their Graph permissions, Azure roles and database users Azure roles: yes, automatically. Graph permissions: yes, with -SqlServer -HostingAccessTenantId, in your own session. Database users: only with a certificate or secret identity (not yet automated: repairing them in your browser or support session). Yes, all of it. The infra step runs in full because the environment no longer exists.
No Azure CLI Yes. The rebuild and its Graph re-grant are REST only. Yes. Remove-PimContainerStack.ps1 and the redeploy, including the image step, are REST only.
Plan mode Yes (default). It writes the capture and changes nothing. Yes. Both scripts plan unless you pass -Apply.
Needs Windows Yes. The capture protects secret values with Windows DPAPI (not yet automated: a capture that runs on Linux or Cloud Shell). No

Both routes share Steps 0–3 and Steps 5–8.


Step 0: Prerequisites

  • Rights: Owner (or Contributor + User Access Administrator) on the subscription. Privileged Role Administrator and Application Administrator in Entra ID. Network Contributor on the hub if the scripts should create the peering. The same as a fresh install (Network options, Prerequisites).
  • Sign-in: browser sign-in, or an Invardia support session. The rebuild uses whichever REST session the shell has: the Invardia Support app’s session for this tenant, otherwise the person signed in.
  • The current values: subscription, resource group (for example rg-pim), environment cae-pim, app ca-pim-manager, SQL server, registry, and the Container Apps subnet’s VNet. If the original install was done with Invoke-PimDeployAll, its parameters are in %LOCALAPPDATA%\pim\deploy-profiles\<tenant-id>-<resource-group>.json on the machine that ran it.
  • A private-endpoint subnet range inside the existing PIM VNet (for example 10.50.1.0/26). If the VNet has no free space, add an address range to it first (your team).

Step 1: Prepare the network (your team)

The same as Step 1 of the install guide:

  1. Hub peering. Either give the installer Network Contributor on the hub, or have the hub owner run the script from New-PimHubPrepareScript.ps1 (the hub owner runs the written file with PowerShell 7 on any machine, first without and then with -Apply; browser sign-in, no Azure CLI, no modules).
  2. Gateway transit / forwarded traffic on the peering, if admins come through the hub gateway or firewall.
  3. DNS forwarding to your resolver for the new default domain (printed by Step 4) and the privatelink zones.
  4. Outbound path: a NAT gateway or a route table to your firewall, with the allow-list in Network options, 2.3. If you introduce a NAT gateway now, note that storage accounts which admitted PIM by its old egress IP must admit the new one, or the subnet.
  5. Azure Policy: exemptions or adjustments, if a deny policy would block a Premium registry or private endpoints.

Verify: the hub owner confirms their side. The forwarders and the egress rules are in place.


Step 2: Safety copy

The rebuild does not touch the database, but take a copy anyway:

  • the Azure SQL automatic backups (point-in-time restore) already cover you; note the current time;
  • optionally a BACPAC export of the database from the Azure portal (SQL database > Export), to a storage account you own.

Step 3: Make the registry private (and add the SQL private endpoint later)

Re-run the prerequisites script with the existing names and the private options. It is find-or-create:

  • an existing registry is upgraded to Premium (never downgraded);
  • the private endpoint, the privatelink.azurecr.io zone (both records) and the build pool are added;
  • existing resources are reused.
.\tools\setup\New-PimHostingPrerequisites.ps1 -TenantId <tenant-id> -SubscriptionId <subscription-id> `
    -Token contoso -UseSignedInAccount -Location swedencentral `
    -ResourceGroupName rg-pim -VnetName vnet-pim -SubnetName <existing ACA subnet name> `
    -VnetAddressPrefix <existing VNet range> -SubnetAddressPrefix <existing ACA subnet range> `
    -AcrName <existing registry> -LogAnalyticsName <existing workspace> -SqlServerName <existing server name> `
    -AcrSku Premium -AcrPublicAccess false -AcrAgentPoolName pimbuild `
    -PrivateEndpointSubnetName pim-endpoints -PrivateEndpointSubnetAddressPrefix 10.50.1.0/26
  • Pass the existing ranges. The script refuses to run without an address plan and will not invent one.
  • On an existing registry, -AcrPublicAccess false adds the private endpoint but does not turn public access off. That happens later, after the check from inside (Step 5): Convert-PimToPrivate.ps1 does it on both routes, Route A’s -LockRegistry does it in the rebuild. Both refuse while the registry has no approved private endpoint; Convert-PimToPrivate.ps1 also refuses while the build pool is missing, not provisioned or outside the VNet, or the updater job does not carry PIM_ACR_AGENT_POOL (below). Image pulls keep working throughout.
  • Leave out -SqlPrivateEndpoint for now. The rebuild must still reach SQL the way it does today. Step 7 adds the SQL private endpoint after the conversion.

Verify: === VERIFY === ends with RESULT: OK. The output shows privatelink.azurecr.io … 2 record(s) and the build pool’s provisioning state.

Point the nightly updater at the build pool now (Pro). It reads PIM_ACR_AGENT_POOL. Re-run your original Invoke-PimDeployAll command (Route B does this anyway in Step 4B) with -AcrAgentPoolName pimbuild -Apply. The updater step re-applies the job, and the infra step is skipped while the environment exists. Alternatively, wait for Step 5.


Step 4A: Route A, the rebuild

Plan. This writes a fresh capture of the live environment and changes nothing:

.\tools\setup\Rebuild-PimEnvInternal.ps1 -Tag contoso -SubscriptionId <subscription-id> -ResourceGroup rg-pim `
    -HubVnetId /subscriptions/<hub-subscription-id>/resourceGroups/rg-connectivity/providers/Microsoft.Network/virtualNetworks/vnet-hub `
    -EasyAuthTenantId <tenant-id> -EasyAuthAllowedPrincipals PIM-Admins

The plan prints:

  • the jobs, app and environment it would delete;
  • the environment it would create (--internal-only true, the same subnet and workspace);
  • that the PIM Manager is restored with internal ingress first, then Entra sign-in is attached and verified, and only then is ingress switched on for the VNet;
  • the private DNS zone it would create for the new domain, linked to the PIM VNet and each -HubVnetId;
  • notes on the registry (“could be turned off … pass -LockRegistry”) and on SQL (“NO private endpoint …”).

Apply:

.\tools\setup\Rebuild-PimEnvInternal.ps1 -Tag contoso -SubscriptionId <subscription-id> -ResourceGroup rg-pim `
    -HubVnetId <hub-vnet-id> [-PrivateDnsResourceGroup rg-dns] `
    -EasyAuthTenantId <tenant-id> -EasyAuthAllowedPrincipals PIM-Admins `
    -LockRegistry `
    -SqlServer <server>.database.windows.net -HostingAccessTenantId <tenant-id> `
    [-SqlAdminClientId <app id> -SqlAdminCertThumbprint <thumbprint>] `
    -Apply
Parameter Why
-HubVnetId (one or more) The private DNS zone for the new domain is linked to these networks as well as the PIM VNet. Without it, only the PIM VNet resolves the PIM Manager.
-PrivateDnsResourceGroup Where that zone goes. The default is the PIM resource group. It must be in the same subscription (not yet automated: a zone in another subscription).
-EasyAuthTenantId, -EasyAuthAllowedPrincipals / -EasyAuthAllowAllTenantUsers Re-attach Entra sign-in to the new address and say who may sign in. If the PIM Manager already restricts sign-in to assigned users, that restriction is kept. Without sign-in the PIM Manager stays on internal ingress and is not reachable from the VNet.
-LockRegistry Turns the registry’s public network access off. It is refused unless the registry is Premium and has a private endpoint (Step 3).
-SqlServer -SqlAdminClientId -SqlAdminCertThumbprint (or -SqlAdminClientSecret) Repairs the database users. Recreated apps get new managed identities, and the database users created for the old ones no longer match. This needs an identity that is a member of grp-pim-sql-admins, and a host SQL admits (today’s public endpoint plus a firewall rule for your host, if needed). There is no browser-sign-in option here (not yet automated). Without these values the script repairs the Azure roles, and tells you the database half is still owed.
-HostingAccessTenantId (with -SqlServer); optionally -HostingAccessClientId -HostingAccessCertThumbprint Re-grants the new identities’ Microsoft Graph permissions, the “Run now” right (Container Apps Jobs Operator on the engine job), the engine’s Reader on its own job and at the tenant root, and the SchedulerTickJobId setting, as part of the rebuild. With the tenant id alone it runs in your session (browser or Invardia support; REST only). It writes one setting to SQL, so your host must still reach the server. Alternative below.
-ResumeFromCapture (with -CaptureDir) If a run stopped after the delete, the live environment no longer has anything to capture. Resume from the capture on disk, which skips the deletes already done.

If you did not pass the Graph re-grant: in the PIM Manager go to Overview > permission check, or Get Started > Engine permissions. It shows the exact Grant-PimEnginePermissions.ps1 command with your environment’s object ids and permission names. That script uses browser sign-in, no modules and no Azure CLI. Run it once for the engine identity. A new grant reaches the engine within about 30 minutes.

Verify (printed by the script):

  • step 4: static IP : 10.50.0.x;
  • step 8: Easy Auth bound to … (unauth -> RedirectToLoginPage);
  • step 9: ingress switched on and read back;
  • step 10: the private DNS zone for the new domain is created, and the registry reports public network access OFF (read back);
  • step 11: every data-bearing resource that existed before this rebuild still exists;
  • REBUILD COMPLETE, with the old and new domains.

The capture’s secret files are shredded at the end.

Then continue with Step 5.


Step 4B: Route B, remove and redeploy

# 1. plan, then delete the Container Apps stack (jobs, apps, environment, their system identities' role assignments)
.\tools\setup\Remove-PimContainerStack.ps1 -SubscriptionId <subscription-id> -ResourceGroup rg-pim
.\tools\setup\Remove-PimContainerStack.ps1 -SubscriptionId <subscription-id> -ResourceGroup rg-pim -Apply

Remove-PimContainerStack.ps1 is REST only (no Azure CLI). It keeps SQL, the registry, the VNet, the user-assigned identities, Key Vault, storage and Log Analytics, and reads back that no Microsoft.App resource is left.

# 2. the same parameters as the original install, with the private-mode values changed or added
. .\tools\setup\_PimDeployProfile.ps1
$p = (Read-PimDeployProfile -Path "$env:LOCALAPPDATA\pim\deploy-profiles\<tenant-id>-rg-pim.json").parameters   # no secrets are ever saved
# No profile (e.g. the install ran on another machine)? Invardia's wizard regenerates the same set from the install answers.
$p.Exposure                  = 'internal'
$p.AcrSku                    = 'Premium'
$p.AcrPublicAccess           = 'false'
$p.AcrAgentPoolName          = 'pimbuild'
$p.PrivateEndpointSubnetName = 'pim-endpoints'
$p.PrivateEndpointSubnetAddressPrefix = '10.50.1.0/26'
$p.SqlPrivateEndpoint        = $false        # add the SQL private endpoint in Step 7, after the first private deploy works
$p.HubVnetName               = 'vnet-hub'
$p.HubVnetResourceGroup      = 'rg-connectivity'
$p.HubVnetSubscriptionId     = '<hub-subscription-id>'
$p.SkipHostedSmoke           = $true
.\tools\setup\Invoke-PimDeployAll.ps1 @p             # plan
.\tools\setup\Invoke-PimDeployAll.ps1 @p -Apply

The infra step now runs in full:

  • the internal-only environment;
  • the PIM Manager, created closed, then opened by the easyauth step;
  • the engine and bootstrap jobs;
  • the peering and the private DNS zone;
  • the Graph permissions, Azure roles and database users for the new identities.

The updater step sets PIM_ACR_AGENT_POOL. The existing sign-in app registration is reused, and its reply address is moved to the new address.

Verify: the same as the install guide, Step 4.


Step 5: Verify from inside the network

As in the install guide, Step 6:

  • the new name resolves to the static IP from the hub or your VPN or Private Access clients;
  • the PIM Manager answers 302 to login.microsoftonline.com, never 200;
  • from the internet it does not resolve.

Sign in, open Overview, and watch one engine tick (5 minutes) complete. Check that Settings > Jobs shows the engine and the updater with recent runs.


Step 6: Tidy up the old address and set a stable one

The script cannot see these leftovers, and says so:

  • old reply addresses for *.<name-A>… on the PIM Manager’s sign-in app registration (“PIM4EntraPS Manager”): remove them in Entra ID;
  • any DNS record you made for the old address, such as a public CNAME or an AD record: remove it;
  • IP allow-list rules on the old app do not matter any more, because the app was recreated.

Optional custom name, which survives future rebuilds:

.\tools\setup\Set-PimManagerCustomHost.ps1 -SubscriptionId <subscription-id> -TenantId <tenant-id> -ResourceGroup rg-pim `
    -HostName pim.contoso.local -KeyVaultName kv-contoso-certs -KeyVaultCertName pim-contoso-local `
    -Dns PrivateDnsZone -PrivateDnsResourceGroup rg-dns -LinkVnetIds <hub-vnet-id>,<pim-vnet-id>

A private environment needs your own certificate (Key Vault, or -PfxPath). The record is written only into a private zone contoso.local that already exists in rg-dns; otherwise the script prints what to create. With -Dns AdDns, PIM prints the record for you to add. The rebuild script itself does not take a custom name (not yet automated).


Step 7: Make SQL private and close the public endpoints

  1. Add the SQL private endpoint by re-running the prerequisites script from Step 3 with -SqlPrivateEndpoint added. This creates the endpoint and the privatelink.database.windows.net zone linked to the PIM VNet.

  2. Verify from inside: Resolve-DnsName <server>.database.windows.net returns a 10.x address. The PIM Manager still works.

  3. Turn the SQL server’s public network access off, after the deploy in item 4 below if you run it from outside the network (that deploy still reaches SQL from the deploy host). Look at the plan first, then add -Apply. The script refuses unless the server has an approved private endpoint, and reads the setting back. Convert-PimToPrivate.ps1 -SqlPrivate runs it for you as its last step. To undo it, run the same command with -Enable.

    .\tools\setup\Set-PimSqlPublicAccess.ps1 -TenantId <tenant-id> -SubscriptionId <subscription-id> -ResourceGroup rg-pim `
        -ServerName <existing server name> -Disable
    
  4. Future deploys apply schema changes in-cloud only if they know the store is private. From now on, every Invoke-PimDeployAll run (and so the saved deploy profile that Update-PimCommunity.ps1 replays) must carry -Exposure internal, -SqlPrivateEndpoint, -AcrAgentPoolName and -SkipHostedSmoke. Route A leaves the old profile saying external. A deploy with the wrong exposure only warns, because the environment cannot change, but it takes the public-SQL paths. Run one deploy with the corrected parameters and -Apply to save them. The end-of-install check (Confirm-PimInstall.ps1) and Set-PimLicense.ps1 must now run from inside the network.


Step 8: Updates after the conversion

The same as the install guide, Step 8. Pro updates keep arriving on your ring, built on the build pool, provided:

  • outbound invardia.com is allowed (the signed manifest and the release archive both come from it; Network options, 2.3);
  • the updater job carries PIM_ACR_AGENT_POOL.

Community installs update with Update-PimCommunity.ps1 -Apply from the deploy host.


Rollback

When How
Before -Apply Nothing changed (plan mode).
The rebuild stopped part-way (Route A) Re-run with -ResumeFromCapture -CaptureDir <the printed folder>. The capture is the only copy of the app definitions once they are deleted, so keep that folder until the run completes.
You want public mode back Rebuild-PimEnvExternal.ps1 -Tag … -SubscriptionId … -ResourceGroup … -EasyAuthTenantId <tenant> -Apply. This is the same tool in the other direction. It refuses to expose a PIM Manager it cannot prove is protected by sign-in, and removes the stale private DNS zone (-KeepOldPrivateDnsZone keeps it). The address changes again.
Registry locked too early Turn public network access back on for the registry (portal). Only a registry with a private endpoint can be locked by the script, so pulls through the endpoint keep working.
SQL public access off too early Turn it back on, then work through the private endpoint first.
Data Not touched by any step. Point-in-time restore or the BACPAC from Step 2 if anything else goes wrong.
InvardiaInvardia

See the risk. Control the access.

Products

PIM ManagerSecurityInsightEntra Policy SuiteAll featuresPricingFree 60-day Pro trialBook a demoWhy Invardia

Docs

DocumentationSearch the docsPIM Manager settingsSecurityInsight settingsPIM network guidesCommand wizardNetwork egressPIM Manager design configuratorSecurityInsight design configuratorDesign your installationCalculate your cost

Company

AboutContactCustomer portalSupportHow support worksHow updates reach youSupport in numbersSetup scriptsRemote support accessContact: info@invardia.com

Legal

Terms of saleLicence termsData processingPrivacyCookiesCompany information
© 2026 Invardia – a brand of 2LINKIT, founded and owned by Morten Knudsen · Strandvejen 21A, 6000 Kolding, Denmark · VAT DK39208032 · Contact: info@invardia.com