[← Back to Reviews Index](../Stewards%20Reviews%20Index.md)

# Infra Networking Review — Azure-AI-RAG-CSharp-Semantic-Kernel-Functions

| Field | Value |
|---|---|
| **Project** | Azure-AI-RAG-CSharp-Semantic-Kernel-Functions |
| **Review date** | 2026-03-22 |
| **Steward** | Infra Networking Steward |
| **Critical** | 4 |
| **Notable** | 6 |
| **Minor** | 3 |
| **Info** | 2 |
| **Total** | 15 |

---

## 1. Network Architecture Overview

The infrastructure deploys a RAG (Retrieval-Augmented Generation) workload on Azure using the following compute and data services:

| Service | Bicep File | Role |
|---|---|---|
| App Service Plan (Windows, B2) | `infra/core/host/app-service.bicep` | Hosts the .NET 8 API web app |
| App Service Plan (Linux, B2) | `infra/core/host/app-service.bicep` | Hosts the React frontend and Python Function App |
| API Web App | `infra/app/api-app.bicep` | .NET 8 RAG API; connects to Cosmos DB, AI Search, OpenAI |
| React Web App | `infra/app/web-app.bicep` | Frontend SPA; calls API Web App over public internet |
| Azure Function App (Loader) | `infra/app/loader-function.bicep` | Python blob-triggered ingestion pipeline |
| Azure Storage Account | `infra/core/storage/blob-storage-account.bicep` | Blob store for documents; triggers Function App |
| Azure Cosmos DB (NoSQL) | `infra/core/database/cosmos-db/account.bicep` | Chat history and products container |
| Azure AI Search | `infra/core/search/search-services.bicep` | RAG vector/semantic search index |
| Azure OpenAI | `infra/core/ai/openai/openai-account.bicep` | GPT-4o and text-embedding-ada-002 deployments |
| Key Vault | `infra/core/security/key-vault.bicep` | File is **empty** — no Key Vault resource is deployed |
| Application Insights + Log Analytics | `infra/core/monitor/monitoring.bicep` | Observability |

**Overall network posture: flat public internet.** There is no VNet, no subnets, no private endpoints, no NSGs, and no private DNS zones anywhere in the codebase. Every data service — Storage, Cosmos DB, AI Search, and OpenAI — is deployed with `publicNetworkAccess: 'Enabled'` (or equivalent default) and accepts traffic from any source on the internet with no IP restriction layer. The only access control in place is RBAC via a User-Assigned Managed Identity, which is correctly configured but does not substitute for network-level isolation.

---

## 2. VNet Integration Assessment

**Finding: No VNet exists in the entire infrastructure.**

The `infra/main.bicep` does not deploy a `Microsoft.Network/virtualNetworks` resource. There are no subnet definitions, no VNet integration configurations on any App Service or Function App, and no delegation of subnets to `Microsoft.Web/serverFarms`.

- `infra/app/api-app.bicep` sets `publicNetworkAccess: 'Enabled'` at line 106 and has no `virtualNetworkSubnetId` on `siteConfig`.
- `infra/app/web-app.bicep` sets `publicNetworkAccess: 'Enabled'` at line 32 with no VNet integration.
- `infra/app/loader-function.bicep` has no `publicNetworkAccess` property and no `virtualNetworkSubnetId`.
- `infra/core/host/app-service.bicep` provisions both App Service Plans at **Basic (B2)** tier, which does not support VNet integration. Standard (S1+) or Premium tier is required.

Without VNet integration, the compute tier cannot route outbound traffic through private endpoints even if they were added, and data services have no mechanism to restrict access to internal traffic only.

---

## 3. Private Endpoints Assessment

**Finding: No private endpoints are configured for any data service.**

| Service | Public Network Access Setting | Private Endpoint | IP Restriction |
|---|---|---|---|
| Storage Account | `'Enabled'` (explicit, line 15) | None | None |
| Cosmos DB | Not set (defaults to Enabled) | None | None |
| Azure AI Search | `'enabled'` (param default, line 30) | None | None |
| Azure OpenAI | `'Enabled'` (explicit, line 28) | None | None |
| Key Vault | File is empty — no KV deployed | N/A | N/A |

All four data and AI services are reachable from the public internet by any source IP. There are no `Microsoft.Network/privateEndpoints` resources, no `networkAcls` blocks on storage or Cosmos DB, and no meaningful IP allowlists in any Bicep file.

Additional note on Cosmos DB: the base `account.bicep` hardcodes `disableLocalAuth: false` in the resource body, overriding the `disableKeyBasedAuth` parameter accepted by the module. Key-based authentication is always enabled regardless of the value passed by callers.

---

## 4. CORS Assessment

**Finding: No CORS configuration is present anywhere in the Bicep files.**

- `infra/core/storage/blob-storage-account.bicep` — The `blobServices` child resource (line 22) has no `corsRules` property. Azure Storage defaults to no CORS rules (blocking browser cross-origin access), but there is no explicit restrictive policy managed in IaC to prevent accidental future CORS misconfiguration.
- The API App Service and Web App do not configure CORS via the `cors` property on `siteConfig`. CORS for the API is expected to be handled in application code, but there is no infrastructure-level backup restriction.
- Blob containers `load`, `completed`, and `images` are created without an explicit `publicAccess: 'None'` property. Only the `archive` container sets `publicAccess: 'None'` explicitly. While the storage account default is to block anonymous blob access, omitting the explicit property on individual containers is inconsistent.

---

## 5. NSG Assessment

**Finding: No Network Security Groups are defined anywhere in the infrastructure.**

There are no `Microsoft.Network/networkSecurityGroups` resources in any Bicep file. Because there is no VNet and no subnets, NSGs cannot be associated with any subnet. All network access control relies entirely on Azure service-level authentication (managed identity RBAC). There is no network-layer defence-in-depth and no NSG baseline module to build upon when a VNet is introduced.

---

## 6. Service Endpoints Assessment

**Finding: No service endpoints are configured.**

No VNet or subnet resources exist, so service endpoints (`Microsoft.Storage`, `Microsoft.DocumentDB`, `Microsoft.CognitiveServices`, `Microsoft.Search`) cannot be applied. Connections from App Service and Function App to Storage and Cosmos DB traverse the public internet tier rather than the Azure backbone private network.

---

## 7. DNS Assessment

**Finding: No private DNS zones are configured.**

Because no private endpoints exist, no private DNS zones are required today. However, if private endpoints are added in the future, the following private DNS zones must be created and linked to the VNet with A-record entries via `privateDnsZoneGroups`:

- `privatelink.blob.core.windows.net` — Storage Account
- `privatelink.documents.azure.com` — Cosmos DB
- `privatelink.search.windows.net` — Azure AI Search
- `privatelink.openai.azure.com` — Azure OpenAI
- `privatelink.vaultcore.azure.net` — Key Vault

The OpenAI account uses `customSubDomainName`, which is correct for public access but will need a private endpoint DNS override record if the service is placed behind a VNet.

---

## 8. Findings

| Severity | ID | Title | File |
|---|---|---|---|
| 🔴 Critical | INET-PVTEP-001 | Storage Account publicly accessible with no IP restriction | `infra/core/storage/blob-storage-account.bicep` |
| 🔴 Critical | INET-PVTEP-002 | Cosmos DB publicly accessible with no network restriction | `infra/core/database/cosmos-db/account.bicep` |
| 🔴 Critical | INET-PVTEP-003 | Azure AI Search publicly accessible with no IP restriction or private endpoint | `infra/core/search/search-services.bicep` |
| 🔴 Critical | INET-PVTEP-004 | Azure OpenAI publicly accessible with no network restriction | `infra/core/ai/openai/openai-account.bicep` |
| 🟡 Notable | INET-VNET-001 | No VNet deployed — entire workload runs on public internet | `infra/main.bicep` |
| 🟡 Notable | INET-VNET-002 | App Service (API) has no VNet integration | `infra/app/api-app.bicep` |
| 🟡 Notable | INET-VNET-003 | Function App has no VNet integration | `infra/app/loader-function.bicep` |
| 🟡 Notable | INET-VNET-004 | App Service Plan Basic tier prevents VNet integration | `infra/core/host/app-service.bicep` |
| 🟡 Notable | INET-KV-001 | Key Vault module file is empty — Key Vault is not deployed | `infra/core/security/key-vault.bicep` |
| 🟡 Notable | INET-CORS-001 | Storage Account blob service has no explicit CORS policy | `infra/core/storage/blob-storage-account.bicep` |
| 🟢 Minor | INET-STOR-001 | Blob containers missing explicit publicAccess: None | `infra/core/storage/blob-storage-account.bicep` |
| 🟢 Minor | INET-NSG-001 | No NSG resources exist in the infrastructure | `infra/main.bicep` |
| 🟢 Minor | INET-DNS-001 | No private DNS zones scaffolded for future private endpoint readiness | `infra/main.bicep` |
| ℹ️ Info | INET-AUTH-001 | Managed Identity RBAC used consistently across all services (positive finding) | Multiple files |
| ℹ️ Info | INET-AUTH-002 | Azure AI Search local auth correctly disabled | `infra/core/search/search-services.bicep` |

---

### Finding Details

#### INET-PVTEP-001 — Storage Account publicly accessible with no IP restriction

**Severity:** Critical

**File:** `infra/core/storage/blob-storage-account.bicep`, line 15

The storage account is deployed with `publicNetworkAccess: 'Enabled'` and `allowSharedKeyAccess: true`. No `networkAcls` property is defined. Any internet actor with a valid storage key or SAS token can reach the account. The Function App blob trigger and document ingestion pipeline use this account as their primary data surface.

**Recommendation:** Add `networkAcls` with `defaultAction: 'Deny'` and `bypass: ['AzureServices']`. Set `allowSharedKeyAccess: false` since the Function App already authenticates via managed identity. Add a private endpoint once VNet integration is in place.

---

#### INET-PVTEP-002 — Cosmos DB publicly accessible with no network restriction

**Severity:** Critical

**File:** `infra/core/database/cosmos-db/account.bicep`

The Cosmos DB account has no `publicNetworkAccess` property (defaults to enabled), no `ipRules`, no `virtualNetworkRules`, and no private endpoint. Additionally, `disableLocalAuth: false` is hardcoded in the resource body, overriding the caller's intent — key-based access is always enabled, leaving the account accessible over the public internet with account keys.

**Recommendation:** Add `publicNetworkAccess: 'Disabled'` (or at minimum `ipRules` to restrict to known App Service outbound IPs). Fix the hardcoded `disableLocalAuth: false` override to respect the parameter value. Add a private endpoint as the target state.

---

#### INET-PVTEP-003 — Azure AI Search publicly accessible with no IP restriction or private endpoint

**Severity:** Critical

**File:** `infra/core/search/search-services.bicep`, line 30

The `publicNetworkAccess` parameter defaults to `'enabled'`. The `networkRuleSet` default has `bypass: 'None'` and `ipRules: []`. The empty `ipRules` array combined with `publicNetworkAccess: 'enabled'` does not restrict access — `ipRules` only takes effect in conjunction with a `defaultAction: 'Deny'` posture on a service that supports it. The search index holds vectorised RAG document data and is fully internet-accessible.

**Recommendation:** Change the `publicNetworkAccess` parameter default to `'disabled'`. Add a private endpoint, or add explicit `ipRules` in `networkRuleSet` to restrict access to known outbound IPs of the App Service and Function App.

---

#### INET-PVTEP-004 — Azure OpenAI publicly accessible with no network restriction

**Severity:** Critical

**File:** `infra/core/ai/openai/openai-account.bicep`, line 28

The OpenAI Cognitive Services account has `publicNetworkAccess: 'Enabled'` and no `networkAcls`. The endpoint is callable from any internet source. While managed identity is used for authentication, the endpoint URL is embedded in app settings, and probing or rate-limit abuse from external actors is possible.

**Recommendation:** Add `networkAcls` with `defaultAction: 'Deny'` and allowlist the App Service and Function App outbound IPs. Add a private endpoint with `privatelink.openai.azure.com` once VNet integration is in place.

---

#### INET-VNET-001 — No VNet deployed — entire workload runs on public internet

**Severity:** Notable

**File:** `infra/main.bicep`

No `Microsoft.Network/virtualNetworks` resource is deployed. All compute-to-data connectivity traverses Azure's public network. There is no integration subnet for App Service outbound traffic and no private endpoint subnet for data services.

**Recommendation:** Add a VNet module with at minimum two subnets: an integration subnet (delegated to `Microsoft.Web/serverFarms`) for App Service and Function App outbound routing, and a private endpoint subnet for data service private endpoints.

---

#### INET-VNET-002 — App Service (API) has no VNet integration

**Severity:** Notable

**File:** `infra/app/api-app.bicep`, line 106

The API App Service sets `publicNetworkAccess: 'Enabled'` with no `virtualNetworkSubnetId` configured in `siteConfig`. Without VNet integration, even if private endpoints were added to backend services, the API cannot route outbound traffic through them.

**Recommendation:** Add `virtualNetworkSubnetId` referencing a subnet delegated to `Microsoft.Web/serverFarms`, and set `vnetRouteAllEnabled: true` on `siteConfig` to route all outbound traffic through the VNet. This requires upgrading the App Service Plan to Standard tier (see INET-VNET-004).

---

#### INET-VNET-003 — Function App has no VNet integration

**Severity:** Notable

**File:** `infra/app/loader-function.bicep`

The loader Function App has no `virtualNetworkSubnetId` and no `publicNetworkAccess` restriction. Blob trigger connections, Storage API calls, and AI Search/OpenAI outbound calls all traverse public endpoints. This is the document ingestion pipeline and handles raw document content.

**Recommendation:** Same as INET-VNET-002. Configure `virtualNetworkSubnetId` and `vnetRouteAllEnabled: true`. The Function App and API App share the Linux and Windows plans respectively, so plan upgrade (INET-VNET-004) also covers this service.

---

#### INET-VNET-004 — App Service Plan Basic tier prevents VNet integration

**Severity:** Notable

**File:** `infra/core/host/app-service.bicep`

Both App Service Plans are provisioned at `B2` (Basic tier). VNet integration for outbound traffic requires Standard (S1 or higher) or Premium tier. This is a structural blocker for INET-VNET-002 and INET-VNET-003.

**Recommendation:** Upgrade both App Service Plans from `B2` (Basic) to at minimum `S2` (Standard) or `P1v3` (Premium). This is a prerequisite for adding VNet integration to all hosted apps and functions.

---

#### INET-KV-001 — Key Vault module file is empty — Key Vault is not deployed

**Severity:** Notable

**File:** `infra/core/security/key-vault.bicep`

The Key Vault Bicep module file is effectively empty (1 line). The loader Function App has a `KeyVaultUri` app setting with an empty string value, indicating Key Vault was intended to be part of the architecture but was never implemented. Secrets that should be stored in Key Vault (e.g., Cosmos DB connection strings) are either absent from configuration or stored as plain app settings. The API App hardcodes `CosmosDb_ConnectionString` as an empty app setting, suggesting the Cosmos DB connection string is configured outside IaC.

**Recommendation:** Implement the Key Vault module with `publicNetworkAccess: 'Disabled'`, a private endpoint, RBAC role assignments for the managed identity, and network ACLs. Populate `KeyVaultUri` in the Function App. Move the Cosmos DB connection string and any other secrets to Key Vault references using `@Microsoft.KeyVault(SecretUri=...)` syntax in app settings.

---

#### INET-CORS-001 — Storage Account blob service has no explicit CORS policy

**Severity:** Notable

**File:** `infra/core/storage/blob-storage-account.bicep`, line 22

The `blobServices` child resource has no `corsRules` property defined. While the Azure default is to block all CORS (no rules), the absence of an explicit policy in IaC means any CORS rule added manually (via portal or CLI) — including a wildcard — would not be tracked or reviewed through the IaC pipeline.

**Recommendation:** Add an explicit `corsRules: []` to the `blobServices` resource if no cross-origin access is needed. If the `images` container must be accessible from the browser, define a restrictive CORS rule with specific origins. Never use `allowedOrigins: ['*']`.

---

#### INET-STOR-001 — Blob containers missing explicit publicAccess: None

**Severity:** Minor

**File:** `infra/core/storage/blob-storage-account.bicep`

The `load`, `completed`, and `images` containers are created without a `properties.publicAccess` property. Only the `archive` container sets `publicAccess: 'None'` explicitly. While the storage account default prevents anonymous blob access, inconsistent explicit settings create drift risk — especially given that the storage account itself has `publicNetworkAccess: 'Enabled'`.

**Recommendation:** Add `properties: { publicAccess: 'None' }` to all three containers (`load`, `completed`, `images`) for consistency and defence-in-depth.

---

#### INET-NSG-001 — No NSG resources exist in the infrastructure

**Severity:** Minor

**File:** `infra/main.bicep`

No `Microsoft.Network/networkSecurityGroups` resources are defined. When a VNet is introduced, NSGs must be defined and attached to subnets before they can enforce least-privilege traffic rules. There is no NSG module or baseline to build from.

**Recommendation:** Create an NSG module alongside the VNet module. Define explicit allow rules (e.g., HTTPS inbound from known sources, deny-all default), add descriptions to every rule, and ensure no rule uses `source: '*'` for management ports (SSH/RDP).

---

#### INET-DNS-001 — No private DNS zones scaffolded for future private endpoint readiness

**Severity:** Minor

**File:** `infra/main.bicep`

No `Microsoft.Network/privateDnsZones` resources are defined. This is expected given there are no private endpoints today, but private DNS zones are a prerequisite for any future private endpoint implementation. Without them, DNS resolution for services will continue to resolve to public endpoints even after private endpoints are created.

**Recommendation:** When adding private endpoints, create and link the following private DNS zones to the VNet: `privatelink.blob.core.windows.net`, `privatelink.documents.azure.com`, `privatelink.search.windows.net`, `privatelink.openai.azure.com`, and `privatelink.vaultcore.azure.net`.

---

#### INET-AUTH-001 — Managed Identity RBAC used consistently across all services

**Severity:** Info

All compute resources (API App Service, Function App) use a User-Assigned Managed Identity for authentication to Storage, AI Search, OpenAI, and Cosmos DB. RBAC role assignments are defined in Bicep for all data services. This is the correct authentication posture and significantly reduces credential exposure risk even in the absence of network isolation.

---

#### INET-AUTH-002 — Azure AI Search local auth correctly disabled

**Severity:** Info

**File:** `infra/core/search/search-services.bicep`

The `search` module is called in `main.bicep` with `disableLocalAuth: true`, which is correctly propagated through `search-services.bicep` to the search resource. This prevents API key-based access to the search service, enforcing RBAC-only access to the search index.

---

## 9. Recommended Networking Improvements

| Finding | Recommended Action | Priority |
|---|---|---|
| INET-PVTEP-001 | Add `networkAcls { defaultAction: 'Deny', bypass: 'AzureServices' }` to Storage Account; set `allowSharedKeyAccess: false` | High |
| INET-PVTEP-002 | Add `publicNetworkAccess: 'Disabled'` and `ipRules` to Cosmos DB; fix hardcoded `disableLocalAuth: false` override | High |
| INET-PVTEP-003 | Change AI Search `publicNetworkAccess` default to `'disabled'`; add IP rules or private endpoint | High |
| INET-PVTEP-004 | Add `networkAcls { defaultAction: 'Deny' }` to OpenAI; allowlist App Service outbound IPs | High |
| INET-VNET-001 | Deploy VNet with integration subnet (delegated to `Microsoft.Web/serverFarms`) and private endpoint subnet | High |
| INET-VNET-004 | Upgrade App Service Plans from Basic (B2) to Standard (S2) or Premium (P1v3) — prerequisite for VNet integration | High |
| INET-VNET-002 | Add `virtualNetworkSubnetId` and `vnetRouteAllEnabled: true` to API App Service | Medium |
| INET-VNET-003 | Add `virtualNetworkSubnetId` and `vnetRouteAllEnabled: true` to loader Function App | Medium |
| INET-KV-001 | Implement Key Vault module with private endpoint and RBAC; populate `KeyVaultUri`; migrate secrets | Medium |
| INET-CORS-001 | Add explicit `corsRules: []` (or restricted origin rules) to the `blobServices` resource | Medium |
| INET-STOR-001 | Add `properties: { publicAccess: 'None' }` to `load`, `completed`, and `images` containers | Low |
| INET-NSG-001 | Create NSG module with least-privilege rules when introducing VNet subnets | Low |
| INET-DNS-001 | Scaffold private DNS zones and VNet links as part of private endpoint implementation | Low |

### Recommended Architecture Target State

```
Internet
    │
    ▼
[App Service / Function App]
    │
    └── VNet Integration (subnet delegated to Microsoft.Web/serverFarms)
              │
    [Private Endpoint Subnet]
              │
              ├── Private Endpoint ──► Azure Storage Account
              ├── Private Endpoint ──► Azure Cosmos DB
              ├── Private Endpoint ──► Azure AI Search
              ├── Private Endpoint ──► Azure OpenAI
              └── Private Endpoint ──► Azure Key Vault

Private DNS Zones (linked to VNet):
  privatelink.blob.core.windows.net
  privatelink.documents.azure.com
  privatelink.search.windows.net
  privatelink.openai.azure.com
  privatelink.vaultcore.azure.net

NSGs attached to all subnets with deny-all defaults and explicit allow rules.
```

---

*This review is based on static analysis of Bicep source files only. It does not reflect runtime configuration, Azure Policy enforcement, or network controls applied outside the IaC codebase (e.g., manually applied NSGs, Azure Firewall rules, or Front Door WAF policies). Generated by the Infra Networking Steward (`infra-networking-steward`) on 2026-03-22.*
