Skip to content

Commit a35f6da

Browse files
feat: restrict backend Container App to private access in WAF deployment
When enablePrivateNetworking (WAF mode) is active: - Set Container App Environment to internal with public access disabled - Set Container App ingress to internal (not externally accessible) - Frontend Python server proxies /api/* requests to backend over VNet - /config endpoint returns same-origin /api URL in WAF mode - Non-WAF deployments remain unchanged (direct public API access) Resolves AB#39249 Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent 6a3dda5 commit a35f6da

6 files changed

Lines changed: 63 additions & 13 deletions

File tree

docs/images/readme/1.png

122 KB
Loading

docs/images/readme/2.png

158 KB
Loading

infra/main.bicep

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1142,8 +1142,8 @@ module containerAppEnvironment 'br/public:avm/res/app/managed-environment:0.11.2
11421142
tags: tags
11431143
enableTelemetry: enableTelemetry
11441144
// WAF aligned configuration for Private Networking
1145-
publicNetworkAccess: 'Enabled' // Always enabling the publicNetworkAccess for Container App Environment
1146-
internal: false // Must be false when publicNetworkAccess is'Enabled'
1145+
publicNetworkAccess: enablePrivateNetworking ? 'Disabled' : 'Enabled'
1146+
internal: enablePrivateNetworking ? true : false
11471147
infrastructureSubnetResourceId: enablePrivateNetworking ? virtualNetwork.?outputs.?containerSubnetResourceId : null
11481148
// WAF aligned configuration for Monitoring
11491149
appLogsConfiguration: enableMonitoring
@@ -1191,7 +1191,7 @@ module containerApp 'br/public:avm/res/app/container-app:0.18.1' = {
11911191
environmentResourceId: containerAppEnvironment.outputs.resourceId
11921192
managedIdentities: { userAssignedResourceIds: [userAssignedIdentity.outputs.resourceId] }
11931193
ingressTargetPort: 8000
1194-
ingressExternal: true
1194+
ingressExternal: enablePrivateNetworking ? false : true
11951195
activeRevisionsMode: 'Single'
11961196
corsPolicy: {
11971197
allowedOrigins: [
@@ -1399,7 +1399,7 @@ module containerAppMcp 'br/public:avm/res/app/container-app:0.18.1' = {
13991399
environmentResourceId: containerAppEnvironment.outputs.resourceId
14001400
managedIdentities: { userAssignedResourceIds: [userAssignedIdentity.outputs.resourceId] }
14011401
ingressTargetPort: 9000
1402-
ingressExternal: true
1402+
ingressExternal: enablePrivateNetworking ? false : true
14031403
activeRevisionsMode: 'Single'
14041404
corsPolicy: {
14051405
allowedOrigins: [
@@ -1531,6 +1531,7 @@ module webSite 'modules/web-sites.bicep' = {
15311531
WEBSITES_PORT: '3000'
15321532
WEBSITES_CONTAINER_START_TIME_LIMIT: '1800' // 30 minutes, adjust as needed
15331533
BACKEND_API_URL: 'https://${containerApp.outputs.fqdn}'
1534+
PROXY_API_REQUESTS: enablePrivateNetworking ? 'true' : 'false'
15341535
AUTH_ENABLED: 'false'
15351536
}
15361537
// WAF aligned configuration for Monitoring

infra/main_custom.bicep

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1140,8 +1140,8 @@ module containerAppEnvironment 'br/public:avm/res/app/managed-environment:0.11.2
11401140
tags: tags
11411141
enableTelemetry: enableTelemetry
11421142
// WAF aligned configuration for Private Networking
1143-
publicNetworkAccess: 'Enabled' // Always enabling the publicNetworkAccess for Container App Environment
1144-
internal: false // Must be false when publicNetworkAccess is'Enabled'
1143+
publicNetworkAccess: enablePrivateNetworking ? 'Disabled' : 'Enabled'
1144+
internal: enablePrivateNetworking ? true : false
11451145
infrastructureSubnetResourceId: enablePrivateNetworking ? virtualNetwork.?outputs.?containerSubnetResourceId : null
11461146
// WAF aligned configuration for Monitoring
11471147
appLogsConfiguration: enableMonitoring
@@ -1218,7 +1218,7 @@ module containerApp 'br/public:avm/res/app/container-app:0.18.1' = {
12181218
environmentResourceId: containerAppEnvironment.outputs.resourceId
12191219
managedIdentities: { userAssignedResourceIds: [userAssignedIdentity.outputs.resourceId] }
12201220
ingressTargetPort: 8000
1221-
ingressExternal: true
1221+
ingressExternal: enablePrivateNetworking ? false : true
12221222
activeRevisionsMode: 'Single'
12231223
corsPolicy: {
12241224
allowedOrigins: [
@@ -1441,7 +1441,7 @@ module containerAppMcp 'br/public:avm/res/app/container-app:0.18.1' = {
14411441
environmentResourceId: containerAppEnvironment.outputs.resourceId
14421442
managedIdentities: { userAssignedResourceIds: [userAssignedIdentity.outputs.resourceId] }
14431443
ingressTargetPort: 9000
1444-
ingressExternal: true
1444+
ingressExternal: enablePrivateNetworking ? false : true
14451445
activeRevisionsMode: 'Single'
14461446
corsPolicy: {
14471447
allowedOrigins: [
@@ -1582,6 +1582,7 @@ module webSite 'modules/web-sites.bicep' = {
15821582
WEBSITES_PORT: '8000'
15831583
//WEBSITES_CONTAINER_START_TIME_LIMIT: '1800' // 30 minutes, adjust as needed
15841584
BACKEND_API_URL: 'https://${containerApp.outputs.fqdn}'
1585+
PROXY_API_REQUESTS: enablePrivateNetworking ? 'true' : 'false'
15851586
AUTH_ENABLED: 'false'
15861587
ENABLE_ORYX_BUILD: 'True'
15871588
}

src/frontend/frontend_server.py

Lines changed: 52 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,11 @@
11
import os
22

3+
import httpx
34
import uvicorn
45
from dotenv import load_dotenv
5-
from fastapi import FastAPI
6+
from fastapi import FastAPI, Request
67
from fastapi.middleware.cors import CORSMiddleware
7-
from fastapi.responses import FileResponse
8+
from fastapi.responses import FileResponse, StreamingResponse
89
from fastapi.staticfiles import StaticFiles
910

1011
# Load environment variables from .env file
@@ -23,6 +24,10 @@
2324
BUILD_DIR = os.path.join(os.path.dirname(__file__), "build")
2425
INDEX_HTML = os.path.join(BUILD_DIR, "index.html")
2526

27+
# Proxy configuration for WAF/private networking deployments
28+
PROXY_API_REQUESTS = os.getenv("PROXY_API_REQUESTS", "false").lower() == "true"
29+
BACKEND_API_URL = os.getenv("BACKEND_API_URL", "http://localhost:8000")
30+
2631
# Serve static files from build directory
2732
app.mount(
2833
"/assets", StaticFiles(directory=os.path.join(BUILD_DIR, "assets")), name="assets"
@@ -36,17 +41,59 @@ async def serve_index():
3641

3742
@app.get("/config")
3843
async def get_config():
39-
backend_url = os.getenv("BACKEND_API_URL", "http://localhost:8000")
4044
auth_enabled = os.getenv("AUTH_ENABLED", "false")
41-
backend_url = backend_url + "/api"
45+
46+
if PROXY_API_REQUESTS:
47+
# WAF mode: frontend proxies API calls, so tell browser to use same origin
48+
api_url = "/api"
49+
else:
50+
# Non-WAF mode: browser calls backend directly
51+
backend_url = os.getenv("BACKEND_API_URL", "http://localhost:8000")
52+
api_url = backend_url + "/api"
4253

4354
config = {
44-
"API_URL": backend_url,
55+
"API_URL": api_url,
4556
"ENABLE_AUTH": auth_enabled,
4657
}
4758
return config
4859

4960

61+
@app.get("/health")
62+
async def health():
63+
return {"status": "healthy"}
64+
65+
66+
# API proxy routes for WAF/private networking deployments
67+
if PROXY_API_REQUESTS:
68+
69+
@app.api_route("/api/{path:path}", methods=["GET", "POST", "PUT", "DELETE", "PATCH"])
70+
async def proxy_api(request: Request, path: str):
71+
"""Proxy API requests to the private backend over VNet."""
72+
target_url = f"{BACKEND_API_URL}/api/{path}"
73+
query_string = str(request.query_params)
74+
if query_string:
75+
target_url = f"{target_url}?{query_string}"
76+
77+
headers = dict(request.headers)
78+
headers.pop("host", None)
79+
80+
body = await request.body()
81+
82+
async with httpx.AsyncClient(timeout=300.0) as client:
83+
response = await client.request(
84+
method=request.method,
85+
url=target_url,
86+
headers=headers,
87+
content=body,
88+
)
89+
90+
return StreamingResponse(
91+
iter([response.content]),
92+
status_code=response.status_code,
93+
headers=dict(response.headers),
94+
)
95+
96+
5097
@app.get("/{full_path:path}")
5198
async def serve_app(full_path: str):
5299
# Remediation: normalize and check containment before serving

src/frontend/requirements.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
fastapi
22
uvicorn[standard]
33
# uvicorn removed and added above to allow websocket support
4+
httpx
45
jinja2
56
azure-identity
67
python-dotenv

0 commit comments

Comments
 (0)