IMPORTANT: Template Doctor uses Managed Identity (MI) for production Cosmos DB authentication, NOT connection strings. This document explains how it works and how to set it up.
- Overview
- How Managed Identity Works
- Current Deployment State
- Setup Options
- Environment Variables
- Troubleshooting
✅ Security: No connection strings in code, config, or logs
✅ Zero Secrets: Tokens are acquired automatically by Azure
✅ Automatic Rotation: Azure handles credential lifecycle
✅ RBAC Control: Fine-grained permissions via Azure roles
✅ Audit Trail: All database access logged in Azure AD
┌──────────────────────────────────────┐
│ Container App (Template Doctor) │
│ • System-Assigned Managed Identity│ ◄── No secrets stored
└────────────┬─────────────────────────┘
│
│ 1. Requests token from Azure AD
▼
┌──────────────────────────────────────┐
│ Azure AD (Entra ID) │
│ Validates Container App identity │
└────────────┬─────────────────────────┘
│
│ 2. Returns access token
▼
┌──────────────────────────────────────┐
│ Application Code (database.ts) │
│ • DefaultAzureCredential │
│ • Builds MongoDB conn string │
│ • Uses token as username/password │
└────────────┬─────────────────────────┘
│
│ 3. Connects with token
▼
┌──────────────────────────────────────┐
│ Azure Cosmos DB (MongoDB API) │
│ • Validates token against RBAC │
│ • Grants access if role assigned │
└──────────────────────────────────────┘
When deployed via Bicep (infra/core/host/container-app.bicep):
resource containerApp 'Microsoft.App/containerApps@2023-05-01' = {
identity: {
type: 'SystemAssigned' // ◄── Creates MI automatically
}
}Azure creates:
- Service Principal in Azure AD
- Principal ID (unique GUID)
- Managed Identity attached to the Container App
Two methods:
infra/database.bicep grants the Cosmos DB Built-in Data Contributor role:
resource roleAssignment 'Microsoft.DocumentDB/databaseAccounts/mongodbRoleAssignments@2024-05-15' = {
properties: {
roleDefinitionId: '${cosmosAccount.id}/mongodbRoleDefinitions/00000000-0000-0000-0000-000000000002'
principalId: principalId // ◄── Container App's MI Principal ID
scope: cosmosAccount.id
}
}- Go to Cosmos DB account → Data Explorer
- Click RBAC → Add Role Assignment
- Select role:
Cosmos DB Built-in Data Contributor - Assign to: Container App's Managed Identity
Code in packages/server/src/services/database.ts:
import { DefaultAzureCredential } from '@azure/identity';
// Get COSMOS_ENDPOINT from environment (e.g., https://cosmos-abc123.documents.azure.com)
const cosmosEndpoint = process.env.COSMOS_ENDPOINT;
// Acquire access token using Container App's Managed Identity
const credential = new DefaultAzureCredential();
const tokenResponse = await credential.getToken('https://cosmos.azure.com/.default');
const token = tokenResponse.token;
// Build MongoDB connection string with token as credentials
const connString = `mongodb://${encodeURIComponent(token)}:${encodeURIComponent(token)}@${cosmosEndpoint.replace('https://', '')}:10255/?ssl=true&replicaSet=globaldb&retrywrites=false&maxIdleTimeMS=120000&appName=@template-doctor@`;
// Connect to Cosmos DB
const client = new MongoClient(connString);
await client.connect();Key Point: The token is used as both username and password in the MongoDB connection string. Cosmos DB validates this token against its RBAC configuration.
Tokens expire after ~1 hour. The application automatically refreshes:
// Token refresh every 1 hour
setInterval(async () => {
await this.disconnect();
await this.connect(); // ◄── Acquires new token
}, 3600000);// Cosmos DB Module - COMMENTED OUT: Using existing database
/*
module cosmos './database.bicep' = {
name: 'cosmos-db-deployment'
scope: rg
params: {
location: location
environmentName: environmentName
principalId: principalId // ◄── Would pass Container App's MI
}
}
*/This means:
azd upwill NOT provision Cosmos DB automatically- You must either:
- Uncomment the module to let azd provision it (Recommended)
- Create Cosmos DB manually and configure MI yourself
infra/main.bicep currently passes MONGODB_URI (connection string) instead of COSMOS_ENDPOINT:
env: concat([
{
name: 'MONGODB_URI' // ◄── WRONG: This is for connection strings
value: mongodbUri // ◄── User must provide full connection string
}
])This bypasses Managed Identity! Users are forced to use connection strings.
// Cosmos DB Module
module cosmos './database.bicep' = {
name: 'cosmos-db-deployment'
scope: rg
params: {
location: location
environmentName: environmentName
principalId: containerApp.outputs.principalId // Pass Container App's MI
}
}Replace the MONGODB_URI environment variable with:
env: concat([
{
name: 'COSMOS_ENDPOINT'
value: 'https://${cosmos.outputs.cosmosAccountName}.documents.azure.com'
}
{
name: 'COSMOS_DATABASE_NAME'
value: cosmos.outputs.cosmosDatabaseName
}
// ... other env vars
])In infra/main.bicep, delete:
@secure()
@description('MongoDB connection string - set in .env as MONGODB_URI')
param mongodbUri stringazd upWhat happens:
- Bicep provisions Cosmos DB (serverless, MongoDB API)
- Bicep creates Container App with System-Assigned MI
- Bicep grants MI the
Cosmos DB Built-in Data Contributorrole - Container App starts with
COSMOS_ENDPOINTenv var - Application uses
DefaultAzureCredentialto connect (NO connection string!)
If you already have a Cosmos DB account:
az cosmosdb show \
--name YOUR_COSMOS_ACCOUNT_NAME \
--resource-group YOUR_RG \
--query "documentEndpoint" -o tsvExample output: https://cosmos-abc123.documents.azure.com
After deploying Container App:
az containerapp show \
--name YOUR_CONTAINER_APP_NAME \
--resource-group YOUR_RG \
--query "identity.principalId" -o tsvExample output: 12345678-1234-1234-1234-123456789abc
Option A: Azure CLI
az cosmosdb mongodb role assignment create \
--account-name YOUR_COSMOS_ACCOUNT_NAME \
--resource-group YOUR_RG \
--role-definition-id "00000000-0000-0000-0000-000000000002" \
--principal-id "CONTAINER_APP_MI_PRINCIPAL_ID" \
--scope "/"Option B: Azure Portal
- Go to Cosmos DB account → Data Explorer
- Click RBAC
- Click + Add
- Select role:
Cosmos DB Built-in Data Contributor - Assign to: Container App's Managed Identity (paste Principal ID)
- Scope:
/(entire account)
az containerapp update \
--name YOUR_CONTAINER_APP_NAME \
--resource-group YOUR_RG \
--set-env-vars \
"COSMOS_ENDPOINT=https://YOUR_COSMOS_ACCOUNT.documents.azure.com" \
"COSMOS_DATABASE_NAME=template-doctor"az containerapp update \
--name YOUR_CONTAINER_APP_NAME \
--resource-group YOUR_RG \
--remove-env-vars "MONGODB_URI"az containerapp revision restart \
--name YOUR_CONTAINER_APP_NAME \
--resource-group YOUR_RG# Set these in Container App environment
COSMOS_ENDPOINT=https://cosmos-abc123.documents.azure.com
COSMOS_DATABASE_NAME=template-doctor
# DO NOT SET these (MI handles authentication):
# MONGODB_URI=<should not be set>
# COSMOS_KEY=<should not be set># Leave MONGODB_URI unset in .env
# Docker Compose will use: mongodb://mongodb:27017/template-doctorconst mongoUri = process.env.MONGODB_URI;
const cosmosEndpoint = process.env.COSMOS_ENDPOINT;
if (mongoUri) {
// Local MongoDB (connection string)
this.client = new MongoClient(mongoUri);
} else if (cosmosEndpoint) {
// Cosmos DB with Managed Identity (token-based)
const credential = new DefaultAzureCredential();
const token = await credential.getToken('https://cosmos.azure.com/.default');
const connString = `mongodb://${token}:${token}@${cosmosEndpoint}:10255/...`;
this.client = new MongoClient(connString);
} else {
throw new Error('No database configuration found');
}Cause: Container App doesn't have System-Assigned MI enabled.
Fix:
az containerapp identity assign \
--name YOUR_CONTAINER_APP_NAME \
--resource-group YOUR_RG \
--system-assignedCause: MI doesn't have RBAC role assigned on Cosmos DB.
Fix: Follow Step 3 in Option 2 to assign the role.
Cause: Container App environment variables not configured.
Fix:
az containerapp update \
--name YOUR_CONTAINER_APP_NAME \
--resource-group YOUR_RG \
--set-env-vars "COSMOS_ENDPOINT=https://YOUR_COSMOS_ACCOUNT.documents.azure.com"Cause: Cosmos DB firewall blocking Container App.
Fix: Enable Public Network Access in Cosmos DB:
- Go to Cosmos DB → Networking
- Select All networks (or add Container App's subnet)
- Click Save
Check Container App logs:
az containerapp logs show \
--name YOUR_CONTAINER_APP_NAME \
--resource-group YOUR_RG \
--followLook for:
{"level":"INFO","msg":"Connecting to Cosmos DB","cosmosEndpoint":"https://cosmos-abc123.documents.azure.com"}
{"level":"INFO","msg":"Connected to Cosmos DB database","databaseName":"template-doctor"}NOT this (means connection string is being used):
{"level":"INFO","msg":"Connecting to local MongoDB"}- Cosmos DB MongoDB API with Managed Identity
- Container Apps Managed Identity
- DefaultAzureCredential
- Cosmos DB Built-in Roles
- Fix Bicep: Update
infra/main.bicepto useCOSMOS_ENDPOINTinstead ofMONGODB_URI - Uncomment Cosmos module: Let azd provision Cosmos DB automatically
- Update setup script: Clarify that production uses MI, not connection strings
- Test deployment: Verify MI authentication works end-to-end