Skip to content

Commit 6bb94a0

Browse files
Harden WAF deployment: private backend ingress and frontend proxy routing
1 parent 44e0714 commit 6bb94a0

6 files changed

Lines changed: 94 additions & 6 deletions

File tree

docs/DeploymentGuide.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -194,6 +194,13 @@ Review the configuration options below. You can customize any settings that meet
194194
| **Framework** | Basic configuration | [Well-Architected Framework](https://learn.microsoft.com/en-us/azure/well-architected/) |
195195
| **Features** | Core functionality | Reliability, security, operational excellence |
196196

197+
When using the Production/WAF deployment (`enablePrivateNetworking=true`), networking is configured as follows:
198+
199+
- Backend Container App endpoints are internal-only (`ingressExternal=false`) and not publicly reachable.
200+
- Container Apps Environment is deployed in internal mode with VNet integration.
201+
- The web frontend remains public and routes browser API traffic through same-origin `/api` proxying to the private backend over VNet.
202+
- Private DNS is configured for the internal Container Apps Environment domain.
203+
197204
**To use production configuration:**
198205

199206
Copy the contents from the production configuration file to your main parameters file:

docs/TechnicalArchitecture.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -177,6 +177,8 @@ The API also provides schema management, schema set (collection) management, and
177177
### Claim Process Monitor Web
178178
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.
179179

180+
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.
181+
180182
### App Configuration
181183
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.
182184

infra/main.bicep

Lines changed: 36 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -931,7 +931,8 @@ module avmContainerAppEnv 'br/public:avm/res/app/managed-environment:0.13.2' = {
931931
}
932932
]
933933
enableTelemetry: enableTelemetry
934-
publicNetworkAccess: 'Enabled' // Always enabled for Container Apps Environment
934+
publicNetworkAccess: enablePrivateNetworking ? 'Disabled' : 'Enabled'
935+
internal: enablePrivateNetworking ? true : false
935936

936937
// <========== WAF related parameters
937938

@@ -944,6 +945,34 @@ module avmContainerAppEnv 'br/public:avm/res/app/managed-environment:0.13.2' = {
944945
}
945946
}
946947

948+
// ========== Private DNS Zone for internal Container App Environment ========== //
949+
// When the CAE is internal, its FQDN is resolvable only within the VNet via this zone.
950+
module caeDnsZone 'br/public:avm/res/network/private-dns-zone:0.8.0' = if (enablePrivateNetworking) {
951+
name: take('avm.res.network.private-dns-zone.cae.${solutionSuffix}', 64)
952+
params: {
953+
name: avmContainerAppEnv.outputs.defaultDomain
954+
tags: tags
955+
enableTelemetry: enableTelemetry
956+
a: [
957+
{
958+
name: '*'
959+
aRecords: [
960+
{
961+
ipv4Address: avmContainerAppEnv.outputs.staticIp
962+
}
963+
]
964+
ttl: 300
965+
}
966+
]
967+
virtualNetworkLinks: [
968+
{
969+
name: take('vnetlink-${virtualNetworkResourceName}-cae', 64)
970+
virtualNetworkResourceId: virtualNetwork!.outputs.resourceId
971+
}
972+
]
973+
}
974+
}
975+
947976
// //=========== Managed Identity for Container Registry ========== //
948977
module avmContainerRegistryReader 'br/public:avm/res/managed-identity/user-assigned-identity:0.5.0' = {
949978
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' = {
11321161
}
11331162
]
11341163
}
1135-
ingressExternal: true
1164+
ingressExternal: enablePrivateNetworking ? false : true
11361165
activeRevisionsMode: 'Single'
11371166
ingressTransport: 'auto'
11381167
corsPolicy: {
@@ -1201,6 +1230,10 @@ module avmContainerApp_Web 'br/public:avm/res/app/container-app:0.22.1' = {
12011230
env: [
12021231
{
12031232
name: 'APP_API_BASE_URL'
1233+
value: enablePrivateNetworking ? '/api' : 'https://${avmContainerApp_API.outputs.fqdn}'
1234+
}
1235+
{
1236+
name: 'APP_BACKEND_API_URL'
12041237
value: 'https://${avmContainerApp_API.outputs.fqdn}'
12051238
}
12061239
{
@@ -1808,7 +1841,7 @@ module avmContainerApp_API_update 'br/public:avm/res/app/container-app:0.22.1' =
18081841
}
18091842
]
18101843
}
1811-
ingressExternal: true
1844+
ingressExternal: enablePrivateNetworking ? false : true
18121845
activeRevisionsMode: 'Single'
18131846
ingressTransport: 'auto'
18141847
corsPolicy: {

infra/main_custom.bicep

Lines changed: 36 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -934,7 +934,8 @@ module avmContainerAppEnv 'br/public:avm/res/app/managed-environment:0.13.2' = {
934934
}
935935
]
936936
enableTelemetry: enableTelemetry
937-
publicNetworkAccess: 'Enabled' // Always enabled for Container Apps Environment
937+
publicNetworkAccess: enablePrivateNetworking ? 'Disabled' : 'Enabled'
938+
internal: enablePrivateNetworking ? true : false
938939

939940
// <========== WAF related parameters
940941

@@ -947,6 +948,34 @@ module avmContainerAppEnv 'br/public:avm/res/app/managed-environment:0.13.2' = {
947948
}
948949
}
949950

951+
// ========== Private DNS Zone for internal Container App Environment ========== //
952+
// When the CAE is internal, its FQDN is resolvable only within the VNet via this zone.
953+
module caeDnsZone 'br/public:avm/res/network/private-dns-zone:0.8.0' = if (enablePrivateNetworking) {
954+
name: take('avm.res.network.private-dns-zone.cae.${solutionSuffix}', 64)
955+
params: {
956+
name: avmContainerAppEnv.outputs.defaultDomain
957+
tags: tags
958+
enableTelemetry: enableTelemetry
959+
a: [
960+
{
961+
name: '*'
962+
aRecords: [
963+
{
964+
ipv4Address: avmContainerAppEnv.outputs.staticIp
965+
}
966+
]
967+
ttl: 300
968+
}
969+
]
970+
virtualNetworkLinks: [
971+
{
972+
name: take('vnetlink-${virtualNetworkResourceName}-cae', 64)
973+
virtualNetworkResourceId: virtualNetwork!.outputs.resourceId
974+
}
975+
]
976+
}
977+
}
978+
950979
// //=========== Managed Identity for Container Registry ========== //
951980
module avmContainerRegistryReader 'br/public:avm/res/managed-identity/user-assigned-identity:0.5.0' = {
952981
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' = {
11451174
}
11461175
]
11471176
}
1148-
ingressExternal: true
1177+
ingressExternal: enablePrivateNetworking ? false : true
11491178
activeRevisionsMode: 'Single'
11501179
ingressTransport: 'auto'
11511180
corsPolicy: {
@@ -1219,6 +1248,10 @@ module avmContainerApp_Web 'br/public:avm/res/app/container-app:0.22.1' = {
12191248
env: [
12201249
{
12211250
name: 'APP_API_BASE_URL'
1251+
value: enablePrivateNetworking ? '/api' : 'https://${avmContainerApp_API.outputs.fqdn}'
1252+
}
1253+
{
1254+
name: 'APP_BACKEND_API_URL'
12221255
value: 'https://${avmContainerApp_API.outputs.fqdn}'
12231256
}
12241257
{
@@ -1841,7 +1874,7 @@ module avmContainerApp_API_update 'br/public:avm/res/app/container-app:0.22.1' =
18411874
}
18421875
]
18431876
}
1844-
ingressExternal: true
1877+
ingressExternal: enablePrivateNetworking ? false : true
18451878
activeRevisionsMode: 'Single'
18461879
ingressTransport: 'auto'
18471880
corsPolicy: {

src/ContentProcessorWeb/env.sh

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,5 +6,6 @@ do
66
echo $key=$value
77
# Use sed to replace only the exact matches of the key
88
find /usr/share/nginx/html -type f -exec sed -i "s|\b${key}\b|${value}|g" '{}' +
9+
sed -i "s|\b${key}\b|${value}|g" /etc/nginx/nginx.conf
910
done
1011
echo 'done'

src/ContentProcessorWeb/nginx-custom.conf

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,18 @@ http {
1818
listen 3000;
1919
server_name localhost;
2020

21+
# Route browser API calls through the web container so private backend
22+
# endpoints remain internal-only in WAF/private networking deployments.
23+
location /api/ {
24+
proxy_http_version 1.1;
25+
proxy_set_header Host $host;
26+
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
27+
proxy_set_header X-Forwarded-Proto $scheme;
28+
proxy_set_header Upgrade $http_upgrade;
29+
proxy_set_header Connection "upgrade";
30+
proxy_pass APP_BACKEND_API_URL/;
31+
}
32+
2133
location / {
2234
root /usr/share/nginx/html;
2335
try_files $uri $uri/ /index.html;

0 commit comments

Comments
 (0)