Skip to content

Commit ae8ed02

Browse files
Merge pull request #861 from microsoft/feature/39249-waf-api-private-access
feat: Restrict backend API to private access in WAF deployment
2 parents a52cb11 + d0f7dd6 commit ae8ed02

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

@@ -1388,7 +1405,8 @@ module webSiteFrontend 'modules/web-sites.bicep' = {
13881405
{
13891406
name: 'appsettings'
13901407
properties: {
1391-
APP_API_BASE_URL: 'https://api-${solutionSuffix}.azurewebsites.net'
1408+
APP_API_BASE_URL: enablePrivateNetworking ? '' : 'https://api-${solutionSuffix}.azurewebsites.net'
1409+
BACKEND_API_HOST: enablePrivateNetworking ? 'api-${solutionSuffix}.azurewebsites.net' : ''
13921410
}
13931411
applicationInsightResourceId: enableMonitoring ? applicationInsights!.outputs.resourceId : null
13941412
}

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
// ===================================================
@@ -1363,7 +1365,22 @@ module webSiteBackend 'modules/web-sites.bicep' = {
13631365
vnetRouteAllEnabled: enablePrivateNetworking ? true : false
13641366
vnetImagePullEnabled: enablePrivateNetworking ? true : false
13651367
virtualNetworkSubnetId: enablePrivateNetworking ? virtualNetwork!.outputs.webSubnetResourceId : null
1366-
publicNetworkAccess: 'Enabled'
1368+
publicNetworkAccess: enablePrivateNetworking ? 'Disabled' : 'Enabled'
1369+
privateEndpoints: enablePrivateNetworking
1370+
? [
1371+
{
1372+
name: 'pep-${backendWebSiteResourceName}'
1373+
customNetworkInterfaceName: 'nic-${backendWebSiteResourceName}'
1374+
privateDnsZoneGroup: {
1375+
privateDnsZoneGroupConfigs: [
1376+
{ privateDnsZoneResourceId: avmPrivateDnsZones[dnsZoneIndex.webApp]!.outputs.resourceId }
1377+
]
1378+
}
1379+
service: 'sites'
1380+
subnetResourceId: virtualNetwork!.outputs.pepsSubnetResourceId
1381+
}
1382+
]
1383+
: []
13671384
}
13681385
}
13691386

@@ -1391,9 +1408,10 @@ module webSiteFrontend 'modules/web-sites.bicep' = {
13911408
properties: {
13921409
SCM_DO_BUILD_DURING_DEPLOYMENT: 'true'
13931410
ENABLE_ORYX_BUILD: 'true'
1394-
REACT_APP_API_BASE_URL: 'https://api-${solutionSuffix}.azurewebsites.net'
1411+
REACT_APP_API_BASE_URL: enablePrivateNetworking ? '' : 'https://api-${solutionSuffix}.azurewebsites.net'
13951412
WEBSITE_NODE_DEFAULT_VERSION: '~20'
1396-
APP_API_BASE_URL: 'https://api-${solutionSuffix}.azurewebsites.net'
1413+
APP_API_BASE_URL: enablePrivateNetworking ? '' : 'https://api-${solutionSuffix}.azurewebsites.net'
1414+
BACKEND_API_HOST: enablePrivateNetworking ? 'api-${solutionSuffix}.azurewebsites.net' : ''
13971415
}
13981416
applicationInsightResourceId: enableMonitoring ? applicationInsights!.outputs.resourceId : null
13991417
}

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)