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

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

| Field | Value |
|---|---|
| Project | Azure-AI-RAG-CSharp-Semantic-Kernel-Functions |
| Run Date | 2026-03-22 |
| Steward | Infra Security Steward (`infra-security-steward`) |
| Critical | 4 |
| Notable | 9 |
| Minor | 3 |
| Info | 3 |
| Total | 19 |

---

## 1. Infrastructure Security Overview

The infrastructure provisions a full Azure AI RAG stack: an App Service (API, React SPA), Azure Functions (loader), Azure Cosmos DB for NoSQL, Azure AI Search, Azure OpenAI (Cognitive Services), Azure Blob Storage, and a monitoring stack (Log Analytics, Application Insights). All resources reside in a single resource group, and a shared user-assigned managed identity is the primary authentication mechanism for compute-to-service access.

**Overall security posture: Poor.** Multiple critical issues are present: the storage account has public network access explicitly enabled with no blob public access control, App Service and Function App resources are missing HTTPS-only enforcement, and no Key Vault is provisioned despite a placeholder file existing and app settings referencing `KeyVaultUri`. Azure OpenAI, AI Search, Cosmos DB, and Storage all accept public internet traffic with no IP restrictions or network rules. Diagnostic settings cover only two of eight resource types.

Positive notes: managed identity is used consistently across compute resources (OpenAI, Search, Storage, Function App blob trigger), RBAC is assigned at individual resource scope rather than subscription-wide, and App Service and Function App forward diagnostic logs to Log Analytics.

---

## 2. Public Access Assessment

| Resource | File | Issue |
|---|---|---|
| Storage Account | `infra/core/storage/blob-storage-account.bicep` | `publicNetworkAccess: 'Enabled'` — no network restrictions |
| Storage Account | `infra/core/storage/blob-storage-account.bicep` | `allowBlobPublicAccess` not set — defaults to enabled on older API |
| Storage containers (load, completed, images) | `infra/core/storage/blob-storage-account.bicep` | No `publicAccess: 'None'` on three of four containers |
| Azure OpenAI | `infra/core/ai/openai/openai-account.bicep` | `publicNetworkAccess: 'Enabled'` — no network restrictions |
| Azure AI Search | `infra/core/search/search-services.bicep` | Default `publicNetworkAccess: 'enabled'`; no IP rules enforced |
| API App Service | `infra/app/api-app.bicep` | `publicNetworkAccess: 'Enabled'` — HTTPS-only absent |
| React Web App | `infra/app/web-app.bicep` | `publicNetworkAccess: 'Enabled'` — HTTPS-only absent |
| Function App | `infra/app/loader-function.bicep` | No `publicNetworkAccess` or HTTPS-only setting |
| Cosmos DB | `infra/core/database/cosmos-db/account.bicep` | No `publicNetworkAccess` property — defaults to public |

The storage account is the most severe: public network access is explicitly `Enabled`, `allowBlobPublicAccess` is absent, and three of the four blob containers (`load`, `completed`, `images`) have no `publicAccess: 'None'` setting. Only the `archive` container explicitly sets `publicAccess: 'None'`.

---

## 3. Key Vault Security Assessment

The file `infra/core/security/key-vault.bicep` is empty (1 line). No Key Vault resource is provisioned anywhere in the infrastructure.

This is critical from a secrets management perspective:

- The API App Service configures a `CosmosDb_ConnectionString` app setting with an empty placeholder value. This connection string must be supplied at deploy time with no Key Vault reference.
- The Function App has a `KeyVaultUri` app setting with an empty value — indicating intent to use Key Vault, but no vault is provisioned.
- Without a Key Vault, secrets cannot be soft-deleted, purge-protected, or audited.
- No RBAC mode, soft-delete, purge protection, or network restriction findings can be raised because the resource does not exist. The absence itself is flagged as the primary finding.

---

## 4. Managed Identity Assessment

**Positive findings:**

- A single user-assigned managed identity is provisioned in `infra/core/security/managed-identity.bicep` and shared across all services.
- The Function App uses managed identity for `AzureWebJobsStorage` (credential: `managedidentity`), eliminating storage connection strings.
- The Function App blob trigger uses managed identity (`BlobTriggerConnection__credential: 'managedidentity'`).
- Azure OpenAI, AI Search, and Storage all receive RBAC role assignments scoped to the individual resource rather than the subscription.
- Both the API App Service and the Function App receive user-assigned identity.

**Issues:**

- The storage account has `allowSharedKeyAccess: true`, meaning storage account keys remain active alongside managed identity. This is unnecessary and increases the blast radius if keys are leaked.
- Cosmos DB: `disableKeyBasedAuth: false` is hardcoded in `infra/core/database/cosmos-db/nosql/account.bicep` line 25, overriding the caller's parameter. Key-based access remains enabled regardless of how the module is called.
- The React web app (`web-app.bicep`) has no managed identity assigned — acceptable for a static frontend, but noted as an observation.

---

## 5. RBAC Assignments Assessment

| Assignment | File | Role | Scope | Assessment |
|---|---|---|---|---|
| Storage Blob Data Contributor | `blob-storage-account.bicep` | `ba92f5b4` | storageAcct | Superseded by Blob Data Owner — redundant |
| Storage Account Contributor | `blob-storage-account.bicep` | `17d1049b` | storageAcct | Management-plane access; not needed for data access |
| Storage Blob Data Owner | `blob-storage-account.bicep` | `b7e6dc6d` | storageAcct | Overly broad; Owner supersedes Contributor |
| Storage Queue Data Contributor | `blob-storage-account.bicep` | `974c5e8b` | storageAcct | Appropriate |
| Storage Table Data Contributor | `blob-storage-account.bicep` | `0a9a7e1f` | storageAcct | Appropriate |
| Search Service Contributor | `search-services.bicep` | `7ca78c08` | search | Management-plane; beyond RAG data-plane needs |
| Search Index Data Contributor | `search-services.bicep` | `8ebe5a00` | search | Appropriate |
| Search Index Data Reader | `search-services.bicep` | `1407120a` | search | Appropriate |
| Cognitive Services OpenAI Contributor | `openai-account.bicep` | `a001fd3d` | openAiService | Appropriate |

**Issues:**

- Three storage roles are assigned simultaneously: Storage Blob Data Contributor, Storage Blob Data Owner, and Storage Account Contributor. Storage Blob Data Owner fully supersedes Storage Blob Data Contributor. Storage Account Contributor is a management-plane role (ARM) granting key listing and rotation — unnecessary when managed identity handles all data access.
- Search Service Contributor grants management-plane access (index creation/deletion, service configuration). If the identity only needs to ingest and query data, `Search Index Data Contributor` and `Search Index Data Reader` are sufficient.
- All role assignments use the deprecated API version `2020-04-01-preview`. The current stable API version is `2022-04-01`.

---

## 6. TLS and Encryption Assessment

| Resource | Property | Status |
|---|---|---|
| Storage Account | `minimumTlsVersion` | Not set — risk of TLS 1.0 default |
| Storage Account | `supportsHttpsTrafficOnly` | Not set — ARM defaults to `true`, but not explicit |
| Storage Account | `allowSharedKeyAccess` | `true` — shared key auth enabled |
| API App Service | `httpsOnly` | Not set — defaults to `false` |
| React Web App | `httpsOnly` | Not set — defaults to `false` |
| Function App | `httpsOnly` | Not set — defaults to `false` |
| Azure AI Search | TLS | Platform-managed |
| Cosmos DB | Encryption at rest | Platform default (enabled) |
| Azure OpenAI | TLS | Platform-managed |

None of the App Service or Function App resources set `httpsOnly: true` at the `properties` level. Without this, Azure App Service will accept HTTP requests alongside HTTPS. The storage account does not declare `minimumTlsVersion`, which for some older API configurations can default to `TLS1_0`.

---

## 7. Diagnostic Settings Assessment

| Resource | Diagnostic Settings | Assessment |
|---|---|---|
| API App Service | Configured — AppServiceHTTPLogs, AppServiceConsoleLogs, AppServiceAppLogs, AllMetrics | Good |
| Function App (loader) | Configured — FunctionAppLogs, AllMetrics | Good |
| React Web App | Not configured | Missing |
| Storage Account | Not configured | Missing |
| Azure AI Search | Not configured | Missing |
| Azure OpenAI | Not configured | Missing |
| Cosmos DB | Not configured | Missing |
| Key Vault | N/A — not provisioned | Missing (see §3) |

Five resources have no diagnostic settings. Storage, AI Search, OpenAI, Cosmos DB, and the React Web App all process or serve sensitive data and should forward audit and diagnostic logs to the Log Analytics workspace that is already provisioned.

---

## 8. Findings

| Severity | ID | Title | File |
|---|---|---|---|
| 🔴 Critical | ISEC-PUBLIC-001 | Storage account has public network access explicitly enabled | `infra/core/storage/blob-storage-account.bicep` |
| 🔴 Critical | ISEC-PUBLIC-002 | Storage account missing `allowBlobPublicAccess: false`; three containers lack `publicAccess: 'None'` | `infra/core/storage/blob-storage-account.bicep` |
| 🔴 Critical | ISEC-HTTPS-001 | App Services and Function App missing `httpsOnly: true` | `infra/app/api-app.bicep`, `infra/app/web-app.bicep`, `infra/app/loader-function.bicep` |
| 🔴 Critical | ISEC-KV-001 | No Key Vault provisioned; secrets have no managed secret store | `infra/core/security/key-vault.bicep` |
| 🟡 Notable | ISEC-PUBLIC-003 | Azure OpenAI has public network access enabled with no IP restrictions | `infra/core/ai/openai/openai-account.bicep` |
| 🟡 Notable | ISEC-PUBLIC-004 | Azure AI Search public network access defaults to `enabled` with no IP rules | `infra/core/search/search-services.bicep` |
| 🟡 Notable | ISEC-PUBLIC-005 | Cosmos DB has no `publicNetworkAccess` restriction configured | `infra/core/database/cosmos-db/account.bicep` |
| 🟡 Notable | ISEC-AUTH-001 | Cosmos DB key-based authentication not disabled (`disableKeyBasedAuth: false` hardcoded in nosql wrapper) | `infra/core/database/cosmos-db/nosql/account.bicep` |
| 🟡 Notable | ISEC-RBAC-001 | Storage account has redundant and over-permissioned RBAC roles (Blob Data Owner + Blob Data Contributor + Account Contributor) | `infra/core/storage/blob-storage-account.bicep` |
| 🟡 Notable | ISEC-RBAC-002 | Search Service Contributor grants management-plane access beyond what RAG operations require | `infra/core/search/search-services.bicep` |
| 🟡 Notable | ISEC-DIAG-001 | Storage, AI Search, OpenAI, Cosmos DB, and React web app have no diagnostic settings | Multiple files |
| 🟡 Notable | ISEC-STORAGE-001 | Storage account has `allowSharedKeyAccess: true` — shared keys enabled alongside managed identity | `infra/core/storage/blob-storage-account.bicep` |
| 🟡 Notable | ISEC-COSMOSDB-001 | Cosmos DB managed identity role assignment module (`data-plane-role-assignment.bicep`) is never called from main.bicep or database.bicep | `infra/main.bicep`, `infra/app/database.bicep` |
| 🟢 Minor | ISEC-TLS-001 | Storage account missing explicit `minimumTlsVersion: 'TLS1_2'` | `infra/core/storage/blob-storage-account.bicep` |
| 🟢 Minor | ISEC-TLS-002 | Storage account missing explicit `supportsHttpsTrafficOnly: true` | `infra/core/storage/blob-storage-account.bicep` |
| 🟢 Minor | ISEC-RBAC-003 | Role assignments use deprecated API version `2020-04-01-preview` | Multiple files |
| ℹ️ Info | ISEC-MI-001 | User-assigned managed identity used consistently across all compute resources | `infra/core/security/managed-identity.bicep` |
| ℹ️ Info | ISEC-MI-002 | Function App uses managed identity for AzureWebJobsStorage and BlobTriggerConnection — no storage connection strings in config | `infra/app/loader-function.bicep` |
| ℹ️ Info | ISEC-DIAG-002 | API App Service and Function App have comprehensive diagnostic settings forwarding to Log Analytics | `infra/app/api-app.bicep`, `infra/app/loader-function.bicep` |

---

## 9. Recommended Security Hardening

| Finding | Recommended Action | Priority |
|---|---|---|
| ISEC-PUBLIC-001 | Set `publicNetworkAccess: 'Disabled'` on the storage account; use private endpoints or VNet service endpoints if compute requires direct access | Critical |
| ISEC-PUBLIC-002 | Add `allowBlobPublicAccess: false` to storage account properties; add `properties: { publicAccess: 'None' }` to `load`, `completed`, and `images` containers | Critical |
| ISEC-HTTPS-001 | Add `httpsOnly: true` to the `properties` block of all three `Microsoft.Web/sites` resources (API app, web app, function app) | Critical |
| ISEC-KV-001 | Implement `key-vault.bicep` with `enableSoftDelete: true`, `enablePurgeProtection: true`, `enableRbacAuthorization: true`, and network restrictions; replace the `CosmosDb_ConnectionString` plain-text setting with a Key Vault reference | Critical |
| ISEC-PUBLIC-003 | Set `publicNetworkAccess: 'Disabled'` on Azure OpenAI; use private endpoint or restrict via `networkAcls` to known VNet/IP ranges | Notable |
| ISEC-PUBLIC-004 | Set `publicNetworkAccess: 'disabled'` on AI Search; configure `networkRuleSet.ipRules` or private endpoint if external ingestion is needed | Notable |
| ISEC-PUBLIC-005 | Add `publicNetworkAccess: 'Disabled'` to the Cosmos DB account properties; use a private endpoint for application access | Notable |
| ISEC-AUTH-001 | In `infra/core/database/cosmos-db/nosql/account.bicep` line 25, replace `disableKeyBasedAuth: false` with `disableKeyBasedAuth: disableKeyBasedAuth` to honour the caller's parameter; set the default to `true` | Notable |
| ISEC-RBAC-001 | Remove `Storage Blob Data Contributor` (superseded by Owner) and `Storage Account Contributor` (management plane unnecessary for data access) | Notable |
| ISEC-RBAC-002 | Remove `Search Service Contributor`; retain only `Search Index Data Contributor` and `Search Index Data Reader` for the RAG workload | Notable |
| ISEC-DIAG-001 | Add `Microsoft.Insights/diagnosticSettings` resources for Storage (blob service), AI Search, Azure OpenAI, Cosmos DB, and the React web app, all forwarding to the existing Log Analytics workspace | Notable |
| ISEC-STORAGE-001 | Set `allowSharedKeyAccess: false` on the storage account to disable key-based auth — all consumers already use managed identity | Notable |
| ISEC-COSMOSDB-001 | Wire up `data-plane-role-assignment.bicep` from `database.bicep` or `main.bicep` to assign the Cosmos DB SQL role to the managed identity; replace the empty `CosmosDb_ConnectionString` app setting with managed identity-based access | Notable |
| ISEC-TLS-001 | Add `minimumTlsVersion: 'TLS1_2'` to storage account properties | Minor |
| ISEC-TLS-002 | Add `supportsHttpsTrafficOnly: true` to storage account properties | Minor |
| ISEC-RBAC-003 | Update all `Microsoft.Authorization/roleAssignments` to use API version `2022-04-01` instead of the deprecated `2020-04-01-preview` | Minor |

---

## 10. Cross-Cutting Observations

- The `CosmosDb_ConnectionString` app setting in `api-app.bicep` is an empty string placeholder. No Key Vault is in place, so credentials must be injected post-deployment via pipeline or manual steps — a credentials-management gap.
- The `KeyVaultUri` app setting in `loader-function.bicep` is also empty, confirming the team anticipated Key Vault integration but did not finish provisioning it.
- The React web app has no managed identity, no HTTPS-only enforcement, no diagnostic settings, and no authentication configuration in Bicep. While it serves a static SPA, it is still a public-facing entry point with zero security hardening in IaC.
- Cosmos DB is deployed with a hardcoded `location: 'centralus'` in `main.bicep` (line 83), while all other resources use the parameterized `location`. This may cause data residency mismatches in non-Central US deployments.

---

*This review is based on static analysis of Bicep source files as of 2026-03-22. It does not reflect runtime configuration, Azure Policy enforcement, pipeline-injected values, or post-deployment manual changes. Generated by the Infra Security Steward (`infra-security-steward`).*
