Skip to content

Commit 55450b2

Browse files
Merge pull request #608 from microsoft/feature/US-39405-waf-restrict-api-private-aks
feat: restrict backend API to private access in WAF mode #39405
2 parents eddd852 + 9e276e1 commit 55450b2

6 files changed

Lines changed: 185 additions & 25 deletions

File tree

App/frontend-app/src/components/documentViewer/iFrameComponent.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ export function IFrameComponent({ className, metadata, urlWithSasToken, iframeKe
3636
);
3737
}
3838
case "application/pdf": {
39-
const url = new URL(urlWithSasToken);
39+
const url = new URL(urlWithSasToken, window.location.origin);
4040
url.searchParams.append("embed", "True");
4141

4242
return <iframe title="PDF Viewer" key={iframeKey} src={url.toString()} width="100%" height="100%" />;

App/frontend-app/vite.config.ts

Lines changed: 30 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,35 @@
1-
import { defineConfig } from "vite";
1+
import { defineConfig, loadEnv } from "vite";
22
import react from "@vitejs/plugin-react";
33
import postcss from "./postcss.config.js";
44

5-
export default defineConfig({
6-
plugins: [react()],
7-
css: {
8-
postcss,
9-
},
10-
server: {
11-
watch: {
12-
usePolling: true,
5+
export default defineConfig(({ mode }) => {
6+
const env = loadEnv(mode, process.cwd(), '');
7+
const backendTarget = env.BACKEND_PROXY_TARGET || 'http://aiservice-service:80';
8+
9+
return {
10+
plugins: [react()],
11+
css: {
12+
postcss,
13+
},
14+
server: {
15+
watch: {
16+
usePolling: true,
17+
},
18+
host: true,
19+
strictPort: true,
20+
port: 5900,
21+
allowedHosts: true,
22+
proxy: {
23+
'/backend': {
24+
target: backendTarget,
25+
changeOrigin: true,
26+
rewrite: (path: string) => path.replace(/^\/backend/, ''),
27+
},
28+
'/api': {
29+
target: backendTarget,
30+
changeOrigin: true,
31+
},
32+
},
1333
},
14-
host: true,
15-
strictPort: true,
16-
port : 5900,
17-
allowedHosts: true
18-
}
34+
};
1935
});
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
apiVersion: networking.k8s.io/v1
2+
kind: Ingress
3+
metadata:
4+
name: kmgs-ingress
5+
namespace: ns-km
6+
annotations:
7+
cert-manager.io/cluster-issuer: "letsencrypt-prod"
8+
nginx.ingress.kubernetes.io/proxy-body-size: "0"
9+
nginx.ingress.kubernetes.io/ssl-redirect: "false"
10+
nginx.ingress.kubernetes.io/use-regex: "true"
11+
nginx.ingress.kubernetes.io/proxy-connect-timeout: "3600"
12+
nginx.ingress.kubernetes.io/proxy-send-timeout: "3600"
13+
nginx.ingress.kubernetes.io/proxy-read-timeout: "3600"
14+
nginx.ingress.kubernetes.io/send-timeout: "3600"
15+
nginx.ingress.kubernetes.io/rewrite-target: /$2
16+
spec:
17+
ingressClassName: webapprouting.kubernetes.azure.com
18+
defaultBackend:
19+
service:
20+
name: frontapp-service
21+
port:
22+
number: 5900
23+
rules:
24+
- host: {{ fqdn }}
25+
http:
26+
paths:
27+
- path: /()(.*)
28+
pathType: Prefix
29+
backend:
30+
service:
31+
name: frontapp-service
32+
port:
33+
number: 5900
34+
tls:
35+
- hosts:
36+
- {{ fqdn }}
37+
secretName: secret-kmgs
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
# NetworkPolicy to restrict backend services (aiservice, kernelmemory) to only
2+
# accept traffic from authorized pods within the cluster.
3+
# Frontend (frontapp) and aiservice can reach backend; external traffic is blocked.
4+
# Applied automatically in WAF deployment mode.
5+
---
6+
apiVersion: networking.k8s.io/v1
7+
kind: NetworkPolicy
8+
metadata:
9+
name: deny-external-to-backend
10+
namespace: ns-km
11+
spec:
12+
podSelector:
13+
matchExpressions:
14+
- key: app
15+
operator: In
16+
values:
17+
- aiservice
18+
- kernelmemory
19+
policyTypes:
20+
- Ingress
21+
ingress:
22+
# Allow traffic from frontend pods (Vite proxy forwards API calls to aiservice)
23+
- from:
24+
- podSelector:
25+
matchLabels:
26+
app: frontapp
27+
# Allow traffic from aiservice to kernelmemory (inter-service communication)
28+
- from:
29+
- podSelector:
30+
matchLabels:
31+
app: aiservice
32+
# Allow traffic from ingress controller namespace (app-routing-system)
33+
- from:
34+
- namespaceSelector:
35+
matchLabels:
36+
kubernetes.io/metadata.name: app-routing-system

Deployment/resourcedeployment.ps1

Lines changed: 52 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -551,6 +551,16 @@ try {
551551

552552
Write-Host "Validation Completed" -ForegroundColor Green
553553

554+
# Detect WAF deployment mode from resource group tag (set by Bicep: Type = 'WAF' | 'Non-WAF')
555+
$isWafDeployment = $false
556+
$rgDeploymentType = az group show --name $deploymentResult.ResourceGroupName --query "tags.Type" -o tsv 2>$null
557+
if ($rgDeploymentType -eq "WAF") {
558+
$isWafDeployment = $true
559+
Write-Host "WAF deployment mode detected. Backend APIs will be restricted to private access." -ForegroundColor Cyan
560+
} else {
561+
Write-Host "Non-WAF deployment mode detected. Standard deployment will be used." -ForegroundColor Yellow
562+
}
563+
554564
# Step 1-3 Loading aiservice's configution file template then replace the placeholder with the actual values
555565
# Define the placeholders and their corresponding values for AI service configuration
556566

@@ -729,14 +739,21 @@ try {
729739
# 6-1. Get Az Network resource Name with the public IP address
730740
Write-Host "Assign DNS Name to the public IP address" -ForegroundColor Green
731741
$publicIpName=$(az network public-ip list --resource-group $aksResourceGroupName --query "[?ipAddress=='$externalIP'].name" --output tsv)
732-
# 6-2. Generate Unique backend API fqdn Name - esgdocanalysis-3 digit random number with padding 0
733-
$dnsName = "kmgs$($(Get-Random -Minimum 0 -Maximum 9999).ToString("D4"))"
734-
735-
# Validate if the AKS Resource Group Name, Public IP name and DNS Name are provided
742+
# 6-2. Reuse existing DNS name if already assigned, otherwise generate a new one
743+
# Validate if the AKS Resource Group Name and Public IP name are provided
736744
ValidateVariableIsNullOrEmpty -variableValue $aksResourceGroupName -variableName "AKS Resource Group name"
737745

738746
ValidateVariableIsNullOrEmpty -variableValue $publicIpName -variableName "Public IP name"
739747

748+
$existingDnsName = az network public-ip show --resource-group $aksResourceGroupName --name $publicIpName --query "dnsSettings.domainNameLabel" --output tsv 2>$null
749+
if ($existingDnsName) {
750+
Write-Host "Reusing existing DNS name: $existingDnsName" -ForegroundColor Yellow
751+
$dnsName = $existingDnsName
752+
} else {
753+
$dnsName = "kmgs$($(Get-Random -Minimum 0 -Maximum 9999).ToString("D4"))"
754+
Write-Host "Generated new DNS name: $dnsName" -ForegroundColor Green
755+
}
756+
740757
ValidateVariableIsNullOrEmpty -variableValue $dnsName -variableName "DNS Name"
741758

742759
# 6-3. Assign DNS Name to the public IP address
@@ -834,12 +851,19 @@ try {
834851
$certManagerTemplate | Set-Content -Path $certManagerPath -Force
835852

836853
# 3.2 Update deploy.ingress.yaml.template file and save as deploy.ingress.yaml
837-
# webfront / apibackend
854+
# In WAF mode, use the WAF-specific template that only exposes the frontend publicly
855+
# In non-WAF mode, use the standard template that exposes both frontend and backend
838856
$ingressPlaceholders = @{
839857
'{{ fqdn }}' = $fqdn
840858
}
841859

842-
$ingressTemplate = Get-Content -Path .\kubernetes\deploy.ingress.yaml.template -Raw
860+
if ($isWafDeployment) {
861+
$ingressTemplatePath = ".\kubernetes\deploy.ingress.waf.yaml.template"
862+
Write-Host "Using WAF ingress template (frontend-only public access)." -ForegroundColor Cyan
863+
} else {
864+
$ingressTemplatePath = ".\kubernetes\deploy.ingress.yaml.template"
865+
}
866+
$ingressTemplate = Get-Content -Path $ingressTemplatePath -Raw
843867
$ingress = Invoke-PlaceholdersReplacement $ingressTemplate $ingressPlaceholders
844868
$ingressPath = ".\kubernetes\deploy.ingress.yaml"
845869
$ingress | Set-Content -Path $ingressPath -Force
@@ -940,8 +964,16 @@ try {
940964
# Front App
941965
###############################
942966

943-
$frontAppConfigServicePlaceholders = @{
944-
'{{ backend-fqdn }}' = "https://${fqdn}/backend"
967+
# WAF mode: use relative path so browser calls go through frontend Vite proxy (backend stays private)
968+
# Standard mode: use absolute URL (backend is on public ingress)
969+
if ($isWafDeployment) {
970+
$frontAppConfigServicePlaceholders = @{
971+
'{{ backend-fqdn }}' = "/backend"
972+
}
973+
} else {
974+
$frontAppConfigServicePlaceholders = @{
975+
'{{ backend-fqdn }}' = "https://${fqdn}/backend"
976+
}
945977
}
946978

947979
## Load and update the front app configuration template
@@ -1006,7 +1038,18 @@ try {
10061038
kubectl apply -f "./kubernetes/deploy.service.yaml" -n $kubenamespace
10071039

10081040
# 5.5. Deploy Ingress Controller in Kubernetes for external access
1009-
kubectl apply -f "./kubernetes/deploy.ingress.yaml" -n $kubenamespace
1041+
if ($isWafDeployment) {
1042+
# WAF mode: use WAF ingress (no public backend route — backend traffic proxied through frontend)
1043+
Write-Host "Applying WAF-specific ingress (backend is private, proxied through frontend)..." -ForegroundColor Cyan
1044+
kubectl apply -f "./kubernetes/deploy.ingress.yaml" -n $kubenamespace
1045+
1046+
# Deploy network policies to restrict direct backend pod access
1047+
kubectl apply -f "./kubernetes/deploy.networkpolicy.yaml.template" -n $kubenamespace
1048+
Write-Host "WAF ingress and network policies applied successfully." -ForegroundColor Green
1049+
} else {
1050+
# Standard mode: public ingress with backend route
1051+
kubectl apply -f "./kubernetes/deploy.ingress.yaml" -n $kubenamespace
1052+
}
10101053

10111054
# #####################################################################
10121055
# # Data file uploading

docs/DeploymentGuide.md

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -122,6 +122,8 @@ Review the configuration options below. You can customize any settings that meet
122122
| **Use Case** | POCs, development, testing | Production workloads |
123123
| **Framework** | Basic configuration | [Well-Architected Framework](https://learn.microsoft.com/en-us/azure/well-architected/) |
124124
| **Features** | Core functionality | Reliability, security, operational excellence |
125+
| **Backend API Access** | Public (accessible via ingress) | Private (internal-only, not exposed to public internet) |
126+
| **Network Policies** | None | Kubernetes NetworkPolicy isolates backend from external traffic |
125127

126128
**To use production configuration:**
127129

@@ -145,7 +147,33 @@ azd env set AZURE_ENV_VM_ADMIN_USERNAME <your-username>
145147
azd env set AZURE_ENV_VM_ADMIN_PASSWORD <your-password>
146148
```
147149

148-
### 3.3 Advanced Configuration (Optional)
150+
### 3.3 WAF Deployment: Network Architecture (Production Only)
151+
152+
> **Note:** This section describes the networking architecture automatically configured when using the **Production** deployment type (WAF mode).
153+
154+
When deploying with WAF configuration (`enablePrivateNetworking: true`), the following security measures are applied:
155+
156+
- **Public Ingress (Frontend Only)**: Only the frontend web application is exposed through the public nginx ingress. **No backend API routes are on the public ingress** — backend services are completely private.
157+
- **Server-Side Proxy**: The frontend container (Vite) acts as a reverse proxy. Browser API calls to `/backend` are intercepted by the frontend server and forwarded internally to the backend service via ClusterIP DNS — the request never leaves the cluster.
158+
- **ClusterIP Services**: Backend services (`aiservice`, `kernelmemory`) use ClusterIP services for internal communication only. They have no public IP or external load balancer.
159+
- **Kubernetes Network Policies**: NetworkPolicy resources enforce traffic isolation — backend pods only accept traffic from frontend pods and the ingress controller within the cluster.
160+
- **Private Endpoints**: All Azure PaaS services (Cosmos DB, Storage, Search, OpenAI, etc.) use private endpoints and are not accessible from the public internet.
161+
162+
**Traffic Flow (WAF mode):**
163+
```
164+
Internet → Public Ingress (nginx) → / → Frontend (frontapp:5900)
165+
166+
Vite Proxy (server-side)
167+
/backend → aiservice (ClusterIP, internal only)
168+
/api → aiservice (ClusterIP, internal only)
169+
170+
Azure PaaS (via Private Endpoints)
171+
172+
Backend API from internet → NOT ROUTABLE (no public ingress route exists)
173+
Direct access to backend pods → BLOCKED by NetworkPolicy
174+
```
175+
176+
### 3.4 Advanced Configuration (Optional)
149177

150178
<details>
151179
<summary><b>Configurable Parameters</b></summary>

0 commit comments

Comments
 (0)