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..8edaf0f4 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-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) @@ -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.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 7696c022..343e860c 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-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) @@ -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;