From 6bb94a02069c8b1ca2a687a4ed15437f1b348f8f Mon Sep 17 00:00:00 2001 From: "Ashwal Vishwanath (Persistent Systems Inc)" Date: Thu, 23 Apr 2026 01:48:35 +0530 Subject: [PATCH 1/4] Harden WAF deployment: private backend ingress and frontend proxy routing --- docs/DeploymentGuide.md | 7 ++++ docs/TechnicalArchitecture.md | 2 ++ infra/main.bicep | 39 +++++++++++++++++++++-- infra/main_custom.bicep | 39 +++++++++++++++++++++-- src/ContentProcessorWeb/env.sh | 1 + src/ContentProcessorWeb/nginx-custom.conf | 12 +++++++ 6 files changed, 94 insertions(+), 6 deletions(-) diff --git a/docs/DeploymentGuide.md b/docs/DeploymentGuide.md index 94c3d2f3..05e838a5 100644 --- a/docs/DeploymentGuide.md +++ b/docs/DeploymentGuide.md @@ -194,6 +194,13 @@ Review the configuration options below. You can customize any settings that meet | **Framework** | Basic configuration | [Well-Architected Framework](https://learn.microsoft.com/en-us/azure/well-architected/) | | **Features** | Core functionality | Reliability, security, operational excellence | +When using the Production/WAF deployment (`enablePrivateNetworking=true`), networking is configured as follows: + +- Backend Container App endpoints are internal-only (`ingressExternal=false`) and not publicly reachable. +- Container Apps Environment is deployed in internal mode with VNet integration. +- The web frontend remains public and routes browser API traffic through same-origin `/api` proxying to the private backend over VNet. +- Private DNS is configured for the internal Container Apps Environment domain. + **To use production configuration:** Copy the contents from the production configuration file to your main parameters file: diff --git a/docs/TechnicalArchitecture.md b/docs/TechnicalArchitecture.md index dce44b65..f2005e29 100644 --- a/docs/TechnicalArchitecture.md +++ b/docs/TechnicalArchitecture.md @@ -177,6 +177,8 @@ The API also provides schema management, schema set (collection) management, and ### Claim Process Monitor Web Using Azure Container App, this app acts as the UI for the process monitoring queue. The app is built with React and TypeScript. It acts as an API client to create an experience for uploading new documents, creating and managing claim batches, monitoring current and historical processes, and reviewing output results including summarization and gap analysis. +In WAF/private networking deployments (`enablePrivateNetworking=true`), the frontend remains public while backend APIs are internal-only. The web container proxies `/api/*` traffic to the private API Container App over VNet so backend endpoints are not directly exposed to the public internet. + ### App Configuration Using Azure App Configuration, app settings and configurations are centralized and used with the Content Processor, Content Process API, Content Process Workflow, and Claim Process Monitor Web. diff --git a/infra/main.bicep b/infra/main.bicep index 1c815935..276a68bc 100644 --- a/infra/main.bicep +++ b/infra/main.bicep @@ -931,7 +931,8 @@ module avmContainerAppEnv 'br/public:avm/res/app/managed-environment:0.13.2' = { } ] enableTelemetry: enableTelemetry - publicNetworkAccess: 'Enabled' // Always enabled for Container Apps Environment + publicNetworkAccess: enablePrivateNetworking ? 'Disabled' : 'Enabled' + internal: enablePrivateNetworking ? true : false // <========== WAF related parameters @@ -944,6 +945,34 @@ module avmContainerAppEnv 'br/public:avm/res/app/managed-environment:0.13.2' = { } } +// ========== Private DNS Zone for internal Container App Environment ========== // +// When the CAE is internal, its FQDN is resolvable only within the VNet via this zone. +module caeDnsZone 'br/public:avm/res/network/private-dns-zone:0.8.0' = if (enablePrivateNetworking) { + name: take('avm.res.network.private-dns-zone.cae.${solutionSuffix}', 64) + params: { + name: avmContainerAppEnv.outputs.defaultDomain + tags: tags + enableTelemetry: enableTelemetry + a: [ + { + name: '*' + aRecords: [ + { + ipv4Address: avmContainerAppEnv.outputs.staticIp + } + ] + ttl: 300 + } + ] + virtualNetworkLinks: [ + { + name: take('vnetlink-${virtualNetworkResourceName}-cae', 64) + virtualNetworkResourceId: virtualNetwork!.outputs.resourceId + } + ] + } +} + // //=========== Managed Identity for Container Registry ========== // module avmContainerRegistryReader 'br/public:avm/res/managed-identity/user-assigned-identity:0.5.0' = { name: take('avm.res.managed-identity.user-assigned-identity.${solutionSuffix}', 64) @@ -1132,7 +1161,7 @@ module avmContainerApp_API 'br/public:avm/res/app/container-app:0.22.1' = { } ] } - ingressExternal: true + ingressExternal: enablePrivateNetworking ? false : true activeRevisionsMode: 'Single' ingressTransport: 'auto' corsPolicy: { @@ -1201,6 +1230,10 @@ module avmContainerApp_Web 'br/public:avm/res/app/container-app:0.22.1' = { env: [ { name: 'APP_API_BASE_URL' + value: enablePrivateNetworking ? '/api' : 'https://${avmContainerApp_API.outputs.fqdn}' + } + { + name: 'APP_BACKEND_API_URL' value: 'https://${avmContainerApp_API.outputs.fqdn}' } { @@ -1808,7 +1841,7 @@ module avmContainerApp_API_update 'br/public:avm/res/app/container-app:0.22.1' = } ] } - ingressExternal: true + ingressExternal: enablePrivateNetworking ? false : true activeRevisionsMode: 'Single' ingressTransport: 'auto' corsPolicy: { diff --git a/infra/main_custom.bicep b/infra/main_custom.bicep index 7696c022..25287019 100644 --- a/infra/main_custom.bicep +++ b/infra/main_custom.bicep @@ -934,7 +934,8 @@ module avmContainerAppEnv 'br/public:avm/res/app/managed-environment:0.13.2' = { } ] enableTelemetry: enableTelemetry - publicNetworkAccess: 'Enabled' // Always enabled for Container Apps Environment + publicNetworkAccess: enablePrivateNetworking ? 'Disabled' : 'Enabled' + internal: enablePrivateNetworking ? true : false // <========== WAF related parameters @@ -947,6 +948,34 @@ module avmContainerAppEnv 'br/public:avm/res/app/managed-environment:0.13.2' = { } } +// ========== Private DNS Zone for internal Container App Environment ========== // +// When the CAE is internal, its FQDN is resolvable only within the VNet via this zone. +module caeDnsZone 'br/public:avm/res/network/private-dns-zone:0.8.0' = if (enablePrivateNetworking) { + name: take('avm.res.network.private-dns-zone.cae.${solutionSuffix}', 64) + params: { + name: avmContainerAppEnv.outputs.defaultDomain + tags: tags + enableTelemetry: enableTelemetry + a: [ + { + name: '*' + aRecords: [ + { + ipv4Address: avmContainerAppEnv.outputs.staticIp + } + ] + ttl: 300 + } + ] + virtualNetworkLinks: [ + { + name: take('vnetlink-${virtualNetworkResourceName}-cae', 64) + virtualNetworkResourceId: virtualNetwork!.outputs.resourceId + } + ] + } +} + // //=========== Managed Identity for Container Registry ========== // module avmContainerRegistryReader 'br/public:avm/res/managed-identity/user-assigned-identity:0.5.0' = { name: take('avm.res.managed-identity.user-assigned-identity.${solutionSuffix}', 64) @@ -1145,7 +1174,7 @@ module avmContainerApp_API 'br/public:avm/res/app/container-app:0.22.1' = { } ] } - ingressExternal: true + ingressExternal: enablePrivateNetworking ? false : true activeRevisionsMode: 'Single' ingressTransport: 'auto' corsPolicy: { @@ -1219,6 +1248,10 @@ module avmContainerApp_Web 'br/public:avm/res/app/container-app:0.22.1' = { env: [ { name: 'APP_API_BASE_URL' + value: enablePrivateNetworking ? '/api' : 'https://${avmContainerApp_API.outputs.fqdn}' + } + { + name: 'APP_BACKEND_API_URL' value: 'https://${avmContainerApp_API.outputs.fqdn}' } { @@ -1841,7 +1874,7 @@ module avmContainerApp_API_update 'br/public:avm/res/app/container-app:0.22.1' = } ] } - ingressExternal: true + ingressExternal: enablePrivateNetworking ? false : true activeRevisionsMode: 'Single' ingressTransport: 'auto' corsPolicy: { diff --git a/src/ContentProcessorWeb/env.sh b/src/ContentProcessorWeb/env.sh index 8346ce2b..31b2a989 100644 --- a/src/ContentProcessorWeb/env.sh +++ b/src/ContentProcessorWeb/env.sh @@ -6,5 +6,6 @@ do echo $key=$value # Use sed to replace only the exact matches of the key find /usr/share/nginx/html -type f -exec sed -i "s|\b${key}\b|${value}|g" '{}' + + sed -i "s|\b${key}\b|${value}|g" /etc/nginx/nginx.conf done echo 'done' \ No newline at end of file diff --git a/src/ContentProcessorWeb/nginx-custom.conf b/src/ContentProcessorWeb/nginx-custom.conf index 1ff688eb..1980c18e 100644 --- a/src/ContentProcessorWeb/nginx-custom.conf +++ b/src/ContentProcessorWeb/nginx-custom.conf @@ -18,6 +18,18 @@ http { listen 3000; server_name localhost; + # Route browser API calls through the web container so private backend + # endpoints remain internal-only in WAF/private networking deployments. + location /api/ { + proxy_http_version 1.1; + proxy_set_header Host $host; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection "upgrade"; + proxy_pass APP_BACKEND_API_URL/; + } + location / { root /usr/share/nginx/html; try_files $uri $uri/ /index.html; From 1fd18c7fee5b69774327c2fc0e6ace2d221ef1d2 Mon Sep 17 00:00:00 2001 From: "Ashwal Vishwanath (Persistent Systems Inc)" Date: Thu, 23 Apr 2026 11:52:37 +0530 Subject: [PATCH 2/4] Update infra Bicep templates and parameters --- infra/main.bicep | 2 +- infra/main.parameters.json | 18 ++++++++++++++++++ infra/main_custom.bicep | 2 +- 3 files changed, 20 insertions(+), 2 deletions(-) diff --git a/infra/main.bicep b/infra/main.bicep index 276a68bc..8edaf0f4 100644 --- a/infra/main.bicep +++ b/infra/main.bicep @@ -966,7 +966,7 @@ module caeDnsZone 'br/public:avm/res/network/private-dns-zone:0.8.0' = if (enabl ] virtualNetworkLinks: [ { - name: take('vnetlink-${virtualNetworkResourceName}-cae', 64) + name: take('vnetlink-vnet-${solutionSuffix}-cae', 64) virtualNetworkResourceId: virtualNetwork!.outputs.resourceId } ] diff --git a/infra/main.parameters.json b/infra/main.parameters.json index 27461ece..7fdeab31 100644 --- a/infra/main.parameters.json +++ b/infra/main.parameters.json @@ -32,6 +32,24 @@ "existingFoundryProjectResourceId": { "value": "${AZURE_EXISTING_AIPROJECT_RESOURCE_ID}" }, + "enableMonitoring": { + "value": true + }, + "enablePrivateNetworking": { + "value": true + }, + "enableScalability": { + "value": true + }, + "vmAdminUsername": { + "value": "${AZURE_ENV_VM_ADMIN_USERNAME}" + }, + "vmAdminPassword": { + "value": "${AZURE_ENV_VM_ADMIN_PASSWORD}" + }, + "vmSize": { + "value": "${AZURE_ENV_VM_SIZE}" + }, "containerRegistryEndpoint": { "value": "${AZURE_ENV_CONTAINER_REGISTRY_ENDPOINT}" }, diff --git a/infra/main_custom.bicep b/infra/main_custom.bicep index 25287019..343e860c 100644 --- a/infra/main_custom.bicep +++ b/infra/main_custom.bicep @@ -969,7 +969,7 @@ module caeDnsZone 'br/public:avm/res/network/private-dns-zone:0.8.0' = if (enabl ] virtualNetworkLinks: [ { - name: take('vnetlink-${virtualNetworkResourceName}-cae', 64) + name: take('vnetlink-vnet-${solutionSuffix}-cae', 64) virtualNetworkResourceId: virtualNetwork!.outputs.resourceId } ] From 8c0985c82daffd65d3cf929f17d294df39c119d1 Mon Sep 17 00:00:00 2001 From: "Ashwal Vishwanath (Persistent Systems Inc)" Date: Thu, 7 May 2026 18:18:39 +0530 Subject: [PATCH 3/4] fix: resolve nginx 405 on POST and CAE internal mode issues - Change proxy_set_header Host from $host to $proxy_host so Container Apps ingress routes to the API app (not back to web app) - Add client_max_body_size 100m and proxy timeouts to nginx config - Add safe default for APP_BACKEND_API_URL in env.sh - Revert CAE to external mode (internal: false) - only API ingress is internal; web app remains publicly accessible without App Gateway - Remove unnecessary Private DNS Zone for CAE - Update post-deployment scripts to use web app /api proxy when private networking is enabled (instead of calling unreachable internal API) - Add ENABLE_PRIVATE_NETWORKING bicep output for post-deployment detection Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- infra/main.bicep | 35 ++++------------------- infra/main_custom.bicep | 35 ++++------------------- infra/scripts/post_deployment.ps1 | 17 ++++++++++- infra/scripts/post_deployment.sh | 14 ++++++++- src/ContentProcessorWeb/env.sh | 6 ++++ src/ContentProcessorWeb/nginx-custom.conf | 11 ++++++- 6 files changed, 55 insertions(+), 63 deletions(-) diff --git a/infra/main.bicep b/infra/main.bicep index 8edaf0f4..917088ef 100644 --- a/infra/main.bicep +++ b/infra/main.bicep @@ -931,8 +931,8 @@ module avmContainerAppEnv 'br/public:avm/res/app/managed-environment:0.13.2' = { } ] enableTelemetry: enableTelemetry - publicNetworkAccess: enablePrivateNetworking ? 'Disabled' : 'Enabled' - internal: enablePrivateNetworking ? true : false + publicNetworkAccess: 'Enabled' + internal: false // <========== WAF related parameters @@ -945,34 +945,6 @@ module avmContainerAppEnv 'br/public:avm/res/app/managed-environment:0.13.2' = { } } -// ========== Private DNS Zone for internal Container App Environment ========== // -// When the CAE is internal, its FQDN is resolvable only within the VNet via this zone. -module caeDnsZone 'br/public:avm/res/network/private-dns-zone:0.8.0' = if (enablePrivateNetworking) { - name: take('avm.res.network.private-dns-zone.cae.${solutionSuffix}', 64) - params: { - name: avmContainerAppEnv.outputs.defaultDomain - tags: tags - enableTelemetry: enableTelemetry - a: [ - { - name: '*' - aRecords: [ - { - ipv4Address: avmContainerAppEnv.outputs.staticIp - } - ] - ttl: 300 - } - ] - virtualNetworkLinks: [ - { - name: take('vnetlink-vnet-${solutionSuffix}-cae', 64) - virtualNetworkResourceId: virtualNetwork!.outputs.resourceId - } - ] - } -} - // //=========== Managed Identity for Container Registry ========== // module avmContainerRegistryReader 'br/public:avm/res/managed-identity/user-assigned-identity:0.5.0' = { name: take('avm.res.managed-identity.user-assigned-identity.${solutionSuffix}', 64) @@ -1971,5 +1943,8 @@ output CONTAINER_REGISTRY_LOGIN_SERVER string = avmContainerRegistry.outputs.log @description('The name of the Content Understanding AI Services account.') output CONTENT_UNDERSTANDING_ACCOUNT_NAME string = avmAiServices_cu.outputs.name +@description('Whether private networking (WAF) is enabled.') +output ENABLE_PRIVATE_NETWORKING bool = enablePrivateNetworking + @description('The resource group the resources were deployed into.') output AZURE_RESOURCE_GROUP string = resourceGroup().name diff --git a/infra/main_custom.bicep b/infra/main_custom.bicep index 343e860c..f5befa51 100644 --- a/infra/main_custom.bicep +++ b/infra/main_custom.bicep @@ -934,8 +934,8 @@ module avmContainerAppEnv 'br/public:avm/res/app/managed-environment:0.13.2' = { } ] enableTelemetry: enableTelemetry - publicNetworkAccess: enablePrivateNetworking ? 'Disabled' : 'Enabled' - internal: enablePrivateNetworking ? true : false + publicNetworkAccess: 'Enabled' + internal: false // <========== WAF related parameters @@ -948,34 +948,6 @@ module avmContainerAppEnv 'br/public:avm/res/app/managed-environment:0.13.2' = { } } -// ========== Private DNS Zone for internal Container App Environment ========== // -// When the CAE is internal, its FQDN is resolvable only within the VNet via this zone. -module caeDnsZone 'br/public:avm/res/network/private-dns-zone:0.8.0' = if (enablePrivateNetworking) { - name: take('avm.res.network.private-dns-zone.cae.${solutionSuffix}', 64) - params: { - name: avmContainerAppEnv.outputs.defaultDomain - tags: tags - enableTelemetry: enableTelemetry - a: [ - { - name: '*' - aRecords: [ - { - ipv4Address: avmContainerAppEnv.outputs.staticIp - } - ] - ttl: 300 - } - ] - virtualNetworkLinks: [ - { - name: take('vnetlink-vnet-${solutionSuffix}-cae', 64) - virtualNetworkResourceId: virtualNetwork!.outputs.resourceId - } - ] - } -} - // //=========== Managed Identity for Container Registry ========== // module avmContainerRegistryReader 'br/public:avm/res/managed-identity/user-assigned-identity:0.5.0' = { name: take('avm.res.managed-identity.user-assigned-identity.${solutionSuffix}', 64) @@ -2012,5 +1984,8 @@ output AZURE_CONTAINER_REGISTRY_ENDPOINT string = avmContainerRegistry.outputs.l @description('The name of the Content Understanding AI Services account.') output CONTENT_UNDERSTANDING_ACCOUNT_NAME string = avmAiServices_cu.outputs.name +@description('Whether private networking (WAF) is enabled.') +output ENABLE_PRIVATE_NETWORKING bool = enablePrivateNetworking + @description('The resource group the resources were deployed into.') output AZURE_RESOURCE_GROUP string = resourceGroup().name diff --git a/infra/scripts/post_deployment.ps1 b/infra/scripts/post_deployment.ps1 index 04104a50..87736b36 100644 --- a/infra/scripts/post_deployment.ps1 +++ b/infra/scripts/post_deployment.ps1 @@ -50,11 +50,26 @@ Write-Host " [Link] Portal URL: $WORKFLOW_APP_PORTAL_URL" Write-Host "" Write-Host "[Package] Registering schemas and creating schema set..." + +# Check if private networking (WAF) is enabled +$ENABLE_PRIVATE_NETWORKING = $null +try { + $ENABLE_PRIVATE_NETWORKING = azd env get-value ENABLE_PRIVATE_NETWORKING 2>$null +} catch { } + +# When private networking is enabled, the API is internal-only (ingressExternal=false). +# Use the web app's /api proxy to reach the backend through same-origin routing. +if ($ENABLE_PRIVATE_NETWORKING -eq "true") { + Write-Host " [Info] Private networking (WAF) is enabled. Using web app /api proxy to reach backend." + $ApiBaseUrl = "https://$CONTAINER_WEB_APP_FQDN/api" +} else { + $ApiBaseUrl = "https://$CONTAINER_API_APP_FQDN" +} + Write-Host " [Wait] Waiting for API to be ready..." $MaxRetries = 10 $RetryInterval = 15 -$ApiBaseUrl = "https://$CONTAINER_API_APP_FQDN" $ApiReady = $false for ($i = 1; $i -le $MaxRetries; $i++) { diff --git a/infra/scripts/post_deployment.sh b/infra/scripts/post_deployment.sh index 2b0ee0ad..c86f1ded 100644 --- a/infra/scripts/post_deployment.sh +++ b/infra/scripts/post_deployment.sh @@ -60,11 +60,23 @@ echo " 🔗 Portal URL: $WORKFLOW_APP_PORTAL_URL" echo "" echo "đŸ“Ļ Registering schemas and creating schema set..." + +# Check if private networking (WAF) is enabled +ENABLE_PRIVATE_NETWORKING=$(azd env get-value ENABLE_PRIVATE_NETWORKING 2>/dev/null || echo "") + +# When private networking is enabled, the API is internal-only (ingressExternal=false). +# Use the web app's /api proxy to reach the backend through same-origin routing. +if [ "$ENABLE_PRIVATE_NETWORKING" = "true" ]; then + echo " â„šī¸ Private networking (WAF) is enabled. Using web app /api proxy to reach backend." + API_BASE_URL="https://$CONTAINER_WEB_APP_FQDN/api" +else + API_BASE_URL="https://$CONTAINER_API_APP_FQDN" +fi + echo " âŗ Waiting for API to be ready..." MAX_RETRIES=10 RETRY_INTERVAL=15 -API_BASE_URL="https://$CONTAINER_API_APP_FQDN" for i in $(seq 1 $MAX_RETRIES); do STATUS=$(curl -s -o /dev/null -w "%{http_code}" "$API_BASE_URL/schemavault/" 2>/dev/null || echo "000") diff --git a/src/ContentProcessorWeb/env.sh b/src/ContentProcessorWeb/env.sh index 31b2a989..face6f02 100644 --- a/src/ContentProcessorWeb/env.sh +++ b/src/ContentProcessorWeb/env.sh @@ -1,4 +1,10 @@ #!/bin/sh + +# Ensure APP_BACKEND_API_URL has a safe default so nginx can always start. +# When not set, the /api/ proxy_pass will point to a non-routable placeholder +# and return 502, which is acceptable — the direct API path still works. +export APP_BACKEND_API_URL="${APP_BACKEND_API_URL:-http://localhost:8080}" + for i in $(env | grep ^APP_) do key=$(echo $i | cut -d '=' -f 1) diff --git a/src/ContentProcessorWeb/nginx-custom.conf b/src/ContentProcessorWeb/nginx-custom.conf index 1980c18e..c685675f 100644 --- a/src/ContentProcessorWeb/nginx-custom.conf +++ b/src/ContentProcessorWeb/nginx-custom.conf @@ -14,19 +14,28 @@ http { types_hash_max_size 2048; types_hash_bucket_size 128; + # Allow large file uploads through the /api proxy + client_max_body_size 100m; + server { listen 3000; server_name localhost; # Route browser API calls through the web container so private backend # endpoints remain internal-only in WAF/private networking deployments. + # APP_BACKEND_API_URL is substituted at runtime by env.sh; if unset, this + # block is effectively a no-op (returns 502) which is safe for nginx startup. location /api/ { proxy_http_version 1.1; - proxy_set_header Host $host; + proxy_set_header Host $proxy_host; + proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection "upgrade"; + proxy_connect_timeout 60s; + proxy_send_timeout 120s; + proxy_read_timeout 120s; proxy_pass APP_BACKEND_API_URL/; } From 7a4ed0d63d5a523066bce8c608166503fb451202 Mon Sep 17 00:00:00 2001 From: "Ashwal Vishwanath (Persistent Systems Inc)" Date: Thu, 7 May 2026 19:24:18 +0530 Subject: [PATCH 4/4] Revert "fix: resolve nginx 405 on POST and CAE internal mode issues" This reverts commit 8c0985c82daffd65d3cf929f17d294df39c119d1. --- infra/main.bicep | 35 +++++++++++++++++++---- infra/main_custom.bicep | 35 +++++++++++++++++++---- infra/scripts/post_deployment.ps1 | 17 +---------- infra/scripts/post_deployment.sh | 14 +-------- src/ContentProcessorWeb/env.sh | 6 ---- src/ContentProcessorWeb/nginx-custom.conf | 11 +------ 6 files changed, 63 insertions(+), 55 deletions(-) diff --git a/infra/main.bicep b/infra/main.bicep index 917088ef..8edaf0f4 100644 --- a/infra/main.bicep +++ b/infra/main.bicep @@ -931,8 +931,8 @@ module avmContainerAppEnv 'br/public:avm/res/app/managed-environment:0.13.2' = { } ] enableTelemetry: enableTelemetry - publicNetworkAccess: 'Enabled' - internal: false + publicNetworkAccess: enablePrivateNetworking ? 'Disabled' : 'Enabled' + internal: enablePrivateNetworking ? true : false // <========== WAF related parameters @@ -945,6 +945,34 @@ module avmContainerAppEnv 'br/public:avm/res/app/managed-environment:0.13.2' = { } } +// ========== Private DNS Zone for internal Container App Environment ========== // +// When the CAE is internal, its FQDN is resolvable only within the VNet via this zone. +module caeDnsZone 'br/public:avm/res/network/private-dns-zone:0.8.0' = if (enablePrivateNetworking) { + name: take('avm.res.network.private-dns-zone.cae.${solutionSuffix}', 64) + params: { + name: avmContainerAppEnv.outputs.defaultDomain + tags: tags + enableTelemetry: enableTelemetry + a: [ + { + name: '*' + aRecords: [ + { + ipv4Address: avmContainerAppEnv.outputs.staticIp + } + ] + ttl: 300 + } + ] + virtualNetworkLinks: [ + { + name: take('vnetlink-vnet-${solutionSuffix}-cae', 64) + virtualNetworkResourceId: virtualNetwork!.outputs.resourceId + } + ] + } +} + // //=========== Managed Identity for Container Registry ========== // module avmContainerRegistryReader 'br/public:avm/res/managed-identity/user-assigned-identity:0.5.0' = { name: take('avm.res.managed-identity.user-assigned-identity.${solutionSuffix}', 64) @@ -1943,8 +1971,5 @@ output CONTAINER_REGISTRY_LOGIN_SERVER string = avmContainerRegistry.outputs.log @description('The name of the Content Understanding AI Services account.') output CONTENT_UNDERSTANDING_ACCOUNT_NAME string = avmAiServices_cu.outputs.name -@description('Whether private networking (WAF) is enabled.') -output ENABLE_PRIVATE_NETWORKING bool = enablePrivateNetworking - @description('The resource group the resources were deployed into.') output AZURE_RESOURCE_GROUP string = resourceGroup().name diff --git a/infra/main_custom.bicep b/infra/main_custom.bicep index f5befa51..343e860c 100644 --- a/infra/main_custom.bicep +++ b/infra/main_custom.bicep @@ -934,8 +934,8 @@ module avmContainerAppEnv 'br/public:avm/res/app/managed-environment:0.13.2' = { } ] enableTelemetry: enableTelemetry - publicNetworkAccess: 'Enabled' - internal: false + publicNetworkAccess: enablePrivateNetworking ? 'Disabled' : 'Enabled' + internal: enablePrivateNetworking ? true : false // <========== WAF related parameters @@ -948,6 +948,34 @@ module avmContainerAppEnv 'br/public:avm/res/app/managed-environment:0.13.2' = { } } +// ========== Private DNS Zone for internal Container App Environment ========== // +// When the CAE is internal, its FQDN is resolvable only within the VNet via this zone. +module caeDnsZone 'br/public:avm/res/network/private-dns-zone:0.8.0' = if (enablePrivateNetworking) { + name: take('avm.res.network.private-dns-zone.cae.${solutionSuffix}', 64) + params: { + name: avmContainerAppEnv.outputs.defaultDomain + tags: tags + enableTelemetry: enableTelemetry + a: [ + { + name: '*' + aRecords: [ + { + ipv4Address: avmContainerAppEnv.outputs.staticIp + } + ] + ttl: 300 + } + ] + virtualNetworkLinks: [ + { + name: take('vnetlink-vnet-${solutionSuffix}-cae', 64) + virtualNetworkResourceId: virtualNetwork!.outputs.resourceId + } + ] + } +} + // //=========== Managed Identity for Container Registry ========== // module avmContainerRegistryReader 'br/public:avm/res/managed-identity/user-assigned-identity:0.5.0' = { name: take('avm.res.managed-identity.user-assigned-identity.${solutionSuffix}', 64) @@ -1984,8 +2012,5 @@ output AZURE_CONTAINER_REGISTRY_ENDPOINT string = avmContainerRegistry.outputs.l @description('The name of the Content Understanding AI Services account.') output CONTENT_UNDERSTANDING_ACCOUNT_NAME string = avmAiServices_cu.outputs.name -@description('Whether private networking (WAF) is enabled.') -output ENABLE_PRIVATE_NETWORKING bool = enablePrivateNetworking - @description('The resource group the resources were deployed into.') output AZURE_RESOURCE_GROUP string = resourceGroup().name diff --git a/infra/scripts/post_deployment.ps1 b/infra/scripts/post_deployment.ps1 index 87736b36..04104a50 100644 --- a/infra/scripts/post_deployment.ps1 +++ b/infra/scripts/post_deployment.ps1 @@ -50,26 +50,11 @@ Write-Host " [Link] Portal URL: $WORKFLOW_APP_PORTAL_URL" Write-Host "" Write-Host "[Package] Registering schemas and creating schema set..." - -# Check if private networking (WAF) is enabled -$ENABLE_PRIVATE_NETWORKING = $null -try { - $ENABLE_PRIVATE_NETWORKING = azd env get-value ENABLE_PRIVATE_NETWORKING 2>$null -} catch { } - -# When private networking is enabled, the API is internal-only (ingressExternal=false). -# Use the web app's /api proxy to reach the backend through same-origin routing. -if ($ENABLE_PRIVATE_NETWORKING -eq "true") { - Write-Host " [Info] Private networking (WAF) is enabled. Using web app /api proxy to reach backend." - $ApiBaseUrl = "https://$CONTAINER_WEB_APP_FQDN/api" -} else { - $ApiBaseUrl = "https://$CONTAINER_API_APP_FQDN" -} - Write-Host " [Wait] Waiting for API to be ready..." $MaxRetries = 10 $RetryInterval = 15 +$ApiBaseUrl = "https://$CONTAINER_API_APP_FQDN" $ApiReady = $false for ($i = 1; $i -le $MaxRetries; $i++) { diff --git a/infra/scripts/post_deployment.sh b/infra/scripts/post_deployment.sh index c86f1ded..2b0ee0ad 100644 --- a/infra/scripts/post_deployment.sh +++ b/infra/scripts/post_deployment.sh @@ -60,23 +60,11 @@ echo " 🔗 Portal URL: $WORKFLOW_APP_PORTAL_URL" echo "" echo "đŸ“Ļ Registering schemas and creating schema set..." - -# Check if private networking (WAF) is enabled -ENABLE_PRIVATE_NETWORKING=$(azd env get-value ENABLE_PRIVATE_NETWORKING 2>/dev/null || echo "") - -# When private networking is enabled, the API is internal-only (ingressExternal=false). -# Use the web app's /api proxy to reach the backend through same-origin routing. -if [ "$ENABLE_PRIVATE_NETWORKING" = "true" ]; then - echo " â„šī¸ Private networking (WAF) is enabled. Using web app /api proxy to reach backend." - API_BASE_URL="https://$CONTAINER_WEB_APP_FQDN/api" -else - API_BASE_URL="https://$CONTAINER_API_APP_FQDN" -fi - echo " âŗ Waiting for API to be ready..." MAX_RETRIES=10 RETRY_INTERVAL=15 +API_BASE_URL="https://$CONTAINER_API_APP_FQDN" for i in $(seq 1 $MAX_RETRIES); do STATUS=$(curl -s -o /dev/null -w "%{http_code}" "$API_BASE_URL/schemavault/" 2>/dev/null || echo "000") diff --git a/src/ContentProcessorWeb/env.sh b/src/ContentProcessorWeb/env.sh index face6f02..31b2a989 100644 --- a/src/ContentProcessorWeb/env.sh +++ b/src/ContentProcessorWeb/env.sh @@ -1,10 +1,4 @@ #!/bin/sh - -# Ensure APP_BACKEND_API_URL has a safe default so nginx can always start. -# When not set, the /api/ proxy_pass will point to a non-routable placeholder -# and return 502, which is acceptable — the direct API path still works. -export APP_BACKEND_API_URL="${APP_BACKEND_API_URL:-http://localhost:8080}" - for i in $(env | grep ^APP_) do key=$(echo $i | cut -d '=' -f 1) diff --git a/src/ContentProcessorWeb/nginx-custom.conf b/src/ContentProcessorWeb/nginx-custom.conf index c685675f..1980c18e 100644 --- a/src/ContentProcessorWeb/nginx-custom.conf +++ b/src/ContentProcessorWeb/nginx-custom.conf @@ -14,28 +14,19 @@ http { types_hash_max_size 2048; types_hash_bucket_size 128; - # Allow large file uploads through the /api proxy - client_max_body_size 100m; - server { listen 3000; server_name localhost; # Route browser API calls through the web container so private backend # endpoints remain internal-only in WAF/private networking deployments. - # APP_BACKEND_API_URL is substituted at runtime by env.sh; if unset, this - # block is effectively a no-op (returns 502) which is safe for nginx startup. location /api/ { proxy_http_version 1.1; - proxy_set_header Host $proxy_host; - proxy_set_header X-Real-IP $remote_addr; + proxy_set_header Host $host; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection "upgrade"; - proxy_connect_timeout 60s; - proxy_send_timeout 120s; - proxy_read_timeout 120s; proxy_pass APP_BACKEND_API_URL/; }