Skip to content

Citadel Access Contracts - Use-case onboarding

Overview

Automate the onboarding of AI use cases to your APIM-based AI Gateway with a streamlined, infrastructure-as-code approach using Bicep parameter files (.bicepparam).

This package eliminates manual APIM configuration by providing:

  • 📦 Automated Product Creation: Per-service APIM products with naming <serviceCode>-<BU>-<UseCase>-<ENV>
  • 🧩 Mixed asset types: a single product can grant LLM + Tools (MCP) + Agents (A2A) together (prefix MULTI-), with policies applied conditionally per asset type
  • 🔌 API Integration: Automatic API attachment to product with custom or default policies
  • 🔑 Subscription Management: Auto-generated subscription with secure API keys
  • 🔐 Flexible Secret Storage: Optional Azure Key Vault integration or direct credential output
  • 🤖 Azure Microsoft Foundry Integration: Optional APIM connection creation for Foundry agents
  • 📝 Declarative Configuration: Simple .bicepparam & .xml files for version control per use case
  • 🔐 JWT Authentication: Optional layered API Key + JWT Bearer token authentication per product

What Gets Created

Resource Naming Pattern Description
APIM Product {code}-{BU}-{UseCase}-{ENV} Product per service (e.g., LLM-Healthcare-PatientAssistant-DEV). code is the asset-type prefix: LLM / TOOL / AGENT for a single type, or MULTI when one product grants more than one type (e.g. MULTI-HR-ChatAgent-DEV)
APIM Subscription {product}-SUB-01 Subscription with a single shared API key
Key Vault Secrets {secretName} One shared key secret + one endpoint secret per asset (optional)
Foundry Connection {prefix}-{code} APIM connection for Microsoft Foundry agents, wired to the LLM endpoint (optional)

Key Features

✨ Simplified Parameters: No need for full resource IDs - just API names
🔄 Optional Key Vault: Choose between Key Vault storage or direct output
🤖 Optional Foundry Integration: Create APIM connections for AI agents
📋 Policy Templates: Pre-built policies for common use cases
🎯 Multi-Service Support: Onboard multiple AI services in one deployment
🔒 Secure by Default: Credentials stored in Key Vault or marked as secrets
📊 Production Ready: Designed for scale and aligned with DevOps practices


Business Continuity & Resiliency (optional)

A single access contract can optionally be mirrored across additional APIM gateways, Key Vaults, and Foundry instances so it keeps working if the primary instance or region becomes unavailable. The subscription key generated by the primary gateway is reused across all gateways, so client applications use the same api-key regardless of which gateway serves the request (ideal behind a global load balancer such as Azure Front Door or Traffic Manager).

  • 🔁 Same key everywhere: primary gateway generates the key; additional gateways reuse it verbatim.
  • 🌐 Optional global endpoint: point Key Vault/Foundry at a global load balancer URL (like Azure Traffic Manager endpoint).
  • 🧩 Opt-in per instance: add secondary Key Vaults / Foundry projects with configurable endpoint source.
  • 📦 Staged deployment: primary first → additional gateways → Key Vaults & Foundry.

✅ Fully backward compatible — all of these are optional parameters that default to empty. Existing access contracts continue to deploy and behave exactly as before with no changes.

👉 See the Business Continuity & Resiliency Guide for configuration, examples, deployment order, key rotation, and troubleshooting.


Governing Tools (MCP) & Agents (A2A) — mixed asset contracts

Assets published through the Publish Contract (Tools/MCP and Agents/A2A) are governed by an access contract exactly like LLM APIs. A single product can grant access to any mix of the three asset types; the product policy classifies each request and applies the right controls.

  • Product prefix: LLM / TOOL / AGENT for a single asset type; MULTI when a contract grants two or more types (e.g. MULTI-HR-ChatAgent-DEV). The code field on the services entry drives this — no bicep change needed.
  • apiNameMapping[code] lists the union of granted APIs (LLM inference APIs + published Tool/Agent APIs).
  • Conditional policy: TOOL / AGENT / MULTI products default to policies/default-multi-product-policy.xml, which includes the set-asset-kind fragment and a <choose> that applies:
    • LLM → model RBAC (validate-model-access) + llm-token-limit (TPM + quota)
    • Tool / Agent → request-based rate-limit-by-key (calls/min) + quota-by-key (call quota)
    • Common (any type) → opt-in llm-content-safety, alerting, etc.
  • Asset-kind detection: APIM does not expose the API type to policy, so the product policy sets contractToolApis / contractAgentApis (comma-separated API resource names) before including set-asset-kind; anything unlisted defaults to llm.
  • One shared key, one endpoint per asset: the contract mints a single subscription key shared by every granted asset, and Key Vault publishes that key once plus one endpoint secret per asset (via assetEndpoints or publishAllAssetEndpoints). See Shared key, one endpoint secret per asset.
  • Foundry connects to the LLM endpoint of the contract (selected via foundryApiName); pair it with Key Vault so the Tool/Agent endpoints are also available to the contract owner.

✅ Backward compatible — LLM contracts keep the existing LLM product policy untouched. See Access contracts policy snippets for the policy snippets.


Zero-downtime Key Rotation (optional)

Rotate a use case's APIM subscription key without any service interruption using a simple, deliberate 3-step flow — across the primary gateway and every mirrored gateway, Key Vault, and Foundry connection.

Each subscription has two keys (primary + secondary), both always valid. The contract hands consumers one active key and rotates the other:

  • 🔀 Switch (usePrimaryKey=false): push the secondary key everywhere while the primary stays valid — no downtime.
  • ✅ Confirm: use-case owners verify their apps are healthy on the new key.
  • ♻️ Rotate (usePrimaryKey=false + keyRotationEnabled=true): regenerate the now non-active key, invalidating the old value.

✅ Fully backward compatible — defaults (usePrimaryKey=true, keyRotationEnabled=false) reproduce existing behavior exactly. ARM has no native "regenerate" action, so the non-active key is regenerated declaratively by assigning a fresh value, which also keeps keys in sync across all gateways.

👉 See the Key Rotation Guide for the full step-by-step procedure, secondary-key rotation, multi-gateway behavior, verification, and troubleshooting.


Deployment quick reference

For a condensed cheat-sheet of the folder conventions and deployment command, see the Quick Reference Guide.

below are the high-level steps to deploy the use case onboarding Bicep package before diving into the detailed documentation:

  1. Create a folder dedicated for use-cases contracts: a folder like contracts in the citadel-access-contracts module to hold your use case specific files. The shipped base-contracts/common folder holds the base default contract with shared Governance Hub coordinates (subscription, APIM name, and resource group) that you can customize and reuse.
  2. Create a use-case contract folder: under the contracts folder, create a new folder for your use case following the pattern <business-unit>/<use-case> (e.g. sales/assistant, hr/chat-agent).
  3. Create an environment subfolder: under the use case folder, create a subfolder for each environment (e.g. dev, test, prod).
  4. Prepare Parameter File: Create new use case .bicepparam file (you can use main.bicepparam as a base) under the environment folder.
  5. Create/Customize APIM Policy: Use default or create a custom XML policy. For simplicity, policy file can be named ai-product-policy.xml and placed in the same environment folder.
  6. Deploy template with the prepared parameter file:
    # This can be executed in CLI or through a DevOps pipeline
    az deployment sub create --name <use-case-contract-name> --location <location> --template-file main.bicep --parameters contracts/<business-unit>/<use-case>/<environment>/main.bicepparam
    

NOTE: Ensure that you are updating values according to your environment and folder structure.

Architecture Overview

Deployment Flow

flowchart TB
    subgraph Input["📥 Inputs"]
        P1[bicepparam file]
        P5[Policy XML files]
    end

    subgraph Deploy["🚀 Deployment"]
        D1[main.bicep]
        D2[Create Products]
        D3[Attach APIs to Products]
        D4[Apply Policies]
        D5[Create Subscriptions]
        D6{Use Key Vault?}
        D7[Store Secrets in KV]
        D8[Output Credentials]
        D9{Use Foundry?}
        D10[Create Foundry Connections]
    end

    subgraph Output["📤 Outputs"]
        O1[Products Created]
        O2[Subscription Keys]
        O3[KV Secret Names]
        O4[Direct Credentials]
        O5[Foundry Connections]
    end

    Input --> Deploy
    D1 --> D2 --> D3 --> D4 --> D5 --> D6
    D6 -->|Yes| D7 --> O3
    D6 -->|No| D8 --> O4
    D5 --> D9
    D9 -->|Yes| D10 --> O5
    D9 -->|No| O2
    D2 --> O1
    D5 --> O2

Runtime Request Flow

Below is a suggested flow for client applications (i.e. agents) interacting with the onboarded services via the Citadel Access Contracts:

sequenceDiagram
    participant App as AI Agent/App
    participant Entra as Microsoft Entra ID
    participant KV as Azure Key Vault
    participant Foundry as Microsoft Foundry
    participant APIM as AI Gateway
    participant AI as AI Services

    alt Using Key Vault
        App->>KV: Get endpoint + API key
        KV-->>App: Return secrets
    else Using Foundry Connection
        App->>Foundry: Use APIM connection
        Foundry->>Foundry: Get stored credentials
    else Direct Credentials
        Note over App: Use credentials from deployment output
    end

    opt JWT Authentication Enabled
        App->>Entra: Request JWT token (client credentials)
        Entra-->>App: Return Bearer token
    end

    App->>APIM: HTTPS request with api-key + optional Bearer token
    APIM->>APIM: Validate subscription key (always required)
    APIM->>APIM: Apply product policy (check jwtRequired)
    opt JWT Required by Product
        APIM->>APIM: Validate JWT via security-handler fragment
    end
    APIM->>AI: Forward to backend service
    AI-->>APIM: Response
    APIM->>APIM: Logs & metrics
    APIM-->>App: Response with usage headers

Repository Structure

This is a submodule focused on Citadel Access Contracts. The folder structure is as follows:

citadel-access-contracts/
├── main.bicep                          # Main orchestration template
├── main.bicepparam                     # Base parameter file
├── modules/
│   ├── apimOnboardService.bicep        # Product + subscription creation
│   ├── apimProduct.bicep               # APIM product module
│   ├── apimSubscription.bicep          # Subscription module
│   ├── kvSecrets.bicep                 # Key Vault secret storage
│   └── foundryConnection.bicep         # Azure Microsoft Foundry connection module
├── policies/
│   └── default-ai-product-policy.xml   # Default product policy
├── base-contracts/                     # Base default contracts
│   └── common/                         # Governance Hub coordinates (reusable base)
│       ├── main.bicepparam
│       └── ai-product-policy.xml
├── contracts/                          # Use-case contracts folder for source control 
│   └── <business-unit>/                # Business unit folder (e.g., sales)
│       └── <use-case>/                 # Use case folder (e.g., assistant)
│           ├── dev/                    # Environment subfolder
│           │   ├── main.bicepparam     # Use case specific parameters
│           │   └── ai-product-policy.xml   # Custom policy file
│           ├── test/                   # Test environment
│           │   ├── main.bicepparam
│           │   └── ai-product-policy.xml
│           └── prod/                   # Production environment
│               ├── main.bicepparam
│               └── ai-product-policy.xml

Parameter File Reference

Main Parameters (main.bicepparam)

Parameter Type Required Description Example
apim object ✅ APIM instance coordinates { subscriptionId, resourceGroupName, name }
keyVault object ✅* Key Vault for secrets (*required even if not used) { subscriptionId, resourceGroupName, name }
useTargetAzureKeyVault bool ❌ Store secrets in Key Vault (default: false) true or false
useCase object ✅ Use case naming context { businessUnit, useCaseName, environment }
apiNameMapping object ✅ Map service codes to API names { OAI: ["azure-openai-service-api"], ... }
services array ✅ Services to onboard See Services Schema below
productTerms string ❌ Product terms of service "By using this product..."

JWT Authentication: JWT validation is configured per access contract via the policies.jwtAuth.enabled field in the Agent Access Contract Request JSON (see Base Access Contract Request). When enabled, the product policy sets jwtRequired=true, and the security-handler fragment enforces JWT Bearer token validation in addition to the API key.

| useTargetFoundry | bool | ❌ | Create Foundry connections (default: false) | true or false | | foundry | object | ❌* | Microsoft Foundry coordinates (*required if useTargetFoundry=true) | { subscriptionId, resourceGroupName, accountName, projectName } | | foundryConfig | object | ❌ | Foundry connection configuration | See Foundry Config below | | globalGatewayUrl | string | ❌ | Global endpoint (e.g. Front Door) used for all KV/Foundry endpoints (default: '' = primary gateway). See Resiliency Guide | https://ai-gateway.contoso.com | | additionalApimGateways | array | ❌ | Additional APIM gateways to mirror the contract into, reusing the primary key (default: []) | [{ subscriptionId, resourceGroupName, name }] | | additionalKeyVaults | array | ❌ | Additional Key Vaults for endpoint + shared key, with endpointSource selector (default: []) | [{ subscriptionId, resourceGroupName, name, endpointSource }] | | additionalFoundries | array | ❌ | Additional Foundry instances for APIM connections, with endpointSource selector (default: []) | [{ subscriptionId, resourceGroupName, accountName, projectName, endpointSource }] |

Key Rotation: Zero-downtime subscription key rotation is controlled by the optional usePrimaryKey (default true) and keyRotationEnabled (default false) parameters. Defaults preserve existing behavior. See the Key Rotation Guide.

| usePrimaryKey | bool | ❌ | Which subscription key is active (handed to consumers). true = primary (default), false = secondary. See Key Rotation Guide | true or false | | keyRotationEnabled | bool | ❌ | Regenerate the non-active key, invalidating its old value (default: false) | true or false | | rotationKeyOverride | string | ❌ | Explicit rotation key value for deterministic re-runs (default: '' = auto-generate) | <64-char key> |

Service Code mapping

Map service codes (which is a short acronym that represents the category of services fall under) to their API-id in APIM:

{
  LLM: ["azure-openai-api", "universal-llm-api"]
  OAIRT: ["openai-realtime-ws-api"]
  DOC: ["document-intelligence-api", "document-intelligence-api-legacy"]
  SRCH: ["azure-ai-search-index-api"]
  // ... add more services
}

Note: API-id must already exist in your APIM instance. The deployment will fail if an API name is not found.

Above is the list of supported APIs that are provisioned by default by the accelerator.

Adding custom APIs

You can onboard any number of other APIs as well to support your custom services. Add the newly added API-ids to the mapping above accordingly.

Multi-service Bundles

Mapping currently is suggested to focus on a specific category of services (e.g., LLM, Document Intelligence, etc.)

You can create unique mappings that mix different service types under one bundle if needed (this will require the product policy to be aware of that mix to apply the correct policies based on the service type like using tokens-per-mint limits for LLM and request-per-min limits for Document Intelligence).

Use-case service assignment schema

Each service in the services array:

{
  code: string               // Service code (e.g., "LLM", "DOC", "SRCH", "MULTI")
  apiKeySecretName: string   // Name for the single shared API key secret in Key Vault
  endpointSecretName: string // Optional: legacy single endpoint secret (first granted API path)
  policyXml: string          // Optional: Custom policy XML (empty = use default)

  // --- Multi-asset endpoint publishing (optional, backward compatible) ---
  assetEndpoints: array      // Optional: one endpoint secret per asset
                             //   [{ apiName: 'weather-tool', endpointSecretName?: '' }]
                             //   empty endpointSecretName auto-generates a name
  publishAllAssetEndpoints: bool  // Optional: auto-publish an endpoint secret for EVERY
                                  //   API in apiNameMapping[code] (default false)
  foundryApiName: string     // Optional: which granted API is the LLM endpoint for the
                             //   Foundry connection (defaults to first known LLM API)
}

This creates one APIM product + one subscription + one shared API key per service code, and applies the referenced policy XML for that product.

The services array lets you assign multiple independent service codes to a use case (for example granting both LLM and DOC access). Each service code gets its own product + subscription + key (LLM has a different key from Document Intelligence).

Shared key, one endpoint secret per asset

A single product (especially a MULTI- contract granting LLM + Tools/MCP + Agents/A2A) shares one subscription key across every granted asset. Because each asset has its own gateway path, Key Vault publishes:

  • one key secret — apiKeySecretName (shared by all assets in the contract), and
  • one endpoint secret per asset — configured via assetEndpoints or publishAllAssetEndpoints.
Field Effect
endpointSecretName (legacy) Publishes a single endpoint secret for the first granted API. Still honored for backward compatibility; additive when combined with the fields below.
assetEndpoints Publishes one endpoint secret per listed asset. Supply endpointSecretName per entry, or leave it empty to auto-generate <code>-<businessUnit>-<useCase>-<env>-<apiName>-endpoint.
publishAllAssetEndpoints: true Publishes an endpoint secret for every API in apiNameMapping[code] (names auto-generated, or overridden by a matching assetEndpoints entry).
foundryApiName Selects which granted API is the LLM inference endpoint for the Foundry connection (Foundry supports the LLM endpoint only).

✅ Backward compatible — a service with only code + apiKeySecretName + endpointSecretName (no new fields) publishes exactly the same two secrets as before.


What gets created

Component Scope Naming Notes
APIM Product APIM <serviceCode>-<BU>-<UseCase>-<ENV> One per service code you include
APIM Subscription APIM <product>-SUB-01 Single shared key captured into Key Vault
Key Vault key secret KV apiKeySecretName One shared key per contract
Key Vault endpoint secret(s) KV endpointSecretName / assetEndpoints[].endpointSecretName / auto-generated One per asset for multi-asset contracts
Foundry Connection Microsoft Foundry <prefix>-<serviceCode> One connection per service (LLM endpoint), if enabled

Naming examples

  • Product: LLM-Retail-FinancialAssistant-DEV
  • Subscription: LLM-Retail-FinancialAssistant-DEV-SUB-01
  • Foundry Connection: Retail-FinancialAssistant-DEV-LLM
  • Auto-generated endpoint secret: multi-hr-chatagent-dev-weather-tool-endpoint

Prerequisites

Azure Resources

Resource Requirement How to Verify
Citadel Compliant APIM Instance with published APIs matching your apiNameMapping az apim api list -g <rg> -n <apim-name>
Azure Key Vault Accessible with secret set permissions (if using KV) az keyvault show -n <kv-name>
Azure Microsoft Foundry Account and project must exist (if using Foundry) az cognitiveservices account show -n <account-name> -g <rg>

Permissions Required

The deployment identity needs:

Scope Role Purpose
APIM Resource Group API Management Service Contributor Create products and subscriptions
Target Key Vault (if used) Key Vault Secrets Officer Write secrets
Microsoft Foundry Resource Group (if used) Contributor Create connections
Subscription Reader Reference existing resources

Quick Start Guide

Step 1: Create use case folder with environment subfolder

Create a folder structure following the pattern contracts/<business-unit>/<use-case>/<environment>/ with copies of both the main.bicepparam and default policy as a base.

# Create the folder structure: contracts/healthcare/chatbot/dev/
mkdir -p bicep/infra/citadel-access-contracts/contracts/healthcare/chatbot/dev
cd bicep/infra/citadel-access-contracts/contracts/healthcare/chatbot/dev

# Copy template files (note: path goes up 4 levels due to business-unit/use-case/env nesting)
cp ../../../../main.bicepparam main.bicepparam
cp ../../../../policies/default-ai-product-policy.xml ai-product-policy.xml

Step 2: Configure Your Parameters

# Edit main.bicepparam in the environment folder
code main.bicepparam

Update these values:

using '../../../../main.bicep'

param apim = {
  subscriptionId: 'YOUR-SUBSCRIPTION-ID'        // ← Update
  resourceGroupName: 'YOUR-APIM-RESOURCE-GROUP'  // ← Update
  name: 'YOUR-APIM-NAME'                        // ← Update
}

param keyVault = {
  subscriptionId: 'YOUR-SUBSCRIPTION-ID'        // ← Update
  resourceGroupName: 'YOUR-KV-RESOURCE-GROUP'   // ← Update
  name: 'YOUR-KV-NAME'                          // ← Update
}

param useTargetAzureKeyVault = true  // false to output credentials directly

param useCase = {
  businessUnit: 'YourDepartment'     // ← Update
  useCaseName: 'YourUseCaseName'     // ← Update
  environment: 'DEV'                 // DEV, TEST, PROD
}

// Verify these API names exist in your APIM
param apiNameMapping = {
  LLM: ['azure-openai-api', 'universal-llm-api']
  DOC: ['document-intelligence-api', 'document-intelligence-api-legacy']
}

param services = [
  {
    code: 'LLM'
    endpointSecretName: 'OPENAI-ENDPOINT'
    apiKeySecretName: 'OPENAI-API-KEY'
    policyXml: loadTextContent('ai-product-policy.xml')  // or '' for default
  }
  // Add more services as needed
]

Step 3: Validate Configuration

# Preview what will be created (run from the environment folder)
az deployment sub what-if `
  --location swedencentral `
  --template-file ../../../../main.bicep `
  --parameters main.bicepparam

Step 4: Deploy

# Deploy at subscription scope (run from the environment folder)
az deployment sub create `
  --name healthcare-chatbot-dev-onboarding `
  --location swedencentral `
  --template-file ../../../../main.bicep `
  --parameters main.bicepparam

# Or run from the citadel-access-contracts root folder:
az deployment sub create `
  --name healthcare-chatbot-dev-onboarding `
  --location swedencentral `
  --template-file main.bicep `
  --parameters contracts/healthcare/chatbot/dev/main.bicepparam

Step 5: Verify Deployment

# Check products created
az apim product list `
  --resource-group YOUR-APIM-RG `
  --service-name YOUR-APIM-NAME `
  --query "[?contains(name, 'REPLACE-PRODUCT-NAME')].{Name:name, State:state}"

# If using Key Vault, check secrets
az keyvault secret list `
  --vault-name YOUR-KV-NAME `
  --query "[?contains(name, 'openai')].name"

Step 6: Use the Service

from azure.identity import DefaultAzureCredential
from azure.keyvault.secrets import SecretClient

# Get credentials from Key Vault
credential = DefaultAzureCredential()
kv_client = SecretClient(
    vault_url="https://YOUR-KV-NAME.vault.azure.net/",
    credential=credential
)

endpoint = kv_client.get_secret("openai-endpoint").value
api_key = kv_client.get_secret("openai-api-key").value

# Use with your application
print(f"Endpoint: {endpoint}")
# api_key is ready to use as api-key header

Also you can use the citadel-access-contracts-tests notebook to validate end-to-end connectivity of the newly created access contract.


Secret Management Options

Access contract generates sensitive keys for each service onboarded. You have two options to manage these secrets:

When to use: Production deployments, applications with managed identities

param useTargetAzureKeyVault = true

param keyVault = {
  subscriptionId: 'YOUR-SUB-ID'
  resourceGroupName: 'YOUR-KV-RG'
  name: 'YOUR-KV-NAME'
}

Benefits:

  • ✅ Centralized secret management
  • ✅ Automatic rotation support
  • ✅ Access auditing
  • ✅ Integration with managed identities

Retrieval:

from azure.keyvault.secrets import SecretClient
kv_client = SecretClient(vault_url="https://<kv-name>.vault.azure.net/", credential=credential)
endpoint = kv_client.get_secret("llm-endpoint").value

Option 2: Direct Output (CI/CD)

When to use: CI/CD pipelines, serverless functions, non-Azure environments where these values needs to be stored in some other secret store

param useTargetAzureKeyVault = false

// keyVault still required but can use placeholders as values are not considered.
param keyVault = {
  subscriptionId: '00000000-0000-0000-0000-000000000000'
  resourceGroupName: 'placeholder'
  name: 'placeholder'
}

Benefits:

  • ✅ No Key Vault dependency
  • ✅ Direct credential access
  • ✅ Works in any environment

Retrieval from Deployment Output:

$output = az deployment sub show `
  --name my-deployment `
  --query properties.outputs.endpoints.value -o json | ConvertFrom-Json

$oaiEndpoint = ($output | Where-Object { $_.code -eq 'LLM' }).endpoint
$oaiKey = ($output | Where-Object { $_.code -eq 'LLM' }).apiKey

# Store in CI/CD variables (NOT RECOMMENDED. Just for demonstration)
Write-Host "##vso[task.setvariable variable=LLM_ENDPOINT;issecret=true]$oaiEndpoint"
Write-Host "##vso[task.setvariable variable=LLM_KEY;issecret=true]$oaiKey"

⚠️ Security Note: When using direct output, handle credentials as secrets in your CI/CD system.

When to use: Building AI agents in Microsoft Foundry that need APIM gateway access

param useTargetFoundry = true

param foundry = {
  subscriptionId: 'YOUR-FOUNDRY-SUB-ID'
  resourceGroupName: 'YOUR-FOUNDRY-RG'
  accountName: 'YOUR-FOUNDRY-ACCOUNT'
  projectName: 'YOUR-PROJECT-NAME'
}

param foundryConfig = {
  connectionNamePrefix: ''        // Empty = use useCase naming
  authType: 'ProjectManagedIdentity' // Default: MI JWT + api-key header. 'ApiKey' = key only
  managedIdentityAudience: 'https://cognitiveservices.azure.com' // MI token audience (validated by policy)
  deploymentInPath: 'false'       // Model in request body
  isSharedToAll: false            // Share with project users
  inferenceAPIVersion: ''         // APIM defaults
  deploymentAPIVersion: ''        // APIM defaults
  staticModels: []                // Dynamic discovery
  listModelsEndpoint: ''          // APIM defaults
  getModelEndpoint: ''            // APIM defaults
  deploymentProvider: ''          // AzureOpenAI format
  customHeaders: {}               // No custom headers (api-key added automatically for MI)
  authConfig: {}                  // Default api-key header
}

Benefits:

  • ✅ Seamless integration with Microsoft Foundry agents
  • ✅ Credentials stored securely in Foundry connection
  • ✅ Supports the "Bring Your Own AI Gateway" pattern
  • ✅ Automatic model discovery from APIM
  • ✅ No need to manage secrets separately

Usage in Foundry Agent:

import os
from azure.ai.projects import AIProjectClient
from azure.identity import DefaultAzureCredential

# Connection name follows pattern: <prefix>-<serviceCode>
# Example: HR-ChatBot-DEV-LLM
connection_name = "HR-ChatBot-DEV-LLM"
model_deployment = f"{connection_name}/gpt-4o"

os.environ["AZURE_AI_MODEL_DEPLOYMENT_NAME"] = model_deployment

client = AIProjectClient(
    credential=DefaultAzureCredential(),
    endpoint="https://your-foundry.cognitiveservices.azure.com/"
)

# Create agent using the APIM connection
agent = client.agents.create_agent(
    model=model_deployment,
    name="my-hr-assistant",
    instructions="You are a helpful HR assistant."
)

Foundry Configuration Options

Option Type Default Description
connectionNamePrefix string '' Custom prefix for connection names. Empty uses <BU>-<UseCase>-<ENV>
authType string 'ProjectManagedIdentity' Connection auth mode. ProjectManagedIdentity (default) = managed-identity JWT + api-key header; ApiKey = subscription key only (original behavior)
managedIdentityAudience string 'https://cognitiveservices.azure.com' Audience the project MI requests a token for (must match the JWT audience validated by the product policy). Only used when authType = ProjectManagedIdentity
deploymentInPath string 'false' 'true': model in URL, 'false': model in body
isSharedToAll bool false Share connection with all project users
inferenceAPIVersion string '' API version for chat/embeddings (empty = APIM defaults)
deploymentAPIVersion string '' API version for discovery (empty = APIM defaults)
staticModels array [] Fixed model list (skips discovery)
listModelsEndpoint string '' Custom list endpoint (empty = /deployments)
getModelEndpoint string '' Custom get endpoint (empty = /deployments/{id})
deploymentProvider string '' Discovery format (AzureOpenAI or OpenAI)
customHeaders object {} Additional headers for requests (the api-key header is added automatically for ProjectManagedIdentity)
authConfig object {} Custom auth header config (applies to the ApiKey mode)

LLM endpoint selection: the Foundry connection always targets the LLM inference endpoint of the contract. Set foundryApiName on the service to pick which granted API is the LLM endpoint; when omitted the first known LLM API (universal-llm-api / azure-openai-api / unified-ai-api) is used, else the first API in apiNameMapping[code].

Foundry connection authentication (authType)

The Foundry connection supports two authentication modes. The APIM gateway itself supports api-key only or api-key + JWT, and the two connection modes map directly onto that:

authType What Foundry sends APIM validates Product policy needs
ProjectManagedIdentity (default) Authorization: Bearer <MI token> and api-key: <subscription key> (custom header) The subscription key (api-key) and the JWT (audience https://cognitiveservices.azure.com) JWT validation enabled for that audience
ApiKey (backward compatible) api-key: <subscription key> (stored in credentials.key) The subscription key only Nothing extra (api-key is always validated)

How ProjectManagedIdentity works:

  1. The connection is created with authType = ProjectManagedIdentity and no stored key — the subscription key is instead injected into customHeaders as api-key.
  2. At runtime the Foundry project managed identity acquires an Entra ID token for managedIdentityAudience (default https://cognitiveservices.azure.com) and sends it as Authorization: Bearer ….
  3. APIM validates the subscription key (api-key) and the JWT. The access contract's product policy enables JWT validation for the cognitive services audience by reusing the gateway's security-handler (see Foundry ProjectManagedIdentity JWT validation).

Prerequisites for ProjectManagedIdentity:

  • Enable a system-assigned or user-assigned managed identity on the Foundry project (Azure portal → Foundry resource → Projects → your project → Identity).
  • The product policy must validate the JWT for the same audience. When you use the base-access-contract-request module or the validation notebook, this policy is generated automatically. For manual contracts, add the snippet from the policy guide.
  • The gateway validates audience + issuer by default; optionally restrict to the project MI's client ID for tighter security (see the policy guide).

✅ Backward compatible — set authType = 'ApiKey' (and omit managedIdentityAudience) to reproduce the original subscription-key-only connection exactly. Existing contracts that want the old behavior should set authType = 'ApiKey' explicitly.

Combined Targets Example

You can use Key Vault AND Foundry together:

// Store secrets in Key Vault for traditional apps
param useTargetAzureKeyVault = true
param keyVault = {
  subscriptionId: 'YOUR-SUB-ID'
  resourceGroupName: 'YOUR-KV-RG'
  name: 'YOUR-KV-NAME'
}

// Also create Foundry connections for AI agents
param useTargetFoundry = true
param foundry = {
  subscriptionId: 'YOUR-FOUNDRY-SUB-ID'
  resourceGroupName: 'YOUR-FOUNDRY-RG'
  accountName: 'YOUR-FOUNDRY-ACCOUNT'
  projectName: 'YOUR-PROJECT-NAME'
}

This creates:

  • APIM products and subscriptions
  • Key Vault secrets for traditional applications
  • Foundry APIM connections for AI agents

All using the same subscription keys, ensuring consistent governance.

🧩 Foundry supports the LLM endpoint only. For a multi-asset (MULTI-) contract the Foundry connection is wired to the LLM inference endpoint (chosen via foundryApiName, defaulting to the first known LLM API). The Tool (MCP) and Agent (A2A) endpoints in the same contract are not carried by the Foundry connection.

Recommendation: for multi-asset contracts, enable Key Vault alongside Foundry (both useTargetAzureKeyVault = true and useTargetFoundry = true). Foundry gives agents the LLM connection, while Key Vault publishes the shared key and every asset's endpoint (LLM + Tools + Agents) so the contract owner has everything needed to reach all granted asset types using the same api-key.


Creating Custom Policies

Using Default Policy

The simplest approach - omit policyXml or set it to empty string:

param services = [
  {
    code: 'LLM'
    endpointSecretName: 'LLM-ENDPOINT'
    apiKeySecretName: 'LLM-KEY'
    policyXml: ''  // Uses policies/default-ai-product-policy.xml
  }
]

Default policy includes:

  • Model restrictions (GPT-4o, deepseek-r1)
  • Token limits (300 tokens/min + 10,000 tokens/month)
  • Content safety checks

Creating Custom Policy

Step 1: Create policy XML file in your use case folder

You can use the default-ai-product-policy.xml as a base policy for LLM and extend/modify it as needed.

Additional policy capabilities can be found in Access contracts policy snippets for more options and patterns that can be leveraged.

Note: APIM has power policy engine that can be used even beyond what is provided in Citadel Governance Hub. Leverage agentic development like GitHub Copilot to help you create custom policies based on your requirements.

Step 2: Reference policy in bicepparam

param services = [
  {
    code: 'LLM'
    endpointSecretName: 'LLM-ENDPOINT'
    apiKeySecretName: 'LLM-KEY'
    policyXml: loadTextContent('my-custom-policy.xml')
  }
]

Advanced Scenarios

Multi-asset contract (one product, LLM + Tool + Agent, endpoint per asset)

A single MULTI- product grants LLM + Tools/MCP + Agents/A2A under one shared key, and Key Vault receives one endpoint secret per asset plus the shared key. Enable Key Vault (for all endpoints) alongside Foundry (for the LLM connection):

param useTargetAzureKeyVault = true
param useTargetFoundry = true

param apiNameMapping = {
  MULTI: ['universal-llm-api', 'weather-tool', 'hr-chat-agent']
}

param services = [
  {
    code: 'MULTI'
    apiKeySecretName: 'HR-CHATAGENT-KEY'      // single shared key for all assets
    foundryApiName: 'universal-llm-api'       // Foundry connection targets the LLM endpoint
    assetEndpoints: [
      { apiName: 'universal-llm-api', endpointSecretName: 'HR-CHATAGENT-LLM-ENDPOINT' }
      { apiName: 'weather-tool' }             // auto-named endpoint secret
      { apiName: 'hr-chat-agent' }            // auto-named endpoint secret
    ]
    policyXml: loadTextContent('ai-product-policy.xml')
  }
]

Result: 1 product, 1 subscription (shared key), and in Key Vault: 1 key secret (hr-chatagent-key) + 3 endpoint secrets (hr-chatagent-llm-endpoint, multi-hr-chatagent-dev-weather-tool-endpoint, multi-hr-chatagent-dev-hr-chat-agent-endpoint). The Foundry connection points at the LLM endpoint. To publish an endpoint for every granted API without listing them, set publishAllAssetEndpoints: true instead of assetEndpoints.

Multiple Services in One Use Case

Onboard multiple AI services simultaneously:

param apiNameMapping = {
  LLM: ['azure-openai-api', 'universal-llm-api']
  DOC: ['document-intelligence-api', 'document-intelligence-api-legacy']
  SRCH: ['azure-ai-search-index-api']
  OAIRT: ['openai-realtime-ws-api']
}

param services = [
  {
    code: 'LLM'
    endpointSecretName: 'MULTI-LLM-ENDPOINT'
    apiKeySecretName: 'MULTI-LLM-KEY'
    policyXml: loadTextContent('llm-policy.xml')
  }
  {
    code: 'DOC'
    endpointSecretName: 'MULTI-DOC-ENDPOINT'
    apiKeySecretName: 'MULTI-DOC-KEY'
    policyXml: loadTextContent('doc-policy.xml')
  }
  {
    code: 'SRCH'
    endpointSecretName: 'MULTI-SEARCH-ENDPOINT'
    apiKeySecretName: 'MULTI-SEARCH-KEY'
    policyXml: ''  // Use default
  }
]

Result: Creates 3 APIM products, 3 subscriptions, 6 Key Vault secrets (endpoint + key for each service).


Deployment Outputs

After deployment, the following outputs are available:

When Using Key Vault (useTargetAzureKeyVault = true)

Output Type Description Example
apimGatewayUrl string APIM gateway base URL https://apim-gateway.azure-api.net
useKeyVault bool Always true true
products[] array Created products [{ productId: "OAI-Healthcare-...", displayName: "..." }]
subscriptions[] array KV secret names. keyVaultApiKeySecretName is the shared key; keyVaultEndpointSecretName is the first endpoint (legacy); keyVaultEndpointSecretNames[] lists every per-asset endpoint secret [{ name: "MULTI-...-SUB-01", keyVaultApiKeySecretName: "hr-chatagent-key", keyVaultEndpointSecretNames: ["hr-chatagent-llm-endpoint", "multi-hr-chatagent-dev-weather-tool-endpoint"], ... }]

Access secrets from Key Vault:

$subs = (az deployment sub show --name my-deployment --query properties.outputs.subscriptions.value -o json | ConvertFrom-Json)
# Shared key
$key = az keyvault secret show --vault-name <kv-name> --name ($subs[0].keyVaultApiKeySecretName) --query value -o tsv
# Every per-asset endpoint
foreach ($name in $subs[0].keyVaultEndpointSecretNames) {
  $ep = az keyvault secret show --vault-name <kv-name> --name $name --query value -o tsv
  Write-Host "$name = $ep"
}

When NOT Using Key Vault (useTargetAzureKeyVault = false)

Output Type Description Contains Secrets
apimGatewayUrl string APIM gateway base URL No
useKeyVault bool Always false No
products[] array Created products No
endpoints[] array Direct credentials ⚠️ YES

Endpoints output structure:

[
  {
    "code": "OAI",
    "productId": "OAI-Healthcare-PatientAssistant-DEV",
    "subscriptionName": "OAI-Healthcare-PatientAssistant-DEV-SUB-01",
    "endpoint": "https://apim-gateway.azure-api.net/openai",
    "apiKey": "abc123...xyz"  // ⚠️ Sensitive
  }
]

Extract credentials:

$output = az deployment sub show --name my-deployment --query properties.outputs.endpoints.value -o json | ConvertFrom-Json
$oaiCreds = $output | Where-Object { $_.code -eq 'OAI' }

# Store securely in CI/CD
Write-Host "##vso[task.setvariable variable=OAI_ENDPOINT;issecret=true]$($oaiCreds.endpoint)"
Write-Host "##vso[task.setvariable variable=OAI_KEY;issecret=true]$($oaiCreds.apiKey)"

⚠️ Security Warning: The endpoints output contains sensitive API keys when not using Key Vault. Always:

  • Mark as secrets in CI/CD systems
  • Never log or display in plaintext
  • Store in secure secret management systems
  • Rotate keys regularly

JWT Authentication for Access Contracts

Access Contracts support layered authentication: API Key is always required, with optional JWT Bearer token validation per product. This is configured during contract creation, not at deployment time.

Prerequisites

  • The Citadel Governance Hub must be deployed with entraAuth=true so that APIM JWT named values (JWT-TenantId, JWT-AppRegistrationId, JWT-Issuer, JWT-OpenIdConfigUrl) and the security-handler policy fragment are provisioned
  • An Entra ID App Registration exists (auto-provisioned by the entra-id Bicep module or provided manually)
  • Client secret is stored in Key Vault as ENTRA-APP-CLIENT-SECRET

How It Works

  1. The product policy snippet jwt-auth.xml sets jwtRequired=true in the APIM context
  2. The security-handler fragment detects the auth method (api-key, jwt, api-key-jwt)
  3. When jwtRequired=true, the fragment validates the JWT Bearer token against the Entra ID OpenID configuration
  4. Requests without a valid JWT are rejected with 401 Unauthorized

Authentication Flow

Scenario Headers Required Result
API Key only (JWT not enabled) api-key: {key} ✅ Allowed
API Key + JWT (JWT enabled) api-key: {key} + Authorization: Bearer {token} ✅ Allowed
API Key only (JWT enabled) api-key: {key} ❌ 401 - JWT required
JWT only (no API Key) Authorization: Bearer {token} ❌ 401 - API key required
Invalid JWT (JWT enabled) api-key: {key} + Authorization: Bearer {invalid} ❌ 401 - Invalid token

Enabling JWT via Agent Access Contract Request

When using the base-access-contract-request module, enable JWT in the contract JSON:

{
  "contractInfo": {
    "businessUnit": "Security",
    "useCaseName": "SecureAgent",
    "environment": "DEV"
  },
  "policies": {
    "jwtAuth": { "enabled": true },
    "modelAccess": {
      "enabled": true,
      "allowedModels": ["gpt-4o", "gpt-4o-mini"]
    }
  },
  "services": [
    {
      "code": "LLM",
      "endpointSecretName": "SECURE-AGENT-ENDPOINT",
      "apiKeySecretName": "SECURE-AGENT-KEY"
    }
  ]
}

Enabling JWT via Custom Product Policy

For manual access contracts (not using the agent request module), add the JWT snippet to your product policy XML:

<policies>
    <inbound>
        <base />
        <!-- Enable JWT requirement for this product -->
        <set-variable name="jwtRequired" value="true" />

        <!-- Other policy snippets (model access, capacity, etc.) -->
    </inbound>
    <backend>
        <base />
    </backend>
    <outbound>
        <base />
    </outbound>
    <on-error>
        <base />
    </on-error>
</policies>

Acquiring a JWT Token

Use the client credentials flow to obtain a JWT token from Microsoft Entra ID:

POST https://login.microsoftonline.com/{tenant-id}/oauth2/v2.0/token
Content-Type: application/x-www-form-urlencoded

grant_type=client_credentials
&client_id={entra-app-client-id}
&client_secret={entra-app-client-secret}
&scope={audience}/.default

Then include the token in API requests:

POST https://{apim-gateway}/openai/deployments/gpt-4o/chat/completions?api-version=2024-02-15-preview
api-key: {subscription-key}
Authorization: Bearer {jwt-token}
Content-Type: application/json

Validation

Use the JWT Access Contract Validation Notebook to test end-to-end JWT authentication with access contracts.


Support

For issues or questions:


© 2026 Azure AI Landing Zones