Skip to content
Draft
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
1 change: 1 addition & 0 deletions .devcontainer/local/devcontainer.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
},
"ghcr.io/devcontainers/features/azure-cli:1": {},
"ghcr.io/devcontainers/features/docker-outside-of-docker:1": {},
"ghcr.io/devcontainers/features/docker-in-docker:2": {},
"ghcr.io/devcontainers/features/github-cli:1": {},
"ghcr.io/jsburckhardt/devcontainer-features/uv:1": {},
"ghcr.io/devcontainers/features/node:1": {},
Expand Down
6 changes: 3 additions & 3 deletions .vscode/mcp.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
],
"servers": {
// GitHub MCP Server: https://github.com/github/github-mcp-server
"GitHub MCP Server": {
"GitHub-MCP-Server": {
"command": "docker",
"args": [
"run",
Expand All @@ -24,7 +24,7 @@
}
},
// Azure MCP Server https://learn.microsoft.com/azure/developer/azure-mcp-server
"Azure MCP Server": {
"Azure-MCP-Server": {
"command": "npx",
"args": [
"-y",
Expand All @@ -34,7 +34,7 @@
]
},
// Terraform MCP Server: https://github.com/hashicorp/terraform-mcp-server
"Terraform MCP Server": {
"Terraform-MCP-Server": {
"command": "docker",
"args": [
"run",
Expand Down
197 changes: 197 additions & 0 deletions guides/implement_rbac_for_foundry_models/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,197 @@
<!-- META
title: AI Foundry Independent Authorization with APIM and App Roles
description: Deployable CAIRA guide for model-level authorization using Azure AI Foundry, Microsoft Entra app registrations and roles, and APIM JWT policies.
author: CAIRA Team
ms.date: 04/07/2026
ms.topic: architecture
estimated_reading_time: 14
keywords:
- azure ai foundry
- apim
- microsoft entra
- app roles
- jwt
- terraform
-->

# AI Foundry Independent Authorization with APIM and App Roles

This guide provides a deployable reference architecture in Azure that enforces independent model authorization for Azure AI Foundry by combining:

- Microsoft Entra application registrations
- Application roles
- Azure API Management
- APIM JWT validation and route-level authorization policies

## What This Deploys

- Azure AI Foundry account and default project.
- Log Analytics and Application Insights for observability.
- API Management gateway for model routes.
- One API application registration with app roles derived from route rules.
- Client application registrations and role assignments.
- APIM per-operation JWT validation and model route enforcement.
- Optional private networking for Foundry model endpoints:
- A dedicated virtual network and subnet for private endpoints.
- Private DNS zones and VNet links for Cognitive Services, AI Services, and OpenAI private links.
- AI Foundry private endpoint path via the module's private networking support.

## Architecture Files

- [terraform/main.tf](terraform/main.tf)
- [terraform/providers.tf](terraform/providers.tf)
- [terraform/variables.tf](terraform/variables.tf)
- [terraform/outputs.tf](terraform/outputs.tf)
- [terraform/terraform.tfvars.example](terraform/terraform.tfvars.example)
- [scripts/deploy.sh](scripts/deploy.sh)
- [assets/architecture.mmd](assets/architecture.mmd)

## Prerequisites

1. Azure subscription with permissions for resource creation and role assignments.
1. Microsoft Entra permissions to create application registrations, service principals, and app role assignments.
1. Terraform `>= 1.13`.
1. Azure CLI authenticated to the target subscription.

## Configuration

### Core Variables

- `apim_publisher_email`: Required APIM publisher email.
- `model_authorization_rules`: Route-to-model and required role mappings.
- `client_applications`: Client app registrations and assigned roles.

### Optional Private Networking Variables

Use these variables in [terraform/terraform.tfvars](terraform/terraform.tfvars) to enable private networking for Foundry model endpoints.

| Variable | Type | Default | Description |
|---|---|---|---|
| `should_enable_foundry_private_networking` | `bool` | `false` | Enables/disables the optional VNet and private endpoint path for Foundry model traffic. |
| `private_network_address_space` | `list(string)` | `["172.16.0.0/16"]` | Address space for the optional virtual network created by this guide. |
| `private_endpoint_subnet_address_prefixes` | `list(string)` | `["172.16.0.0/24"]` | Address prefixes for the subnet that hosts Foundry private endpoints. |

When `should_enable_foundry_private_networking = false`, the guide creates the infrastructure with public networking.

When `should_enable_foundry_private_networking = true`, Terraform creates networking resources and passes the generated subnet ID to the Foundry module, which disables public access and configures private endpoint access for Foundry model endpoints.

## Deployment Steps

1. Open [terraform](terraform).
1. Copy `terraform.tfvars.example` to `terraform.tfvars`.
1. Set `apim_publisher_email` and update `model_authorization_rules` and `client_applications` as needed.
1. Optional: set `should_enable_foundry_private_networking = true` and adjust CIDR ranges if needed.
1. Run:

```bash
terraform init
terraform validate
terraform plan
terraform apply
```

Or run the helper script:

```bash
./scripts/deploy.sh
```

## Testing Process

Run tests from the Terraform directory:

```bash
cd terraform
terraform test
```

Run a single test file:

```bash
terraform test -filter=tests/acceptance.tftest.hcl
terraform test -filter=tests/integration.tftest.hcl
```

Validate the private networking toggle behavior:

- Disabled mode (`should_enable_foundry_private_networking = false`): tests are active and assert no private networking resources are planned.
- Enabled mode (`should_enable_foundry_private_networking = true`): plan-mode tests are currently commented out due to external module behavior.
- Disabled-mode tests also assert the `ai_foundry_endpoint` output declaration remains present to catch core output regressions.

### Known Test Limitation

Enabled-mode plan tests are commented out in [terraform/tests/acceptance.tftest.hcl](terraform/tests/acceptance.tftest.hcl) and [terraform/tests/integration.tftest.hcl](terraform/tests/integration.tftest.hcl).

Reason:

- In [modules/ai_foundry/private_networking.tf](../../modules/ai_foundry/private_networking.tf), private DNS `data` resources use `count = var.foundry_subnet_id != null ? 1 : 0`.
- In this guide, `foundry_subnet_id` is derived from a subnet created in the same plan, so the value is unknown at plan time for enabled mode.
- Terraform fails with `Invalid count argument` before enabled-mode assertions can run.

Current test behavior:

- `terraform test` passes for active disabled-mode runs.
- Enabled-mode runs remain documented but commented out until the external module is refactored for plan-time determinism.

## Authorization Flow

1. A client app requests a token for `api://<api_application_client_id>`.
1. The client calls an APIM route such as `/gpt4o-mini/chat/completions`.
1. APIM validates JWT issuer, audience, and role claim.
1. APIM rewrites the route to the mapped Foundry deployment endpoint.
1. APIM authenticates to Foundry using its managed identity.

## Private Networking Notes

- This guide applies private networking only to Foundry model endpoints.
- API Management is still the ingress gateway for model routes.
- Ensure APIM can reach the Foundry private endpoint path in your environment when private networking is enabled.
- If deploying to an existing resource group, verify the resource group location and naming compatibility with your networking standards.

## Token Request Example

```bash
TENANT_ID="<tenant-id>"
CLIENT_ID="<client-app-client-id>"
CLIENT_SECRET="<client-app-secret>"
AUDIENCE="api://<api-application-client-id>"

curl -s -X POST "https://login.microsoftonline.com/${TENANT_ID}/oauth2/v2.0/token" \
-H "Content-Type: application/x-www-form-urlencoded" \
-d "client_id=${CLIENT_ID}" \
-d "client_secret=${CLIENT_SECRET}" \
-d "scope=${AUDIENCE}/.default" \
-d "grant_type=client_credentials"
```

## APIM Call Example

```bash
APIM_MODELS_BASE_URL="https://<apim-name>.azure-api.net/models"
ACCESS_TOKEN="<token>"

curl -s -X POST "${APIM_MODELS_BASE_URL}/gpt4o-mini/chat/completions" \
-H "Authorization: Bearer ${ACCESS_TOKEN}" \
-H "Content-Type: application/json" \
-d '{"messages":[{"role":"user","content":"Hello"}]}'
```

## Notes on AVM Usage

This guide uses the following modules:

- `Azure/avm-res-insights-component/azurerm`
- `Azure/naming/azurerm`
- `modules/ai_foundry`
- `modules/ai_foundry_project`
- `modules/common_models`

Azure Verified Modules catalog reference:

- <https://azure.github.io/Azure-Verified-Modules/indexes/terraform/tf-resource-modules/>

## Cleanup

```bash
terraform destroy
```
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
flowchart LR
C1[Client App A<br/>role: model.gpt4o-mini.invoke] --> APIM
C2[Client App B<br/>role: model.gpt5-nano.invoke] --> APIM
Entra[Microsoft Entra ID<br/>app registrations + app roles] --> APIM

APIM[Azure API Management<br/>validate-jwt + role check] --> Foundry[Azure AI Foundry]
APIM --> ModelA[gpt4o-mini deployment]
APIM --> ModelB[gpt5-nano deployment]
33 changes: 33 additions & 0 deletions guides/implement_rbac_for_foundry_models/scripts/deploy.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
#!/usr/bin/env bash
# ---------------------------------------------------------------------
# Copyright (c) Microsoft Corporation. Licensed under the MIT license.
# ---------------------------------------------------------------------

set -euo pipefail

SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
TERRAFORM_DIR="${SCRIPT_DIR}/../terraform"

if [[ ! -f "${TERRAFORM_DIR}/terraform.tfvars" ]]; then
echo "terraform.tfvars not found in ${TERRAFORM_DIR}."
echo "Copy terraform.tfvars.example to terraform.tfvars and update values."
exit 1
fi

cd "${TERRAFORM_DIR}"

echo "Initializing Terraform..."
terraform init

echo "Validating Terraform..."
terraform validate

echo "Planning Terraform deployment..."
terraform plan -out=tfplan

echo "Applying Terraform deployment..."
terraform apply tfplan

echo "Deployment complete."
echo "API audience client id: $(terraform output -raw api_application_client_id)"
echo "APIM models base URL: $(terraform output -raw apim_models_base_url)"
Loading