Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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 <iframe title="PDF Viewer" key={iframeKey} src={url.toString()} width="100%" height="100%" />;
Expand Down
44 changes: 30 additions & 14 deletions App/frontend-app/vite.config.ts
Original file line number Diff line number Diff line change
@@ -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
}
};
});
37 changes: 37 additions & 0 deletions Deployment/kubernetes/deploy.ingress.waf.yaml.template
Original file line number Diff line number Diff line change
@@ -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
36 changes: 36 additions & 0 deletions Deployment/kubernetes/deploy.networkpolicy.yaml.template
Original file line number Diff line number Diff line change
@@ -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)
Comment thread
Abdul-Microsoft marked this conversation as resolved.
- from:
- podSelector:
matchLabels:
app: aiservice
# Allow traffic from ingress controller namespace (app-routing-system)
- from:
- namespaceSelector:
matchLabels:
kubernetes.io/metadata.name: app-routing-system
Comment thread
Abdul-Microsoft marked this conversation as resolved.
61 changes: 52 additions & 9 deletions Deployment/resourcedeployment.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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"
Comment thread
Abdul-Microsoft marked this conversation as resolved.
}
$ingressTemplate = Get-Content -Path $ingressTemplatePath -Raw
$ingress = Invoke-PlaceholdersReplacement $ingressTemplate $ingressPlaceholders
$ingressPath = ".\kubernetes\deploy.ingress.yaml"
$ingress | Set-Content -Path $ingressPath -Force
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
30 changes: 29 additions & 1 deletion docs/DeploymentGuide.md
Original file line number Diff line number Diff line change
Expand Up @@ -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:**

Expand All @@ -142,7 +144,33 @@ azd env set AZURE_ENV_VM_ADMIN_USERNAME <your-username>
azd env set AZURE_ENV_VM_ADMIN_PASSWORD <your-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)

<details>
<summary><b>Configurable Parameters</b></summary>
Expand Down
Loading