diff --git a/App/frontend-app/src/components/documentViewer/iFrameComponent.tsx b/App/frontend-app/src/components/documentViewer/iFrameComponent.tsx
index 4a89f62b..58cf45d3 100644
--- a/App/frontend-app/src/components/documentViewer/iFrameComponent.tsx
+++ b/App/frontend-app/src/components/documentViewer/iFrameComponent.tsx
@@ -36,7 +36,7 @@ export function IFrameComponent({ className, metadata, urlWithSasToken, iframeKe
);
}
case "application/pdf": {
- const url = new URL(urlWithSasToken);
+ const url = new URL(urlWithSasToken, window.location.origin);
url.searchParams.append("embed", "True");
return ;
diff --git a/App/frontend-app/vite.config.ts b/App/frontend-app/vite.config.ts
index 7105ae72..4ccea1db 100644
--- a/App/frontend-app/vite.config.ts
+++ b/App/frontend-app/vite.config.ts
@@ -1,19 +1,35 @@
-import { defineConfig } from "vite";
+import { defineConfig, loadEnv } from "vite";
import react from "@vitejs/plugin-react";
import postcss from "./postcss.config.js";
-export default defineConfig({
- plugins: [react()],
- css: {
- postcss,
- },
- server: {
- watch: {
- usePolling: true,
+export default defineConfig(({ mode }) => {
+ const env = loadEnv(mode, process.cwd(), '');
+ const backendTarget = env.BACKEND_PROXY_TARGET || 'http://aiservice-service:80';
+
+ return {
+ plugins: [react()],
+ css: {
+ postcss,
+ },
+ server: {
+ watch: {
+ usePolling: true,
+ },
+ host: true,
+ strictPort: true,
+ port: 5900,
+ allowedHosts: true,
+ proxy: {
+ '/backend': {
+ target: backendTarget,
+ changeOrigin: true,
+ rewrite: (path: string) => path.replace(/^\/backend/, ''),
+ },
+ '/api': {
+ target: backendTarget,
+ changeOrigin: true,
+ },
+ },
},
- host: true,
- strictPort: true,
- port : 5900,
- allowedHosts: true
- }
+ };
});
diff --git a/Deployment/kubernetes/deploy.ingress.waf.yaml.template b/Deployment/kubernetes/deploy.ingress.waf.yaml.template
new file mode 100644
index 00000000..65bf5009
--- /dev/null
+++ b/Deployment/kubernetes/deploy.ingress.waf.yaml.template
@@ -0,0 +1,37 @@
+apiVersion: networking.k8s.io/v1
+kind: Ingress
+metadata:
+ name: kmgs-ingress
+ namespace: ns-km
+ annotations:
+ cert-manager.io/cluster-issuer: "letsencrypt-prod"
+ nginx.ingress.kubernetes.io/proxy-body-size: "0"
+ nginx.ingress.kubernetes.io/ssl-redirect: "false"
+ nginx.ingress.kubernetes.io/use-regex: "true"
+ nginx.ingress.kubernetes.io/proxy-connect-timeout: "3600"
+ nginx.ingress.kubernetes.io/proxy-send-timeout: "3600"
+ nginx.ingress.kubernetes.io/proxy-read-timeout: "3600"
+ nginx.ingress.kubernetes.io/send-timeout: "3600"
+ nginx.ingress.kubernetes.io/rewrite-target: /$2
+spec:
+ ingressClassName: webapprouting.kubernetes.azure.com
+ defaultBackend:
+ service:
+ name: frontapp-service
+ port:
+ number: 5900
+ rules:
+ - host: {{ fqdn }}
+ http:
+ paths:
+ - path: /()(.*)
+ pathType: Prefix
+ backend:
+ service:
+ name: frontapp-service
+ port:
+ number: 5900
+ tls:
+ - hosts:
+ - {{ fqdn }}
+ secretName: secret-kmgs
diff --git a/Deployment/kubernetes/deploy.networkpolicy.yaml.template b/Deployment/kubernetes/deploy.networkpolicy.yaml.template
new file mode 100644
index 00000000..90f42efc
--- /dev/null
+++ b/Deployment/kubernetes/deploy.networkpolicy.yaml.template
@@ -0,0 +1,36 @@
+# NetworkPolicy to restrict backend services (aiservice, kernelmemory) to only
+# accept traffic from authorized pods within the cluster.
+# Frontend (frontapp) and aiservice can reach backend; external traffic is blocked.
+# Applied automatically in WAF deployment mode.
+---
+apiVersion: networking.k8s.io/v1
+kind: NetworkPolicy
+metadata:
+ name: deny-external-to-backend
+ namespace: ns-km
+spec:
+ podSelector:
+ matchExpressions:
+ - key: app
+ operator: In
+ values:
+ - aiservice
+ - kernelmemory
+ policyTypes:
+ - Ingress
+ ingress:
+ # Allow traffic from frontend pods (Vite proxy forwards API calls to aiservice)
+ - from:
+ - podSelector:
+ matchLabels:
+ app: frontapp
+ # Allow traffic from aiservice to kernelmemory (inter-service communication)
+ - from:
+ - podSelector:
+ matchLabels:
+ app: aiservice
+ # Allow traffic from ingress controller namespace (app-routing-system)
+ - from:
+ - namespaceSelector:
+ matchLabels:
+ kubernetes.io/metadata.name: app-routing-system
diff --git a/Deployment/resourcedeployment.ps1 b/Deployment/resourcedeployment.ps1
index 24dbcfaf..12d71f30 100644
--- a/Deployment/resourcedeployment.ps1
+++ b/Deployment/resourcedeployment.ps1
@@ -551,6 +551,16 @@ try {
Write-Host "Validation Completed" -ForegroundColor Green
+ # Detect WAF deployment mode from resource group tag (set by Bicep: Type = 'WAF' | 'Non-WAF')
+ $isWafDeployment = $false
+ $rgDeploymentType = az group show --name $deploymentResult.ResourceGroupName --query "tags.Type" -o tsv 2>$null
+ if ($rgDeploymentType -eq "WAF") {
+ $isWafDeployment = $true
+ Write-Host "WAF deployment mode detected. Backend APIs will be restricted to private access." -ForegroundColor Cyan
+ } else {
+ Write-Host "Non-WAF deployment mode detected. Standard deployment will be used." -ForegroundColor Yellow
+ }
+
# Step 1-3 Loading aiservice's configution file template then replace the placeholder with the actual values
# Define the placeholders and their corresponding values for AI service configuration
@@ -729,14 +739,21 @@ try {
# 6-1. Get Az Network resource Name with the public IP address
Write-Host "Assign DNS Name to the public IP address" -ForegroundColor Green
$publicIpName=$(az network public-ip list --resource-group $aksResourceGroupName --query "[?ipAddress=='$externalIP'].name" --output tsv)
- # 6-2. Generate Unique backend API fqdn Name - esgdocanalysis-3 digit random number with padding 0
- $dnsName = "kmgs$($(Get-Random -Minimum 0 -Maximum 9999).ToString("D4"))"
-
- # Validate if the AKS Resource Group Name, Public IP name and DNS Name are provided
+ # 6-2. Reuse existing DNS name if already assigned, otherwise generate a new one
+ # Validate if the AKS Resource Group Name and Public IP name are provided
ValidateVariableIsNullOrEmpty -variableValue $aksResourceGroupName -variableName "AKS Resource Group name"
ValidateVariableIsNullOrEmpty -variableValue $publicIpName -variableName "Public IP name"
+ $existingDnsName = az network public-ip show --resource-group $aksResourceGroupName --name $publicIpName --query "dnsSettings.domainNameLabel" --output tsv 2>$null
+ if ($existingDnsName) {
+ Write-Host "Reusing existing DNS name: $existingDnsName" -ForegroundColor Yellow
+ $dnsName = $existingDnsName
+ } else {
+ $dnsName = "kmgs$($(Get-Random -Minimum 0 -Maximum 9999).ToString("D4"))"
+ Write-Host "Generated new DNS name: $dnsName" -ForegroundColor Green
+ }
+
ValidateVariableIsNullOrEmpty -variableValue $dnsName -variableName "DNS Name"
# 6-3. Assign DNS Name to the public IP address
@@ -834,12 +851,19 @@ try {
$certManagerTemplate | Set-Content -Path $certManagerPath -Force
# 3.2 Update deploy.ingress.yaml.template file and save as deploy.ingress.yaml
- # webfront / apibackend
+ # In WAF mode, use the WAF-specific template that only exposes the frontend publicly
+ # In non-WAF mode, use the standard template that exposes both frontend and backend
$ingressPlaceholders = @{
'{{ fqdn }}' = $fqdn
}
- $ingressTemplate = Get-Content -Path .\kubernetes\deploy.ingress.yaml.template -Raw
+ if ($isWafDeployment) {
+ $ingressTemplatePath = ".\kubernetes\deploy.ingress.waf.yaml.template"
+ Write-Host "Using WAF ingress template (frontend-only public access)." -ForegroundColor Cyan
+ } else {
+ $ingressTemplatePath = ".\kubernetes\deploy.ingress.yaml.template"
+ }
+ $ingressTemplate = Get-Content -Path $ingressTemplatePath -Raw
$ingress = Invoke-PlaceholdersReplacement $ingressTemplate $ingressPlaceholders
$ingressPath = ".\kubernetes\deploy.ingress.yaml"
$ingress | Set-Content -Path $ingressPath -Force
@@ -940,8 +964,16 @@ try {
# Front App
###############################
- $frontAppConfigServicePlaceholders = @{
- '{{ backend-fqdn }}' = "https://${fqdn}/backend"
+ # WAF mode: use relative path so browser calls go through frontend Vite proxy (backend stays private)
+ # Standard mode: use absolute URL (backend is on public ingress)
+ if ($isWafDeployment) {
+ $frontAppConfigServicePlaceholders = @{
+ '{{ backend-fqdn }}' = "/backend"
+ }
+ } else {
+ $frontAppConfigServicePlaceholders = @{
+ '{{ backend-fqdn }}' = "https://${fqdn}/backend"
+ }
}
## Load and update the front app configuration template
@@ -1001,7 +1033,18 @@ try {
kubectl apply -f "./kubernetes/deploy.service.yaml" -n $kubenamespace
# 5.5. Deploy Ingress Controller in Kubernetes for external access
- kubectl apply -f "./kubernetes/deploy.ingress.yaml" -n $kubenamespace
+ if ($isWafDeployment) {
+ # WAF mode: use WAF ingress (no public backend route — backend traffic proxied through frontend)
+ Write-Host "Applying WAF-specific ingress (backend is private, proxied through frontend)..." -ForegroundColor Cyan
+ kubectl apply -f "./kubernetes/deploy.ingress.yaml" -n $kubenamespace
+
+ # Deploy network policies to restrict direct backend pod access
+ kubectl apply -f "./kubernetes/deploy.networkpolicy.yaml.template" -n $kubenamespace
+ Write-Host "WAF ingress and network policies applied successfully." -ForegroundColor Green
+ } else {
+ # Standard mode: public ingress with backend route
+ kubectl apply -f "./kubernetes/deploy.ingress.yaml" -n $kubenamespace
+ }
# #####################################################################
# # Data file uploading
diff --git a/docs/DeploymentGuide.md b/docs/DeploymentGuide.md
index 658e63b6..4c136737 100644
--- a/docs/DeploymentGuide.md
+++ b/docs/DeploymentGuide.md
@@ -119,6 +119,8 @@ Review the configuration options below. You can customize any settings that meet
| **Use Case** | POCs, development, testing | Production workloads |
| **Framework** | Basic configuration | [Well-Architected Framework](https://learn.microsoft.com/en-us/azure/well-architected/) |
| **Features** | Core functionality | Reliability, security, operational excellence |
+| **Backend API Access** | Public (accessible via ingress) | Private (internal-only, not exposed to public internet) |
+| **Network Policies** | None | Kubernetes NetworkPolicy isolates backend from external traffic |
**To use production configuration:**
@@ -142,7 +144,33 @@ azd env set AZURE_ENV_VM_ADMIN_USERNAME
azd env set AZURE_ENV_VM_ADMIN_PASSWORD
```
-### 3.3 Advanced Configuration (Optional)
+### 3.3 WAF Deployment: Network Architecture (Production Only)
+
+> **Note:** This section describes the networking architecture automatically configured when using the **Production** deployment type (WAF mode).
+
+When deploying with WAF configuration (`enablePrivateNetworking: true`), the following security measures are applied:
+
+- **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.
+- **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.
+- **ClusterIP Services**: Backend services (`aiservice`, `kernelmemory`) use ClusterIP services for internal communication only. They have no public IP or external load balancer.
+- **Kubernetes Network Policies**: NetworkPolicy resources enforce traffic isolation — backend pods only accept traffic from frontend pods and the ingress controller within the cluster.
+- **Private Endpoints**: All Azure PaaS services (Cosmos DB, Storage, Search, OpenAI, etc.) use private endpoints and are not accessible from the public internet.
+
+**Traffic Flow (WAF mode):**
+```
+Internet → Public Ingress (nginx) → / → Frontend (frontapp:5900)
+ ↓
+ Vite Proxy (server-side)
+ /backend → aiservice (ClusterIP, internal only)
+ /api → aiservice (ClusterIP, internal only)
+ ↓
+ Azure PaaS (via Private Endpoints)
+
+Backend API from internet → NOT ROUTABLE (no public ingress route exists)
+Direct access to backend pods → BLOCKED by NetworkPolicy
+```
+
+### 3.4 Advanced Configuration (Optional)
Configurable Parameters