Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 32 additions & 0 deletions artifacts/PRIVATE_ENDPOINTS_NETWORK_REFERENCE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
# Private Endpoint Diagram Reference

Version: **0.261.333**. Deployer: **1.0.34**. Related issue: [#1765](https://github.com/microsoft/simplechat/issues/1765).

Check warning on line 3 in artifacts/PRIVATE_ENDPOINTS_NETWORK_REFERENCE.md

View workflow job for this annotation

GitHub Actions / malicious-pr-security-review

Important - Changed line contains external connection or remote asset marker. Recommendation%3A Review whether changed code can send prompts, files, credentials, cookies, settings, logs, or user data to a new sink.

Use the [Commercial tables](../docs/guides/enterprise-networking.md#networking-in-azure-commercial) and [Government tables](../docs/guides/enterprise-networking.md#networking-in-azure-government) alongside the Visio sources in this directory. Each section separates fresh script-created networks from the compact existing-network design, with address/usable counts and delegation.

## Fresh Deployment in Either Cloud

Private networking must be enabled, with no existing VNet/subnet IDs supplied.

| Network/Subnet | Address space | Addresses | Azure-usable | Delegation / Notes |
|---|---|---:|---:|---|
| **VNet** | **`10.0.0.0/21`** | **2048** | - | Automatically created |
| `AppServiceIntegration` | `10.0.0.0/24` | 256 | 251 | `Microsoft.Web/serverFarms`; outbound |
| `PrivateEndpoints` | `10.0.2.0/24` | 256 | 251 | All created endpoints, including optional AMPLS |
| **Unallocated** | **`10.0.1.0/24`, `10.0.3.0/24`, `10.0.4.0/22`** | **1536** | - | Future subnets |

## Compact Existing-Network Reference

| Network/Subnet | Address space | Addresses | Azure-usable | Delegation / Notes |
|---|---|---:|---:|---|
| **VNet** | **`10.42.0.0/24`** | **256** | - | Enterprise reference, not script default |
| `snet-app-outbound` | `10.42.0.0/26` | 64 | 59 | `Microsoft.Web/serverFarms` |
| `snet-platform-pe` | `10.42.0.64/26` | 64 | 59 | ACR, Key Vault, optional AMPLS |
| `snet-data-pe` | `10.42.0.128/27` | 32 | 27 | Cosmos, Blob, optional Redis |

Check warning on line 25 in artifacts/PRIVATE_ENDPOINTS_NETWORK_REFERENCE.md

View workflow job for this annotation

GitHub Actions / malicious-pr-security-review

Important - Changed line contains secret or sensitive data source marker. Recommendation%3A Pair this source with any nearby network, logging, serialization, or process execution sink before approving.
| `snet-ai-pe` | `10.42.0.160/27` | 32 | 27 | Supported AI private endpoints |
| `snet-app-inbound-pe` | `10.42.0.192/28` | 16 | 11 | App inbound endpoint |
| **Unallocated** | **`10.42.0.208-10.42.0.255`** | **48** | - | Future growth |

The current scripts accept one integration and one shared endpoint subnet. Existing-network reuse does not automatically distribute endpoints into the reference subnets; segmented placement needs IaC customization or enterprise network tooling. Draw PaaS services outside the VNet and endpoint NICs inside; draw DNS zone links to the VNet rather than nesting zones inside subnets.

Check warning on line 30 in artifacts/PRIVATE_ENDPOINTS_NETWORK_REFERENCE.md

View workflow job for this annotation

GitHub Actions / malicious-pr-security-review

Important - Changed line contains AI, plugin, agent, or workspace boundary marker. Recommendation%3A Check whether prompts, chat history, uploaded documents, embeddings, citations, settings, or identity can cross a new boundary.

Commercial can use Managed Redis and supported Video Indexer private endpoints. Government uses classic Redis and has no Video Indexer PE in the deployed API profile. Both can opt into AMPLS. See the guide's cloud DNS/AMPLS matrix for exact labels. The Visio files remain user-owned and must be updated deliberately to match the selected layout.
Binary file modified artifacts/architecture.vsdx
Binary file not shown.
Binary file modified artifacts/private_endpoints.vsdx
Binary file not shown.
20 changes: 20 additions & 0 deletions deployers/azure.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -900,6 +900,16 @@
fi

echo ""
if [ -n "${var_redisCacheResourceId}" ]; then
redis_api_version="2024-11-01"
if [ "${var_redisCacheKind}" = "managed" ]; then
redis_api_version="2025-07-01"
fi
echo "Disabling public network access for Redis..."
az resource update --ids "${var_redisCacheResourceId}" \
--api-version "${redis_api_version}" \
--set properties.publicNetworkAccess=Disabled > /dev/null

Check warning on line 911 in deployers/azure.yaml

View workflow job for this annotation

GitHub Actions / malicious-pr-security-review

Important - Changed line contains security control, sanitization, or audit marker. Recommendation%3A Confirm the change does not weaken auth, CSRF, CSP, XSS defenses, settings sanitization, redaction, audit logging, or tests.
fi
echo "✓ Private networking configured successfully"
else
echo ""
Expand Down Expand Up @@ -943,6 +953,8 @@
'var_cosmosDb_uri',
'var_enablePrivateNetworking',
'var_keyVaultName',
'var_redisCacheResourceId',
'var_redisCacheKind',
'var_rgName',
'var_subscriptionId',
'var_webService'
Expand Down Expand Up @@ -1086,6 +1098,14 @@
}
Write-Host "✓ Web Application public access disabled"

if (-not [string]::IsNullOrWhiteSpace($env:var_redisCacheResourceId)) {
$redisApiVersion = if ($env:var_redisCacheKind -eq 'managed') { '2025-07-01' } else { '2024-11-01' }
Write-Host 'Disabling public network access for Redis...'
az resource update --ids $env:var_redisCacheResourceId --api-version $redisApiVersion --set properties.publicNetworkAccess=Disabled | Out-Null

Check warning on line 1104 in deployers/azure.yaml

View workflow job for this annotation

GitHub Actions / malicious-pr-security-review

Important - Changed line contains security control, sanitization, or audit marker. Recommendation%3A Confirm the change does not weaken auth, CSRF, CSP, XSS defenses, settings sanitization, redaction, audit logging, or tests.
if ($LASTEXITCODE -ne 0) {
throw 'Failed to disable Redis public access.'

Check warning on line 1106 in deployers/azure.yaml

View workflow job for this annotation

GitHub Actions / malicious-pr-security-review

Important - Changed line contains security control, sanitization, or audit marker. Recommendation%3A Confirm the change does not weaken auth, CSRF, CSP, XSS defenses, settings sanitization, redaction, audit logging, or tests.
}
}
Write-Host ""
Write-Host "✓ Private networking configured successfully"
} else {
Expand Down
9 changes: 9 additions & 0 deletions deployers/azurecli/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -201,6 +201,7 @@
- Azure OpenAI GPT and embedding model names, versions, deployment names, and capacities
- optional Entra security group creation
- `param_EnablePrivateNetworking`
- `param_EnableAzureMonitorPrivateLink` (default false; requires private networking)
- `param_ExistingVirtualNetworkId`
- `param_ExistingAppServiceSubnetId`
- `param_ExistingPrivateEndpointSubnetId`
Expand All @@ -211,6 +212,14 @@

Review those values before running the script.

### AMPLS and Fresh Versus Existing Networks

Set `$param_EnableAzureMonitorPrivateLink = $true` to opt into private-only Application Insights and Log Analytics in either cloud. The script deploys the generated [shared template](../bicep/modules/azureMonitorPrivateNetworking.json), then disables public ingestion/queries only after that deployment succeeds. Keep the adjacent Bicep directory available. Monitoring clients need private routing/DNS and a `/27` or larger endpoint subnet with sufficient free IPs. Use `/26` or larger when sharing a platform subnet with ACR and Key Vault. Coordinate one AMPLS per shared DNS boundary.

Check warning on line 217 in deployers/azurecli/README.md

View workflow job for this annotation

GitHub Actions / malicious-pr-security-review

Important - Changed line contains dynamic execution, persistence, or system access marker. Recommendation%3A Do not execute changed lifecycle scripts or installers while this finding is unresolved.

Check warning on line 217 in deployers/azurecli/README.md

View workflow job for this annotation

GitHub Actions / malicious-pr-security-review

Important - Changed line contains external connection or remote asset marker. Recommendation%3A Review whether changed code can send prompts, files, credentials, cookies, settings, logs, or user data to a new sink.

A fresh private-network deployment without existing network IDs creates `10.0.0.0/21` and two `/24` subnets. The [Commercial tables](../../docs/guides/enterprise-networking.md#networking-in-azure-commercial) and [Government tables](../../docs/guides/enterprise-networking.md#networking-in-azure-government) show script defaults and the compact `10.42.0.0/24` enterprise reference. Existing-network reuse still directs endpoints to one supplied subnet; segmented placement requires customization.

DNS reuse accepts `monitor`, `monitorOms`, `monitorOds`, `monitorAgent`, and `monitorAdx` (Government), with `blobStorage` and `videoIndexer`, using `zoneResourceId`/`createVNetLink`. Government Key Vault uses `privatelink.vaultcore.usgovcloudapi.net`; Commercial Video Indexer uses `privatelink.api.videoindexer.ai`. The Government Video Indexer profile has no PE, and this script does not provision Redis.

## Azure OpenAI deployment types and quota handling

The Azure CLI deployer can now create the default GPT and embedding model deployments for the Azure OpenAI account it creates or reuses. The Azure OpenAI account still uses the Cognitive Services account SKU, but the model deployments themselves use `Standard`, `DatazoneStandard`, or `GlobalStandard`.
Expand Down
67 changes: 58 additions & 9 deletions deployers/azurecli/deploy-simplechat.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -107,7 +107,7 @@ For Azure Government, use the corresponding sovereign-cloud private DNS zones su
- `privatelink.search.azure.us`
- `privatelink.openai.azure.us`
- `privatelink.cognitiveservices.azure.us`
- `privatelink.vaultcore.azure.us`
- `privatelink.vaultcore.usgovcloudapi.net`
- `privatelink.azurecr.us`

For detailed private networking guidance, see:
Expand Down Expand Up @@ -217,6 +217,7 @@ $param_Existing_ResourceGroupName = "RG-SimpleChat-Experiemental" # Leave empty

# Private networking
$param_EnablePrivateNetworking = $false
$param_EnableAzureMonitorPrivateLink = $false
$param_ExistingVirtualNetworkId = "" # Optional existing VNet resource ID to reuse.
$param_ExistingAppServiceSubnetId = "" # Required when reusing an existing VNet for App Service regional integration.
$param_ExistingPrivateEndpointSubnetId = "" # Required when reusing an existing VNet for private endpoints.
Expand Down Expand Up @@ -293,7 +294,7 @@ if ($globalWhichAzurePlatform -eq "AzureCloud") {
cognitiveServices = "privatelink.cognitiveservices.azure.us"
containerRegistry = "privatelink.azurecr.us"
cosmosDb = "privatelink.documents.azure.us"
keyVault = "privatelink.vaultcore.azure.us"
keyVault = "privatelink.vaultcore.usgovcloudapi.net"
openAi = "privatelink.openai.azure.us"
webSites = "privatelink.azurewebsites.us"
}
Expand Down Expand Up @@ -329,7 +330,12 @@ if ($globalWhichAzurePlatform -eq "AzureCloud") {
$paramDateTime_ScriptStart = Get-Date
$paramDateTimeStamp = ($paramDateTime_ScriptStart).ToString("yyyy-MM-dd HH:mm:ss")

$script:SupportedPrivateDnsZoneKeys = @("keyVault", "cosmosDb", "containerRegistry", "aiSearch", "blobStorage", "cognitiveServices", "openAi", "webSites")
$script:SupportedPrivateDnsZoneKeys = @("keyVault", "cosmosDb", "containerRegistry", "aiSearch", "blobStorage", "cognitiveServices", "openAi", "webSites", "videoIndexer", "monitor", "monitorOms", "monitorOds", "monitorAgent", "monitorAdx")
$monitorDnsNames = Get-Content (Join-Path $PSScriptRoot '../bicep/modules/privateDNSZones.json') -Raw | ConvertFrom-Json -AsHashtable
$dnsCloud = if ($globalWhichAzurePlatform -eq 'AzureUSGovernment') { 'azureusgovernment' } else { 'azurecloud' }
foreach ($zoneKey in @('videoIndexer', 'monitor', 'monitorOms', 'monitorOds', 'monitorAgent', 'monitorAdx')) {
$script:PrivateDnsZoneNames[$zoneKey] = $monitorDnsNames[$dnsCloud][$zoneKey]
}
$script:PrivateDnsZoneLinkSuffixes = @{
aiSearch = "searchService"
blobStorage = "storage"
Expand All @@ -339,6 +345,7 @@ $script:PrivateDnsZoneLinkSuffixes = @{
keyVault = "kv"
openAi = "openAiService"
webSites = "webApp"
videoIndexer = "videoIndexer"
}
$script:UseExistingPrivateNetwork = $false
$script:ResolvedVirtualNetworkId = ""
Expand Down Expand Up @@ -1083,7 +1090,10 @@ function Show-PrivateNetworkingChecklist {

Write-Host "`nPrivate DNS zone plan:" -ForegroundColor Yellow
foreach ($zoneKey in $script:SupportedPrivateDnsZoneKeys) {
if ($zoneKey -like 'monitor*' -and -not $param_EnableAzureMonitorPrivateLink) { continue }
if ($zoneKey -eq 'videoIndexer' -and (-not $param_DeployVideoIndexerService -or $param_VideoIndexerArmApiVersion -ne '2025-04-01' -or $globalWhichAzurePlatform -eq 'AzureUSGovernment')) { continue }
$zoneConfig = Get-PrivateDnsZoneConfig -ZoneKey $zoneKey
if ([string]::IsNullOrWhiteSpace($zoneConfig.ZoneName)) { continue }
if ([string]::IsNullOrWhiteSpace($zoneConfig.ZoneResourceId)) {
$zoneAction = "create in deployment resource group"
} else {
Expand All @@ -1109,6 +1119,9 @@ function Show-PrivateNetworkingChecklist {
}

function Test-PrivateNetworkingConfiguration {
if ($param_EnableAzureMonitorPrivateLink -and -not $param_EnablePrivateNetworking) {
throw 'param_EnableAzureMonitorPrivateLink requires param_EnablePrivateNetworking=true.'
}
if (-not $param_EnablePrivateNetworking) {
return
}
Expand Down Expand Up @@ -1281,12 +1294,9 @@ function Ensure-PrivateEndpoint {
if ($LASTEXITCODE -ne 0) { Write-Error "Failed to create private endpoint '$privateEndpointName'."; exit 1 }
}

$existingDnsZoneGroup = az network private-endpoint dns-zone-group show --resource-group $resourceGroupName --endpoint-name $privateEndpointName --name $dnsZoneGroupName --query id --output tsv 2>$null
if (-not $existingDnsZoneGroup) {
Write-Host "Associating private endpoint '$privateEndpointName' with private DNS zone '$($zoneConfig.ZoneName)'."
az network private-endpoint dns-zone-group create --resource-group $resourceGroupName --endpoint-name $privateEndpointName --name $dnsZoneGroupName --private-dns-zone $zoneResourceId --zone-name $zoneConfig.ZoneName | Out-Null
if ($LASTEXITCODE -ne 0) { Write-Error "Failed to create the DNS zone group for private endpoint '$privateEndpointName'."; exit 1 }
}
Write-Host "Ensuring private endpoint '$privateEndpointName' uses private DNS zone '$($zoneConfig.ZoneName)'."
az network private-endpoint dns-zone-group create --resource-group $resourceGroupName --endpoint-name $privateEndpointName --name $dnsZoneGroupName --private-dns-zone $zoneResourceId --zone-name $zoneConfig.ZoneName | Out-Null
if ($LASTEXITCODE -ne 0) { Write-Error "Failed to reconcile the DNS zone group for private endpoint '$privateEndpointName'."; exit 1 }
}

function Update-ResourcePropertiesById {
Expand Down Expand Up @@ -1318,6 +1328,16 @@ function Configure-PrivateNetworkingForDeployment {

Ensure-PrivateNetworkingInfrastructure

if ($param_EnableAzureMonitorPrivateLink) {
$subnetJson = az network vnet subnet show --ids $script:ResolvedPrivateEndpointSubnetId --output json
if ($LASTEXITCODE -ne 0) { throw 'Unable to validate the AMPLS subnet.' }
$subnet = $subnetJson | ConvertFrom-Json
$prefixes = if ($subnet.addressPrefixes) { @($subnet.addressPrefixes) } else { @($subnet.addressPrefix) }
$supportedPrefixes = @($prefixes | Where-Object { $_ -match '^\d+\.\d+\.\d+\.\d+/(\d+)$' -and [int]$Matches[1] -le 27 })
if ($supportedPrefixes.Count -eq 0) { throw 'AMPLS requires an IPv4 subnet of /27 or larger; use /26 or larger when sharing with platform endpoints.' }
Write-Host 'AMPLS opted in: monitoring ingestion and queries will require private routing and DNS. Coordinate one AMPLS per shared DNS boundary.'
}

Write-Host "`n=====> Configuring App Service VNet integration and web app network settings..."
Update-ResourcePropertiesById -ResourceId $(az webapp show --name $appServiceName --resource-group $resourceGroupName --query id --output tsv) -PropertyAssignments @(
"properties.publicNetworkAccess=Disabled",
Expand Down Expand Up @@ -1345,6 +1365,35 @@ function Configure-PrivateNetworkingForDeployment {
Ensure-PrivateEndpoint -NameSuffix "docintel" -ServiceResourceId $docIntelResourceId -GroupId "account" -ZoneKey "cognitiveServices"
Ensure-PrivateEndpoint -NameSuffix "webapp" -ServiceResourceId $webAppResourceId -GroupId "sites" -ZoneKey "webSites"

if ($param_DeployVideoIndexerService -and $param_VideoIndexerArmApiVersion -eq '2025-04-01' -and $globalWhichAzurePlatform -ne 'AzureUSGovernment') {
$videoIndexerResourceId = az resource show --resource-group $resourceGroupName --name $videoIndexerName --resource-type Microsoft.VideoIndexer/accounts --api-version $param_VideoIndexerArmApiVersion --query id -o tsv
if ($LASTEXITCODE -ne 0 -or [string]::IsNullOrWhiteSpace($videoIndexerResourceId)) { throw 'Unable to resolve Video Indexer for its private endpoint.' }
Ensure-PrivateEndpoint -NameSuffix 'videoIndexer' -ServiceResourceId $videoIndexerResourceId -GroupId 'account' -ZoneKey 'videoIndexer'
}

if ($param_EnableAzureMonitorPrivateLink) {
$appInsightsResourceId = az monitor app-insights component show --app $appInsightsName --resource-group $resourceGroupName --query id -o tsv
if ($LASTEXITCODE -ne 0 -or [string]::IsNullOrWhiteSpace($appInsightsResourceId) -or [string]::IsNullOrWhiteSpace($logAnalyticsWorkspaceId)) { throw 'Unable to resolve monitoring resources for AMPLS.' }
$monitorParameters = @{
location = @{ value = $paramLocation }
appName = @{ value = $paramBaseName }
environment = @{ value = $paramEnvironment }
tags = @{ value = $tags }
virtualNetworkId = @{ value = $script:ResolvedVirtualNetworkId }
privateEndpointSubnetId = @{ value = $script:ResolvedPrivateEndpointSubnetId }
privateDnsZoneConfigs = @{ value = $param_PrivateDnsZoneConfigs }
blobStorageDnsZoneId = @{ value = (Ensure-PrivateDnsZone -ZoneKey 'blobStorage') }
monitoringResourceIds = @{ value = @($logAnalyticsWorkspaceId, $appInsightsResourceId) }
} | ConvertTo-Json -Depth 20 -Compress
$monitorTemplate = Join-Path $PSScriptRoot '../bicep/modules/azureMonitorPrivateNetworking.json'
az deployment group create --resource-group $resourceGroupName --name azureMonitorPrivateNetworking --template-file $monitorTemplate --parameters $monitorParameters | Out-Null
if ($LASTEXITCODE -ne 0) { throw 'Failed to deploy AMPLS; monitoring public access has not been disabled.' }
foreach ($monitorResourceId in @($logAnalyticsWorkspaceId, $appInsightsResourceId)) {
az resource update --ids $monitorResourceId --set properties.publicNetworkAccessForIngestion=Disabled properties.publicNetworkAccessForQuery=Disabled | Out-Null
if ($LASTEXITCODE -ne 0) { throw 'Failed to disable monitoring public access.' }
}
}

if (-not [string]::IsNullOrWhiteSpace($OpenAiResourceId)) {
Ensure-PrivateEndpoint -NameSuffix "openai" -ServiceResourceId $OpenAiResourceId -GroupId "account" -ZoneKey "openAi"
} else {
Expand Down
10 changes: 9 additions & 1 deletion deployers/bicep/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -498,7 +498,15 @@ Example `privateDnsZoneConfigs` value:
}
```

Supported `privateDnsZoneConfigs` keys: `keyVault`, `cosmosDb`, `containerRegistry`, `aiSearch`, `blobStorage`, `cognitiveServices`, `openAi`, and `webSites`.
Supported `privateDnsZoneConfigs` keys: `keyVault`, `cosmosDb`, `containerRegistry`, `aiSearch`, `blobStorage`, `cognitiveServices`, `openAi`, `webSites`, `redis`, `videoIndexer`, `monitor`, `monitorOms`, `monitorOds`, `monitorAgent`, and `monitorAdx` (Government).

#### Optional AMPLS and Subnet References

Implemented in version **0.261.333**, deployer **1.0.34** ([#1765](https://github.com/microsoft/simplechat/issues/1765)). `enableAzureMonitorPrivateLink` defaults to false. Opt in with `azd env set ENABLE_AZURE_MONITOR_PRIVATE_LINK true` and `ENABLE_PRIVATE_NETWORKING=true`. This creates one AMPLS endpoint, associates Application Insights and Log Analytics, and disables public ingestion and queries. Use a `/27` or larger endpoint subnet; `/26` or larger is recommended when sharing with platform endpoints. All monitoring clients need private routing and DNS; coordinate one AMPLS per shared DNS boundary.

A **fresh private-network deployment without existing network IDs** creates `10.0.0.0/21`, `AppServiceIntegration` at `10.0.0.0/24`, and `PrivateEndpoints` at `10.0.2.0/24`, in either cloud. See the [Commercial tables](../../docs/guides/enterprise-networking.md#networking-in-azure-commercial) and [Government tables](../../docs/guides/enterprise-networking.md#networking-in-azure-government) for counts and the compact `10.42.0.0/24` existing-network reference. Existing-VNet reuse still accepts one shared endpoint subnet; the segmented reference requires endpoint placement customization.

Optional Redis now gets an offering-aware endpoint (`redisEnterprise` for Managed Redis, `redisCache` for classic). `azd up` disables public access during `postup`, after bootstrap; `azd provision` or `azd deploy` alone does not run this final lockdown. Later provisioning resets Managed Redis bootstrap access and needs another lockdown. Commercial Video Indexer uses `privatelink.api.videoindexer.ai`; Government's API profile still has no Video Indexer PE. Government Key Vault uses `privatelink.vaultcore.usgovcloudapi.net`.

If you use AZD, set `PRIVATE_DNS_ZONE_CONFIGS` to the JSON object above instead of editing the parameter file directly.

Expand Down
Loading
Loading