Networking & Security
Defense in depth: VNet isolation, private endpoints, Entra ID integration, Key Vault for secrets, and the HTTP security headers that protect the agent runtime.
VNet topology
The recommended production layout uses a single VNet with four purpose-specific subnets. Each subnet isolates a class of traffic — compute workloads, data-plane private endpoints, AI service endpoints, and platform services — so that Network Security Groups (NSGs) can enforce least-privilege rules at the subnet boundary.
VNet integration is optional for dev/staging. Container Apps supports VNet injection for production workloads, but running without a VNet is simpler and cheaper for non-production environments.
Private endpoints
Private endpoints keep data-plane traffic on the Azure backbone. Each service gets its own private endpoint in the appropriate subnet, with a corresponding Private DNS Zone for name resolution.
| Service | Private DNS Zone | Purpose |
|---|---|---|
| Azure OpenAI | privatelink.openai.azure.com |
LLM + embedding calls stay on the backbone |
| AI Search | privatelink.search.windows.net |
RAG queries never hit the public internet |
| Knowledge graph backend | privatelink.postgres.database.azure.com (Azure DB for PostgreSQL Flexible Server) — Neo4j on AKS uses a private cluster IP instead |
Knowledge graph reads/writes |
| Blob Storage | privatelink.blob.core.windows.net |
Document ingestion, FAISS indices |
| Key Vault | privatelink.vaultcore.azure.net |
Secret retrieval at startup |
Bicep example
A private endpoint for Azure OpenAI. The same pattern applies to every service in the
table above — change the privateLinkServiceId and
groupIds for the target resource.
resource openAiPrivateEndpoint 'Microsoft.Network/privateEndpoints@2024-01-01' = {
name: 'pe-openai'
location: location
properties: {
subnet: { id: aiSubnet.id }
privateLinkServiceConnections: [
{
name: 'openai-connection'
properties: {
privateLinkServiceId: openAiAccount.id
groupIds: ['account']
}
}
]
}
}
Entra ID integration
The harness uses three identity scenarios, each addressing a different trust boundary.
1. User authentication
Users sign in via Entra ID using the OpenID Connect (OIDC) protocol. The Angular/React dashboard uses MSAL.js with PKCE to acquire tokens client-side. The API validates JWT tokens on every request. An Entra ID App Registration is required with configured redirect URIs and API scopes.
2. Managed Identity (service-to-service)
Container Apps get a system-assigned managed identity. This identity authenticates to Azure OpenAI, Key Vault, Blob Storage, and AI Search without any stored credentials. The following role assignments are required:
| Role | Target Resource |
|---|---|
Cognitive Services OpenAI User |
Azure OpenAI |
Key Vault Secrets User |
Key Vault |
Storage Blob Data Reader |
Blob Storage |
Search Index Data Reader |
AI Search |
3. MCP Server JWT auth
External agents connecting to the MCP server authenticate with JWT bearer tokens. The
MCP server validates issuer, audience, and signing key. Configuration is managed via
AppConfig.MCP.Server.Authentication.
Managed Identity eliminates secret rotation burden and removes the risk of key leakage. API keys are acceptable only for local development. In production, every service-to-service call should use Managed Identity or workload identity federation.
MCP server exposure
How you expose the MCP server depends on who needs to reach it.
Internal consumers (same VNet)
Use internal ingress on Container Apps. MCP clients connect via the VNet-internal FQDN. No public IP is allocated. This is the default for agents running within your infrastructure.
External consumers (other organizations/services)
Use external ingress + Azure Front Door with WAF. JWT auth is mandatory. Rate limiting is recommended to prevent abuse. Front Door provides DDoS protection, TLS termination, and geographic routing.
CORS policy
Explicit allowlist only. Never wildcard in production. The allowed origins must match the exact scheme and host of your dashboard deployment.
{
"MCP": {
"Server": {
"Transport": "Http",
"Port": 8082,
"Authentication": {
"Authority": "https://login.microsoftonline.com/{tenantId}/v2.0",
"Audience": "api://harness-mcp",
"ValidIssuers": ["https://sts.windows.net/{tenantId}/"]
},
"Cors": {
"AllowedOrigins": ["https://your-app.azurecontainerapps.io"]
}
}
}
}
HTTP security headers
Every response from the harness includes these headers. They form the browser-side layer of the defense-in-depth strategy.
| Header | Value | Why |
|---|---|---|
X-Frame-Options |
DENY |
Prevents clickjacking |
X-Content-Type-Options |
nosniff |
Prevents MIME-type sniffing |
Strict-Transport-Security |
max-age=31536000; includeSubDomains |
Forces HTTPS |
Content-Security-Policy |
default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline' fonts.googleapis.com |
Prevents XSS |
Referrer-Policy |
strict-origin-when-cross-origin |
Controls referrer leakage |
Permissions-Policy |
camera=(), microphone=(), geolocation=() |
Disables unused browser APIs |
These headers are configured in the ASP.NET Core middleware pipeline.
See Infrastructure.Common/Middleware/SecurityHeadersMiddleware.cs
in the Developer Guide for the implementation.
Key Vault configuration
All secrets — API keys, connection strings, certificates — live in Azure Key Vault. The harness loads them at startup via the Azure Key Vault configuration provider, which integrates directly with the .NET configuration system.
Secrets map to configuration paths using -- as a delimiter. For example,
a Key Vault secret named AI--AgentFramework--AzureOpenAI--ApiKey maps to
the configuration path AI:AgentFramework:AzureOpenAI:ApiKey. This
convention means you can reference any configuration value as a Key Vault secret
without changing application code.
: separator used in .NET configuration paths.
-- to : at load time.
The container app authenticates to Key Vault using its system-assigned managed identity
with the Key Vault Secrets User role. No connection string or client secret
is needed — the identity is assigned at the infrastructure level.