Install PIM Manager in private (internal) mode from scratch
Written by the PIM Manager team for PIM Manager 2.4.579 · 2026-10-10
This guide installs a Single PIM Manager (one tenant) whose Container Apps environment is internal-only. The PIM Manager gets a private IP address in your virtual network. The database and the container registry are reached through private endpoints, and the nightly updater builds on a registry build pool inside the network.
The Invardia guided install builds public mode only.
Install-PimManager.ps1refusesexposurevalues other thanexternal, with the message “an internal install is a consultant-led setup”. This guide is that consultant-led setup: the same deploy command the guided install uses (Invoke-PimDeployAll.ps1), with the private-mode parameters added.Managing tenant and Managed tenant builds use
Invoke-PimMspBuild.ps1with"exposure": "internal"in their configuration file. That is covered in the command wizard, not here.
Read first: Network options. It explains each decision this guide asks you to make.
Step 0: Decide and collect
| Decision | Your value (example) | Parameter |
|---|---|---|
| Tenant / subscription | <tenant-id> / <subscription-id> |
-TenantId -SubscriptionId |
| Region: any Azure region (the preflight checks, before anything is created, that every service PIM Manager needs is available there and suggests the nearest region if not) | swedencentral |
-Location |
| Resource group and names | rg-pim, vnet-pim, cae-pim, law-pim, acrpimcontoso, sql-pim-contoso |
-ResourceGroup -VnetName -VnetResourceGroup -EnvName -LogAnalyticsWorkspaceName -AcrName -SqlServerFqdn |
| Short name used to derive the remaining resource names | contoso |
-PrereqToken |
| PIM VNet range / Container Apps subnet (/27 minimum, /24 recommended) | 10.50.0.0/22 / 10.50.0.0/24 |
-PrereqVnetAddressPrefix -PrereqSubnetAddressPrefix |
| Private-endpoint subnet (/26) | 10.50.1.0/26 |
-PrivateEndpointSubnetName -PrivateEndpointSubnetAddressPrefix |
| Registry build pool | pimbuild (tier S1, 1 agent) |
-AcrAgentPoolName (on New-PimHostingPrerequisites also -AcrAgentPoolTier -AcrAgentPoolCount) |
| Hub VNet: should the scripts peer to it? | vnet-hub in rg-connectivity, <hub-subscription-id> |
-HubVnetName -HubVnetResourceGroup -HubVnetSubscriptionId, plus -SkipPeering if you create the peering yourself |
| Where the environment’s private DNS zone goes | the hub’s group (default when a hub is given), or your own | -PrivateDnsResourceGroup |
| Firewall allows only port 1433 to SQL? | yes → Proxy |
-SqlConnectionPolicy Proxy |
| Who administers PIM / who may sign in | pim.admin@contoso.com / group PIM-Admins |
-ManagerSuperAdmins -EasyAuthAllowedPrincipals (or -EasyAuthAllowAllTenantUsers: members only, never guests) |
| Mail sender / alert recipients | pim@contoso.com / secops@contoso.com |
-MailSender -AlertRecipients |
| Edition | Pro (updates from Invardia, ring 3) or Community | Pro: -UpdateSource Invardia -UpdateRing 3. Community: omit both. |
Rights: see Network options, Prerequisites. They are the same as for the guided install, plus Network Contributor on the hub if the scripts should create the peering.
What you need on the machine: PowerShell 7 (Windows PowerShell 5.1 also works) and the PIM Manager release package:
- Pro / trial:
Install-Invardiadownloads the signed release package for your installation. Run the commands in this guide from that downloaded package folder (.\tools\setup\<Script>.ps1). They load the engine libraries next to them, so they do not work as single downloaded files. - Community: clone the public repository at the release tag and run the commands from its folder.
- The one standalone script in this guide,
New-PimHubPrepareScript.ps1(Step 1.2), is also published on its own athttps://invardia.com/support/pim/New-PimHubPrepareScript.ps1, so a hub owner can run it without the package.
Every script has its documentation page at https://invardia.com/docs/pim/scripts/<Script>/. See Network options,
section 6, for the full list and the save, verify, read, run form for standalone scripts.
No Azure CLI and no PowerShell modules are needed for any command in this guide. The image is built through the registry’s own REST build API, on the build pool. The file the optional hub preparation script writes in Step 1.2 needs neither: the hub owner runs it with PowerShell 7 and a browser sign-in.
Step 1: Prepare the network (your team)
Do these before the install. None of them is done by PIM.
-
Address space: reserve the PIM VNet range. It must not overlap the hub or anything peered to it.
-
Hub peering. If the identity running the install will not get Network Contributor on the hub, write the hub owner a script:
.\New-PimHubPrepareScript.ps1 -SpokeSubscriptionId <subscription-id> -SpokeResourceGroup rg-pim ` -SpokeVnetName vnet-pim -HubResourceGroup rg-connectivity -HubVnetName vnet-hub ` [-HubSubscriptionId <hub-subscription-id>] [-DeployPrincipalObjectId <object id of the installer>] [-DnsResourceGroup <dns-rg>]-HubSubscriptionIdis optional: leave it out when the hub VNet is in the PIM subscription. The script only writes a file,prepare-hub-pim.ps1, which creates the hub side of the peering with the exact name the setup looks for. The hub owner runs that file with PowerShell 7 on any machine (no Azure CLI, no modules): first.\prepare-hub-pim.ps1to see the plan, then.\prepare-hub-pim.ps1 -Apply(browser sign-in as the hub owner). Then install with-SkipPeering. -
Gateway transit / forwarded traffic. If admins come in over the hub’s VPN or ExpressRoute gateway, plan to turn on allow gateway transit (hub side) and use remote gateways (PIM side) after the install. If traffic goes through a hub firewall, plan allow forwarded traffic. The scripts create the peering with VNet access only.
-
DNS forwarding: conditional forwarders from on-premises / custom DNS to your Azure DNS Private Resolver for the environment’s default domain (known after Step 3) and the privatelink zones (Network options, 1.3).
-
Outbound path: a NAT gateway, or a route table to your firewall, with the allow-list from Network options, section 2.3. Include the build pool’s base-image hosts. The route table can be attached to the Container Apps subnet after Step 3 creates it, or you can create the subnet yourself first with the same name and range.
-
Azure Policy: check the deny effects in Network options, section 5.2 (Premium registry, region, tags).
Verify: your hub owner confirms the hub side of the peering, if you used the prepare script. The DNS forwarders exist (they can point at the resolver now, and the zone comes later).
Step 2: Sign in
Two supported ways, with no client secret and no PowerShell modules:
- Browser sign-in (you, at the keyboard). When a script needs a token and no session exists, PIM’s REST client opens the browser sign-in (authorization code with PKCE, never device code). Use the account that holds the rights above.
- Invardia support session. If Invardia support runs the install for you through your tenant’s Invardia Support app, the scripts use that session automatically. Grant it first. See Support access in the guided-install documentation.
Pass -UseSignedInAccount. The signed-in person also becomes a member of the SQL admin group
grp-pim-sql-admins. They need that to reach the database later.
Step 3: Plan the deployment (changes nothing)
Put the parameters in one place. Every name below is a real parameter of Invoke-PimDeployAll.ps1.
$p = @{
Scenario = 'S2' # Single tenant
TenantId = '<tenant-id>'
SubscriptionId = '<subscription-id>'
Location = 'swedencentral'
Exposure = 'internal' # IMMUTABLE once the environment exists
ResourceGroup = 'rg-pim'
VnetName = 'vnet-pim'
VnetResourceGroup = 'rg-pim'
PrereqToken = 'contoso'
PrereqVnetAddressPrefix = '10.50.0.0/22'
PrereqSubnetAddressPrefix = '10.50.0.0/24'
PrivateEndpointSubnetName = 'pim-endpoints'
PrivateEndpointSubnetAddressPrefix = '10.50.1.0/26'
AcrName = 'acrpimcontoso'
AcrSku = 'Premium' # the default for internal; stated for clarity
AcrPublicAccess = 'false'
AcrAgentPoolName = 'pimbuild'
SqlServerFqdn = 'sql-pim-contoso.database.windows.net'
SqlDatabase = 'PimPlatform'
SqlConnectionString = 'Server=tcp:sql-pim-contoso.database.windows.net,1433;Database=PimPlatform;Encrypt=True;TrustServerCertificate=False;Connection Timeout=30'
SqlPrivateEndpoint = $true # the default for internal; stated for clarity
SqlConnectionPolicy = 'Proxy' # only if your firewall allows 1433 and nothing else
EnvName = 'cae-pim'
LogAnalyticsWorkspaceName = 'law-pim'
LogAnalyticsResourceGroup = 'rg-pim'
WorkerMode = 'cron'
HubVnetName = 'vnet-hub'
HubVnetResourceGroup = 'rg-connectivity'
HubVnetSubscriptionId = '<hub-subscription-id>'
# SkipPeering = $true # if the hub owner made the peering (Step 1.2)
# PrivateDnsResourceGroup = 'rg-dns' # optional, same subscription as PIM
SkipHostedSmoke = $true # the deploy host cannot reach a private PIM Manager; Step 6 verifies from inside
ManagerSuperAdmins = 'pim.admin@contoso.com'
EasyAuthAllowedPrincipals = @('PIM-Admins')
MailSender = 'pim@contoso.com'
AlertRecipients = @('secops@contoso.com')
UseSignedInAccount = $true
SkipAppReg = $true # the hosted engine runs as a managed identity
# Pro only:
UpdateSource = 'Invardia'
UpdateRing = 3
}
.\tools\setup\Invoke-PimDeployAll.ps1 @p
Without -Apply the run is plan only:
- It checks the names, the region and the rights.
- It warns if no hub is given (“the PIM spoke VNet will be left ISOLATED”).
- It prints what the plan would change, per step.
- It prints the resolved defaults, for example
registry SKU: Premium (from -Exposure internal …)andsql: private endpoint ON (from -Exposure internal …).
Verify: the plan lists the steps prereq, image, infra, sqlaccess, schema, mailsender, features,
easyauth, code, updater, access and alerting, with nothing refused.
Step 4: Build
.\tools\setup\Invoke-PimDeployAll.ps1 @p -Apply
What happens, in order. Each step is idempotent, so a failed run is fixed and re-run.
| Step | What it builds | Private-mode detail |
|---|---|---|
prereq (New-PimHostingPrerequisites.ps1) |
Resource group, VNet, delegated Container Apps subnet, Log Analytics, managed identities, registry, SQL server and database, SQL admin group | Premium registry with public access off, its private endpoint in pim-endpoints, the privatelink.azurecr.io zone (both records), the build pool in the same subnet. SQL private endpoint and the privatelink.database.windows.net zone. A user-assigned SQL identity administers the database from inside the network. |
image |
The PIM Manager image | Built on the build pool, inside the VNet, through the registry’s REST build API (-AcrAgentPool) |
infra |
Internal-only Container Apps environment, the PIM Manager (created closed: it answers 403 until sign-in is in place), the engine job ca-pim-tick, the bootstrap job ca-pim-dbinit |
Peering both ways (unless -SkipPeering), the private DNS zone for the default domain (@, *, *.internal → the static IP) linked to the PIM VNet and the hub, the Graph permissions and Azure roles for the managed identities |
sqlaccess |
Subnet service endpoint and VNet rule on SQL | Runs on every deploy |
schema, features, access, alerting |
Database schema, feature switches, PIM Manager administrators, alert recipients | Applied in-cloud by ca-pim-dbinit: this host has no route to private SQL |
mailsender |
Notification mailbox and scoped send right | Needs an active Exchange Administrator; otherwise it becomes a follow-up |
easyauth |
The PIM Manager’s sign-in app, assignment required, then the closing restriction removed | The reply address is the private address |
code |
Rolls the apps and the engine job to the image digest | — |
updater |
Nightly updater ca-pim-update on your ring |
Gets PIM_ACR_AGENT_POOL=<pool>, so the nightly build also runs on the build pool. Community without a published source: skipped (see Step 8). |
The run ends with the PIM Manager’s address and the static IP, and an Entra Private Access / private-link checklist.
Verify (from the deploy host):
- the prereq
=== VERIFY ===block ends withRESULT: OK; - the infra step shows
env static IP = 10.50.0.xandPeering: … Connectedon both sides; - the summary has no failed step.
The verify step reports unverified, not healthy, because the smoke test was skipped (-SkipHostedSmoke). That
is expected. Step 6 verifies from inside.
Afterwards (your team):
- attach the route table or NAT gateway to the Container Apps subnet, if you did not create the subnet yourself;
- set gateway transit and forwarded traffic on the peering if needed;
- point the conditional forwarders at the environment’s default domain, now printed.
Step 5: Licence and support access (Pro)
An enrollment key is refused when SQL is private: the licence step stores the licence from the deploy host, which has no route. Register the licence file from a machine inside the network, such as a jump host or the hub:
.\tools\setup\Set-PimLicense.ps1 -LicensePath .\Contoso.pimlicense -SqlServer sql-pim-contoso.database.windows.net `
-TenantId <tenant-id> -UseSignedInAccount
This needs a member of grp-pim-sql-admins; the installer is one. To give your Invardia Support app access:
.\tools\setup\Grant-PimSupportAccess.ps1 -AppId <support-app-id> -Level troubleshoot -TenantId <tenant-id> `
-SubscriptionId <subscription-id> -ResourceGroup rg-pim -SqlServer sql-pim-contoso.database.windows.net
-Level setup also makes the app a SQL admin and the owner of PIM’s own app registrations. Like the licence step,
this one writes to SQL, so it must run from inside the network.
Step 6: Verify from inside the network
From a client in the hub, or reached through your VPN or Private Access:
Resolve-DnsName ca-pim-manager.<default-domain> # -> 10.50.0.x (the static IP)
Resolve-DnsName sql-pim-contoso.database.windows.net # -> CNAME privatelink... -> 10.50.1.x
Resolve-DnsName acrpimcontoso.azurecr.io # -> 10.50.1.x
Invoke-WebRequest https://ca-pim-manager.<default-domain>/ -MaximumRedirection 0 -SkipHttpErrorCheck |
Select-Object StatusCode, @{n='Location';e={$_.Headers.Location}} # 302 to login.microsoftonline.com -- never 200
From the internet, the name must not resolve to anything you can reach.
Then sign in to the PIM Manager as a SuperAdmin and open Overview. The engine’s first runs and the permission check should be green within one tick (5 minutes).
End-of-install check (repairs what it can), from inside the network:
.\tools\setup\Confirm-PimInstall.ps1 -Role Single -TenantId <tenant-id> -SubscriptionId <subscription-id> `
-ResourceGroup rg-pim -SqlServerFqdn sql-pim-contoso.database.windows.net -EnvName cae-pim `
-AcrName acrpimcontoso -AcrAgentPoolName pimbuild -SuperAdmins pim.admin@contoso.com -AlertRecipients secops@contoso.com
Confirm-PimInstall.ps1 runs over REST in your signed-in or support session, with no Azure CLI. It needs a route to the
private SQL endpoint, which is why it runs from inside the network.
Step 7: Lock down (your team)
-
Turn off SQL public network access. The scripts create the private endpoint but leave the server’s public endpoint on: it admits only the PIM subnet and, during a deploy, the setup host. Once Step 6 passes, set Public network access = Disabled on the SQL server, in the portal or with your Azure Policy. Not yet automated: no single-tenant script turns it off. After that, the deploy’s
sqlaccessstep reports not applicable on later deploys, which is expected. -
Check that the registry reports public network access Disabled. It was created that way.
-
NSGs on the Container Apps subnet: inbound 443 from admin ranges only (Network options, 5.1).
-
Optional: a custom name that survives 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> -WhatIfRemove
-WhatIfto apply. On a private environment you must supply the certificate (Key Vault or-PfxPath/-PfxPassword). The sign-in reply address is added for you. The script writes the record only into a private zonecontoso.localthat already exists inrg-dns. It never creates a zone named after your domain, because that zone would answer for every name in it. If the zone is missing, it prints what to create. With-Dns AdDns, PIM prints the record for you to add to your AD DNS.
Step 8: How updates keep working privately
| Edition | How it updates | What must stay open |
|---|---|---|
| Pro / trial | The in-cloud updater ca-pim-update runs nightly (-UpdateCron, default 03:00 UTC). It reads Invardia’s signed manifest for its ring (-UpdateRing; the ring is set locally in your environment). It verifies the signature, builds the image on the build pool (PIM_ACR_AGENT_POOL), applies the schema in-cloud, rolls by digest, runs a health check and rolls back on failure. |
Outbound invardia.com only for the release itself: the signed manifest and the archive (https://invardia.com/api/updates/pim-manager/..., no separate storage host). Plus login.microsoftonline.com, management.azure.com, *.blob.core.windows.net (the build-context upload), SQL and the registry over their private endpoints. The build pool needs mcr.microsoft.com, *.data.mcr.microsoft.com and api.nuget.org. |
| Community | No in-cloud updater. Run Update-PimCommunity.ps1 -Apply from the deploy host. It pulls the latest release (git pull --ff-only) and re-runs Invoke-PimDeployAll with the parameters your last successful -Apply saved, private parameters included. The profile is in %LOCALAPPDATA%\pim\deploy-profiles\. |
The deploy host needs git, github.com and objects.githubusercontent.com, and the REST endpoints (the image is built on the pool through REST). Schema changes are applied in-cloud. |
Mirroring releases to your own storage is a support-led setup; ask Invardia support.
If the updater ever reports a failed build after you lock the registry, check that the job still carries
PIM_ACR_AGENT_POOL. Re-running Invoke-PimDeployAll … -AcrAgentPoolName pimbuild -Apply restores it.
Rollback
| Situation | What to do |
|---|---|
| A step failed | Fix the cause and re-run the same command. Every step checks what exists first. |
| Wrong exposure chosen | The environment’s internal/external flag cannot be changed. To go public, use Rebuild-PimEnvExternal.ps1 (the reverse of the conversion guide). |
| Remove the whole installation | Delete the resource group, the sign-in app registration (“PIM4EntraPS Manager”), and the group grp-pim-sql-admins. Then remove any peering and DNS records you created on the hub side. |
| Remove only the Container Apps part, keeping data | Remove-PimContainerStack.ps1 -SubscriptionId -ResourceGroup [-Apply] deletes the jobs, the apps, the environment and their role assignments, and keeps SQL, the registry, the VNet and the identities. REST only, no Azure CLI. |