PowerShell Setup: Control 1.20 — Network Isolation and Private Connectivity
Read the FSI PowerShell baseline first
Before running any command in this playbook, read the PowerShell Authoring Baseline for FSI Implementations. It is the canonical source for module version pinning, sovereign-cloud (GCC / GCC High / DoD) endpoints, mutation safety (-WhatIf / SupportsShouldProcess), Dataverse compatibility, and SHA-256 evidence emission. Snippets below show abbreviated patterns; the baseline is authoritative.
Last Updated: April 2026
Modules Required: Az.Accounts, Az.Network, Az.KeyVault, Az.Sql, Az.Storage, Az.Monitor, Az.PrivateDns, Microsoft.PowerApps.Administration.PowerShell
PowerShell Edition: Windows PowerShell 5.1 (Desktop) for Microsoft.PowerApps.Administration.PowerShell; PowerShell 7+ acceptable for the Az.* portions.
Prerequisites
# Pin versions per CAB approval; see PowerShell baseline for the canonical install pattern.
Install-Module Az.Accounts, Az.Network, Az.KeyVault, Az.Sql, Az.Storage, Az.Monitor, Az.PrivateDns -Scope CurrentUser -Repository PSGallery
Install-Module Microsoft.PowerApps.Administration.PowerShell -Scope CurrentUser -Repository PSGallery
# Required guard for Power Apps Administration cmdlets:
if ($PSVersionTable.PSEdition -ne 'Desktop') {
throw "Microsoft.PowerApps.Administration.PowerShell requires Windows PowerShell 5.1 (Desktop edition)."
}
Sovereign cloud authentication. For GCC / GCC High / DoD tenants, every
Add-PowerAppsAccountandConnect-AzAccountcall must pass the matching cloud parameter, or the cmdlet authenticates against commercial endpoints and produces false-clean results. See the baseline §3.
# Example sovereign-aware connect helper
function Connect-FsiClouds {
param(
[ValidateSet('Commercial','GCC','GCCHigh','DoD')] [string] $Cloud = 'Commercial',
[string] $TenantId
)
switch ($Cloud) {
'Commercial' { Add-PowerAppsAccount -Endpoint prod; Connect-AzAccount -Tenant $TenantId -Environment AzureCloud }
'GCC' { Add-PowerAppsAccount -Endpoint usgov; Connect-AzAccount -Tenant $TenantId -Environment AzureUSGovernment }
'GCCHigh' { Add-PowerAppsAccount -Endpoint usgovhigh; Connect-AzAccount -Tenant $TenantId -Environment AzureUSGovernment }
'DoD' { Add-PowerAppsAccount -Endpoint dod; Connect-AzAccount -Tenant $TenantId -Environment AzureUSGovernment }
}
}
Script 1 — Provision VNet, Delegated Subnet, and Private Endpoint Subnet (per region)
<#
.SYNOPSIS
Creates a VNet with a Power Platform-delegated subnet and a private-endpoint subnet.
.DESCRIPTION
Idempotent. Re-running against an existing VNet/subnet is a no-op except for tag updates.
Designed to run TWICE — once for the primary Azure region paired to the Power Platform region,
and once for the paired failover region.
.PARAMETER ResourceGroupName Resource group for the VNet (created if absent).
.PARAMETER Location Azure region (must be paired with the Power Platform region).
.PARAMETER VNetName VNet name (e.g., vnet-pp-prod-eastus).
.PARAMETER VNetAddressPrefix CIDR for VNet (e.g., 10.100.0.0/16).
.PARAMETER DelegatedSubnetCidr CIDR for Power Platform-delegated subnet (recommended /24).
.PARAMETER PrivateEndpointSubnetCidr CIDR for the private-endpoint subnet.
.PARAMETER WhatIf Standard ShouldProcess support.
.EXAMPLE
.\New-PpVNet.ps1 -ResourceGroupName rg-pp-net-prod-eastus -Location eastus `
-VNetName vnet-pp-prod-eastus -VNetAddressPrefix 10.100.0.0/16 `
-DelegatedSubnetCidr 10.100.1.0/24 -PrivateEndpointSubnetCidr 10.100.2.0/24
#>
[CmdletBinding(SupportsShouldProcess, ConfirmImpact='High')]
param(
[Parameter(Mandatory)] [string] $ResourceGroupName,
[Parameter(Mandatory)] [string] $Location,
[Parameter(Mandatory)] [string] $VNetName,
[Parameter(Mandatory)] [string] $VNetAddressPrefix,
[Parameter(Mandatory)] [string] $DelegatedSubnetCidr,
[Parameter(Mandatory)] [string] $PrivateEndpointSubnetCidr
)
$ErrorActionPreference = 'Stop'
if (-not (Get-AzResourceGroup -Name $ResourceGroupName -ErrorAction SilentlyContinue)) {
if ($PSCmdlet.ShouldProcess($ResourceGroupName,'Create resource group')) {
New-AzResourceGroup -Name $ResourceGroupName -Location $Location | Out-Null
}
}
$delegation = New-AzDelegation -Name 'powerplatform' `
-ServiceName 'Microsoft.PowerPlatform/enterprisePolicies'
$delegatedSubnet = New-AzVirtualNetworkSubnetConfig `
-Name 'snet-pp-delegated' -AddressPrefix $DelegatedSubnetCidr -Delegation $delegation
$peSubnet = New-AzVirtualNetworkSubnetConfig `
-Name 'snet-private-endpoints' -AddressPrefix $PrivateEndpointSubnetCidr `
-PrivateEndpointNetworkPoliciesFlag 'Disabled'
$existing = Get-AzVirtualNetwork -ResourceGroupName $ResourceGroupName -Name $VNetName -ErrorAction SilentlyContinue
if ($existing) {
Write-Verbose "[EXISTS] VNet $VNetName — verifying subnets only."
# Idempotent reconcile of subnets is left to the caller; production code should diff and update.
return $existing
}
if ($PSCmdlet.ShouldProcess($VNetName,'Create VNet with delegated subnet')) {
New-AzVirtualNetwork -ResourceGroupName $ResourceGroupName -Name $VNetName `
-Location $Location -AddressPrefix $VNetAddressPrefix `
-Subnet $delegatedSubnet, $peSubnet
}
Script 2 — Create and Link the Power Platform Enterprise (Network) Policy
The cmdlets
New-PowerAppEnvironmentEnterprisePolicyandNew-AdminPowerAppEnvironmentEnterprisePolicyare exposed by theMicrosoft.PowerApps.Administration.PowerShellmodule. Cmdlet names and parameters have evolved; always cross-check against Microsoft Learn: Set up virtual network support for the current shape.
<#
.SYNOPSIS
Creates a Power Platform network (subnet) enterprise policy and links it to a Managed Environment.
.PARAMETER EnvironmentId Power Platform environment GUID.
.PARAMETER PolicyName Display name for the enterprise policy.
.PARAMETER PrimarySubnetResourceId Full ARM resource ID of the primary delegated subnet.
.PARAMETER FailoverSubnetResourceId Full ARM resource ID of the failover delegated subnet.
.PARAMETER Cloud Commercial / GCC / GCCHigh / DoD.
.PARAMETER TenantId Entra tenant GUID.
#>
[CmdletBinding(SupportsShouldProcess, ConfirmImpact='High')]
param(
[Parameter(Mandatory)] [string] $EnvironmentId,
[Parameter(Mandatory)] [string] $PolicyName,
[Parameter(Mandatory)] [string] $PrimarySubnetResourceId,
[Parameter(Mandatory)] [string] $FailoverSubnetResourceId,
[ValidateSet('Commercial','GCC','GCCHigh','DoD')] [string] $Cloud = 'Commercial',
[Parameter(Mandatory)] [string] $TenantId
)
$ErrorActionPreference = 'Stop'
if ($PSVersionTable.PSEdition -ne 'Desktop') {
throw "Use Windows PowerShell 5.1 (Desktop) for Microsoft.PowerApps.Administration.PowerShell."
}
Connect-FsiClouds -Cloud $Cloud -TenantId $TenantId
if (-not (Get-AdminPowerAppEnvironment -EnvironmentName $EnvironmentId -ErrorAction SilentlyContinue)) {
throw "Environment $EnvironmentId not found in this tenant/cloud — verify -Cloud parameter."
}
if ($PSCmdlet.ShouldProcess($EnvironmentId,'Create + link enterprise (network) policy')) {
# Cmdlet names/parameters evolve — verify against current module help before running.
$policy = New-AdminPowerAppEnvironmentEnterprisePolicy `
-EnvironmentName $EnvironmentId `
-DisplayName $PolicyName `
-Type 'NetworkInjection' `
-PrimaryVirtualNetworkSubnetId $PrimarySubnetResourceId `
-FailoverVirtualNetworkSubnetId $FailoverSubnetResourceId
Write-Host "Linked enterprise policy: $($policy.Id)" -ForegroundColor Green
return $policy
}
Why both subnets are required. Microsoft requires a delegated subnet in the Azure region paired with the Power Platform region and in the paired failover region. A single-subnet policy is rejected.
Script 3 — Create Private Endpoints for Key Vault, SQL, Storage, and AMPLS
<#
.SYNOPSIS
Creates a Private Endpoint and DNS zone group for a single dependency resource.
.PARAMETER TargetResourceId ARM resource ID of the dependency (Key Vault, SQL server, Storage, AMPLS).
.PARAMETER GroupId PaaS sub-resource (vault | sqlServer | blob | azuremonitor).
.PARAMETER PrivateDnsZoneId ARM resource ID of the existing Private DNS zone for that group.
.PARAMETER PeResourceGroup Resource group for the PE itself.
.PARAMETER VNetSubnetId ARM resource ID of snet-private-endpoints.
.PARAMETER Location Azure region.
.PARAMETER PeName Private endpoint name (e.g., pe-kv-pp-eastus).
#>
[CmdletBinding(SupportsShouldProcess, ConfirmImpact='Medium')]
param(
[Parameter(Mandatory)] [string] $TargetResourceId,
[Parameter(Mandatory)] [ValidateSet('vault','sqlServer','blob','azuremonitor')] [string] $GroupId,
[Parameter(Mandatory)] [string] $PrivateDnsZoneId,
[Parameter(Mandatory)] [string] $PeResourceGroup,
[Parameter(Mandatory)] [string] $VNetSubnetId,
[Parameter(Mandatory)] [string] $Location,
[Parameter(Mandatory)] [string] $PeName
)
$ErrorActionPreference = 'Stop'
$conn = New-AzPrivateLinkServiceConnection -Name "$PeName-conn" `
-PrivateLinkServiceId $TargetResourceId -GroupId $GroupId
if ($PSCmdlet.ShouldProcess($PeName, "Create private endpoint to $TargetResourceId")) {
$pe = New-AzPrivateEndpoint -ResourceGroupName $PeResourceGroup -Name $PeName `
-Location $Location -Subnet (New-Object Microsoft.Azure.Commands.Network.Models.PSSubnet -Property @{ Id = $VNetSubnetId }) `
-PrivateLinkServiceConnection $conn
$zoneCfg = New-AzPrivateDnsZoneConfig -Name "$GroupId-config" -PrivateDnsZoneId $PrivateDnsZoneId
New-AzPrivateDnsZoneGroup -ResourceGroupName $PeResourceGroup `
-PrivateEndpointName $PeName -Name "default" -PrivateDnsZoneConfig $zoneCfg | Out-Null
Write-Host "Created PE $PeName -> $TargetResourceId ($GroupId)" -ForegroundColor Green
return $pe
}
After creating the PE, disable public network access on the dependency:
# Key Vault
Update-AzKeyVaultNetworkRuleSet -VaultName <name> -ResourceGroupName <rg> -DefaultAction Deny -Bypass None
Update-AzKeyVault -VaultName <name> -ResourceGroupName <rg> -PublicNetworkAccess Disabled
# Azure SQL
Set-AzSqlServer -ResourceGroupName <rg> -ServerName <server> -PublicNetworkAccess "Disabled"
# Storage account
Set-AzStorageAccount -ResourceGroupName <rg> -Name <name> -PublicNetworkAccess Disabled
Pre-flight check. Before disabling public network access, enumerate every consumer (CI/CD agents, Logic Apps, on-prem connectors, developer workstations). Disabling without a plan severs production traffic. Use Azure Resource Graph queries and Key Vault diagnostic logs over the prior 30 days to build the consumer list.
Script 4 — Configure Azure Monitor Private Link Scope (AMPLS) for Application Insights
<#
.SYNOPSIS
Creates an AMPLS, attaches Application Insights / Log Analytics, and locks ingestion / query to private.
#>
[CmdletBinding(SupportsShouldProcess, ConfirmImpact='High')]
param(
[Parameter(Mandatory)] [string] $ResourceGroupName,
[Parameter(Mandatory)] [string] $Location,
[Parameter(Mandatory)] [string] $AmplsName,
[Parameter(Mandatory)] [string[]] $AppInsightsResourceIds,
[Parameter(Mandatory)] [string[]] $LogAnalyticsResourceIds,
[ValidateSet('Open','PrivateOnly')] [string] $IngestionAccessMode = 'PrivateOnly',
[ValidateSet('Open','PrivateOnly')] [string] $QueryAccessMode = 'PrivateOnly'
)
$ErrorActionPreference = 'Stop'
if ($PSCmdlet.ShouldProcess($AmplsName,'Create AMPLS and attach scoped resources')) {
$ampls = New-AzInsightsPrivateLinkScope -ResourceGroupName $ResourceGroupName -Name $AmplsName -Location 'global'
foreach ($rid in $AppInsightsResourceIds + $LogAnalyticsResourceIds) {
$name = ($rid -split '/')[-1]
New-AzInsightsPrivateLinkScopedResource -ResourceGroupName $ResourceGroupName `
-ScopeName $AmplsName -Name "scoped-$name" -LinkedResourceId $rid | Out-Null
}
Update-AzInsightsPrivateLinkScope -ResourceGroupName $ResourceGroupName -Name $AmplsName `
-IngestionAccessMode $IngestionAccessMode -QueryAccessMode $QueryAccessMode | Out-Null
}
Migration tip. Set
IngestionAccessMode = Openuntil every agent host (App Service plan, container, hybrid worker) is confirmed to reach the AMPLS via private endpoint, then tighten toPrivateOnly. Telemetry gaps during migration are common and silent — verify ingestion throughput before and after the switch.
Script 5 — Configure IP Firewall and Cookie Binding (current state)
As of April 2026, IP firewall rules and cookie-binding configuration are portal-managed in PPAC and not exposed by stable, documented Power Apps Administration cmdlets. Microsoft has previewed Power Platform CLI / management-API-based automation; verify what is GA in your tenant before scripting. The script below documents the audit pattern only.
<#
.SYNOPSIS
Documents IP firewall configuration steps for an environment and emits an evidence record.
Does NOT change firewall state — current GA path is the PPAC portal.
#>
param(
[Parameter(Mandatory)] [string] $EnvironmentId,
[Parameter(Mandatory)] [string[]] $AllowedCidrs,
[Parameter(Mandatory)] [string] $EvidencePath
)
$record = [pscustomobject]@{
EnvironmentId = $EnvironmentId
AllowedCidrs = $AllowedCidrs
Mode = 'Enforce'
CookieBinding = 'On'
CapturedUtc = (Get-Date).ToUniversalTime().ToString('o')
CapturedBy = $env:USERNAME
}
$json = $record | ConvertTo-Json -Depth 4
Set-Content -Path $EvidencePath -Value $json -Encoding UTF8
$hash = (Get-FileHash $EvidencePath -Algorithm SHA256).Hash
Add-Content -Path "$EvidencePath.sha256" -Value $hash
Write-Host "Wrote evidence: $EvidencePath (SHA-256 $hash)" -ForegroundColor Green
Script 6 — Validation (read-only)
<#
.SYNOPSIS
Read-only validation of Control 1.20 state for an environment.
Emits a JSON evidence file and SHA-256.
#>
param(
[Parameter(Mandatory)] [string] $EnvironmentId,
[Parameter(Mandatory)] [string] $PrimarySubnetResourceId,
[Parameter(Mandatory)] [string] $FailoverSubnetResourceId,
[Parameter(Mandatory)] [string[]] $DependencyResourceIds,
[Parameter(Mandatory)] [string] $EvidencePath,
[ValidateSet('Commercial','GCC','GCCHigh','DoD')] [string] $Cloud = 'Commercial',
[Parameter(Mandatory)] [string] $TenantId
)
$ErrorActionPreference = 'Stop'
Connect-FsiClouds -Cloud $Cloud -TenantId $TenantId
$results = [ordered]@{
EnvironmentId = $EnvironmentId
Checks = [ordered]@{}
CapturedUtc = (Get-Date).ToUniversalTime().ToString('o')
}
# Check 1: environment is Managed
$env = Get-AdminPowerAppEnvironment -EnvironmentName $EnvironmentId
$results.Checks.IsManagedEnvironment = [bool]$env.IsManaged
# Check 2: enterprise policy linked
$policy = Get-PowerAppEnvironmentEnterprisePolicy -EnvironmentName $EnvironmentId -ErrorAction SilentlyContinue
$results.Checks.EnterprisePolicyLinked = [bool]$policy
$results.Checks.PrimarySubnetMatches = ($policy.Properties.NetworkInjection.virtualNetworks[0].subnetId -eq $PrimarySubnetResourceId)
$results.Checks.FailoverSubnetMatches = ($policy.Properties.NetworkInjection.virtualNetworks[1].subnetId -eq $FailoverSubnetResourceId)
# Check 3: subnet delegation present
foreach ($sid in @($PrimarySubnetResourceId, $FailoverSubnetResourceId)) {
$parts = $sid -split '/'
$rg = $parts[4]; $vnet = $parts[8]; $snet = $parts[10]
$sn = Get-AzVirtualNetworkSubnetConfig -VirtualNetwork (Get-AzVirtualNetwork -ResourceGroupName $rg -Name $vnet) -Name $snet
$results.Checks["DelegationOn_$snet"] = ($sn.Delegations[0].ServiceName -eq 'Microsoft.PowerPlatform/enterprisePolicies')
}
# Check 4: dependency public access disabled
foreach ($rid in $DependencyResourceIds) {
$r = Get-AzResource -ResourceId $rid
$public = $r.Properties.publicNetworkAccess
$results.Checks["PublicAccessDisabled_$($r.Name)"] = ($public -in @('Disabled','SecuredByPerimeter'))
}
$results | ConvertTo-Json -Depth 6 | Set-Content -Path $EvidencePath -Encoding UTF8
$hash = (Get-FileHash $EvidencePath -Algorithm SHA256).Hash
Add-Content -Path "$EvidencePath.sha256" -Value $hash
Write-Host "Validation evidence: $EvidencePath (SHA-256 $hash)" -ForegroundColor Cyan
return $results
Notes on Cmdlet Drift
Microsoft.PowerApps.Administration.PowerShell cmdlets in the network/enterprise-policy area have changed parameter names and aliases between minor versions. If a script in this playbook errors with A parameter cannot be found that matches parameter name '...':
- Run
Get-Command -Module Microsoft.PowerApps.Administration.PowerShell *EnterprisePolicy*andGet-Help <cmdlet> -Full. - Cross-check against the current Microsoft Learn page for VNet setup.
- Pin the working version in your CAB record per the PowerShell baseline §1.
Back to Control 1.20 | Portal Walkthrough | Verification & Testing | Troubleshooting