Skip to content

Commit d0f7dd6

Browse files
feat: restrict backend API to private access in WAF deployment
When enablePrivateNetworking (WAF mode) is active: - Add privatelink.azurewebsites.net private DNS zone linked to VNet - Create private endpoint for backend API App Service in peps subnet - Set publicNetworkAccess to Disabled for backend API - Frontend nginx reverse-proxies /api/ and /history/ requests to backend over VNet - APP_API_BASE_URL set to empty so frontend calls same origin in WAF mode - Non-WAF deployments remain unchanged Resolves AB#39249 Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent 2eb2bf6 commit d0f7dd6

5 files changed

Lines changed: 120 additions & 6 deletions

File tree

infra/main.bicep

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -445,6 +445,7 @@ var privateDnsZones = [
445445
'privatelink.documents.azure.com'
446446
'privatelink${environment().suffixes.sqlServerHostname}'
447447
'privatelink.search.windows.net'
448+
'privatelink.azurewebsites.net'
448449
]
449450

450451
// DNS Zone Index Constants
@@ -459,6 +460,7 @@ var dnsZoneIndex = {
459460
cosmosDB: 7
460461
sqlServer: 8
461462
search: 9
463+
webApp: 10
462464
}
463465

464466
// ===================================================
@@ -1360,7 +1362,22 @@ module webSiteBackend 'modules/web-sites.bicep' = {
13601362
vnetRouteAllEnabled: enablePrivateNetworking ? true : false
13611363
vnetImagePullEnabled: enablePrivateNetworking ? true : false
13621364
virtualNetworkSubnetId: enablePrivateNetworking ? virtualNetwork!.outputs.webSubnetResourceId : null
1363-
publicNetworkAccess: 'Enabled'
1365+
publicNetworkAccess: enablePrivateNetworking ? 'Disabled' : 'Enabled'
1366+
privateEndpoints: enablePrivateNetworking
1367+
? [
1368+
{
1369+
name: 'pep-${backendWebSiteResourceName}'
1370+
customNetworkInterfaceName: 'nic-${backendWebSiteResourceName}'
1371+
privateDnsZoneGroup: {
1372+
privateDnsZoneGroupConfigs: [
1373+
{ privateDnsZoneResourceId: avmPrivateDnsZones[dnsZoneIndex.webApp]!.outputs.resourceId }
1374+
]
1375+
}
1376+
service: 'sites'
1377+
subnetResourceId: virtualNetwork!.outputs.pepsSubnetResourceId
1378+
}
1379+
]
1380+
: []
13641381
}
13651382
}
13661383

@@ -1387,7 +1404,8 @@ module webSiteFrontend 'modules/web-sites.bicep' = {
13871404
{
13881405
name: 'appsettings'
13891406
properties: {
1390-
APP_API_BASE_URL: 'https://api-${solutionSuffix}.azurewebsites.net'
1407+
APP_API_BASE_URL: enablePrivateNetworking ? '' : 'https://api-${solutionSuffix}.azurewebsites.net'
1408+
BACKEND_API_HOST: enablePrivateNetworking ? 'api-${solutionSuffix}.azurewebsites.net' : ''
13911409
}
13921410
applicationInsightResourceId: enableMonitoring ? applicationInsights!.outputs.resourceId : null
13931411
}

infra/main_custom.bicep

Lines changed: 21 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -446,6 +446,7 @@ var privateDnsZones = [
446446
'privatelink.documents.azure.com'
447447
'privatelink${environment().suffixes.sqlServerHostname}'
448448
'privatelink.search.windows.net'
449+
'privatelink.azurewebsites.net'
449450
]
450451

451452
// DNS Zone Index Constants
@@ -460,6 +461,7 @@ var dnsZoneIndex = {
460461
cosmosDB: 7
461462
sqlServer: 8
462463
search: 9
464+
webApp: 10
463465
}
464466

465467
// ===================================================
@@ -1359,7 +1361,22 @@ module webSiteBackend 'modules/web-sites.bicep' = {
13591361
vnetRouteAllEnabled: enablePrivateNetworking ? true : false
13601362
vnetImagePullEnabled: enablePrivateNetworking ? true : false
13611363
virtualNetworkSubnetId: enablePrivateNetworking ? virtualNetwork!.outputs.webSubnetResourceId : null
1362-
publicNetworkAccess: 'Enabled'
1364+
publicNetworkAccess: enablePrivateNetworking ? 'Disabled' : 'Enabled'
1365+
privateEndpoints: enablePrivateNetworking
1366+
? [
1367+
{
1368+
name: 'pep-${backendWebSiteResourceName}'
1369+
customNetworkInterfaceName: 'nic-${backendWebSiteResourceName}'
1370+
privateDnsZoneGroup: {
1371+
privateDnsZoneGroupConfigs: [
1372+
{ privateDnsZoneResourceId: avmPrivateDnsZones[dnsZoneIndex.webApp]!.outputs.resourceId }
1373+
]
1374+
}
1375+
service: 'sites'
1376+
subnetResourceId: virtualNetwork!.outputs.pepsSubnetResourceId
1377+
}
1378+
]
1379+
: []
13631380
}
13641381
}
13651382

@@ -1387,9 +1404,10 @@ module webSiteFrontend 'modules/web-sites.bicep' = {
13871404
properties: {
13881405
SCM_DO_BUILD_DURING_DEPLOYMENT: 'true'
13891406
ENABLE_ORYX_BUILD: 'true'
1390-
REACT_APP_API_BASE_URL: 'https://api-${solutionSuffix}.azurewebsites.net'
1407+
REACT_APP_API_BASE_URL: enablePrivateNetworking ? '' : 'https://api-${solutionSuffix}.azurewebsites.net'
13911408
WEBSITE_NODE_DEFAULT_VERSION: '~20'
1392-
APP_API_BASE_URL: 'https://api-${solutionSuffix}.azurewebsites.net'
1409+
APP_API_BASE_URL: enablePrivateNetworking ? '' : 'https://api-${solutionSuffix}.azurewebsites.net'
1410+
BACKEND_API_HOST: enablePrivateNetworking ? 'api-${solutionSuffix}.azurewebsites.net' : ''
13931411
}
13941412
applicationInsightResourceId: enableMonitoring ? applicationInsights!.outputs.resourceId : null
13951413
}

src/App/WebApp.Dockerfile

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,12 @@ COPY env.sh /docker-entrypoint.d/env.sh
1717
RUN chmod +x /docker-entrypoint.d/env.sh
1818
RUN sed -i 's/\r$//' /docker-entrypoint.d/env.sh
1919

20+
# Custom nginx config with API proxy support
21+
COPY nginx.conf /etc/nginx/nginx.conf
22+
23+
# Create empty api-proxy conf (overwritten at startup for WAF deployments)
24+
RUN mkdir -p /etc/nginx/conf.d && touch /etc/nginx/conf.d/api-proxy.conf
25+
2026
# Expose the application port
2127
EXPOSE 3000
2228

src/App/env.sh

Lines changed: 39 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,48 @@
11
#!/bin/sh
2+
3+
# If BACKEND_API_HOST is set (WAF mode), configure nginx reverse proxy
4+
if [ -n "$BACKEND_API_HOST" ]; then
5+
# Write nginx proxy config for /api/ and /history/ routes
6+
cat > /etc/nginx/conf.d/api-proxy.conf << PROXYEOF
7+
location /api/ {
8+
resolver 168.63.129.16 valid=30s;
9+
set \$backend "https://$BACKEND_API_HOST";
10+
proxy_pass \$backend;
11+
proxy_set_header Host $BACKEND_API_HOST;
12+
proxy_set_header X-Real-IP \$remote_addr;
13+
proxy_set_header X-Forwarded-For \$proxy_add_x_forwarded_for;
14+
proxy_set_header X-Forwarded-Proto \$scheme;
15+
proxy_ssl_server_name on;
16+
proxy_read_timeout 300s;
17+
proxy_connect_timeout 60s;
18+
proxy_buffering off;
19+
}
20+
21+
location /history/ {
22+
resolver 168.63.129.16 valid=30s;
23+
set \$backend "https://$BACKEND_API_HOST";
24+
proxy_pass \$backend;
25+
proxy_set_header Host $BACKEND_API_HOST;
26+
proxy_set_header X-Real-IP \$remote_addr;
27+
proxy_set_header X-Forwarded-For \$proxy_add_x_forwarded_for;
28+
proxy_set_header X-Forwarded-Proto \$scheme;
29+
proxy_ssl_server_name on;
30+
proxy_read_timeout 300s;
31+
proxy_connect_timeout 60s;
32+
proxy_buffering off;
33+
}
34+
PROXYEOF
35+
else
36+
# Ensure empty file exists for non-WAF deployments
37+
> /etc/nginx/conf.d/api-proxy.conf
38+
fi
39+
40+
# Replace APP_* env vars in built frontend files (existing behavior)
241
for i in $(env | grep ^APP_)
342
do
443
key=$(echo $i | cut -d '=' -f 1)
544
value=$(echo $i | cut -d '=' -f 2-)
645
echo $key=$value
7-
# Use sed to replace only the exact matches of the key
846
find /usr/share/nginx/html -type f -exec sed -i "s|\b${key}\b|${value}|g" '{}' +
947
done
1048
echo 'done'

src/App/nginx.conf

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
events {
2+
worker_connections 1024;
3+
}
4+
5+
http {
6+
include /etc/nginx/mime.types;
7+
default_type application/octet-stream;
8+
9+
server {
10+
listen 80;
11+
server_name localhost;
12+
root /usr/share/nginx/html;
13+
index index.html;
14+
15+
# Include API proxy config (generated at startup for WAF/private networking deployments)
16+
include /etc/nginx/conf.d/api-proxy.conf;
17+
18+
# Handle React Router
19+
location / {
20+
try_files $uri $uri/ /index.html;
21+
}
22+
23+
# Cache static assets
24+
location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg)$ {
25+
expires 1y;
26+
add_header Cache-Control "public, immutable";
27+
}
28+
29+
# Security headers
30+
add_header X-Frame-Options "SAMEORIGIN" always;
31+
add_header X-Content-Type-Options "nosniff" always;
32+
add_header X-XSS-Protection "1; mode=block" always;
33+
}
34+
}

0 commit comments

Comments
 (0)