⏱️ Estimated Time: 45-60 minutes | 💰 Cost Impact: Free (no additional charges) | ⭐ Complexity: Intermediate
📚 Learning Path:
- ← Previous: Configuration Management - Managing environment variables and secrets
- 🎯 You Are Here: Authentication & Security (Managed Identity, Key Vault, secure patterns)
- → Next: First Project - Build your first AZD application
- 🏠 Course Home
By completing this lesson, you will:
- Understand Azure authentication patterns (keys, connection strings, managed identity)
- Implement Managed Identity for passwordless authentication
- Secure secrets with Azure Key Vault integration
- Configure role-based access control (RBAC) for AZD deployments
- Apply security best practices in Container Apps and Azure services
- Migrate from key-based to identity-based authentication
Before Managed Identity:
// ❌ SECURITY RISK: Hardcoded secrets in code
const connectionString = "Server=mydb.database.windows.net;User=admin;Password=P@ssw0rd123";
const storageKey = "xK7mN9pQ2wR5tY8uI0oP3aS6dF1gH4jK...";
const cosmosKey = "C2x7B9n4M1p8Q5w3E6r0T2y5U8i1O4p7...";Problems:
- 🔴 Exposed secrets in code, config files, environment variables
- 🔴 Credential rotation requires code changes and redeployment
- 🔴 Audit nightmares - who accessed what, when?
- 🔴 Sprawl - secrets scattered across multiple systems
- 🔴 Compliance risks - fails security audits
After Managed Identity:
// ✅ SECURE: No secrets in code
const credential = new DefaultAzureCredential();
const client = new BlobServiceClient(
"https://mystorageaccount.blob.core.windows.net",
credential // Azure automatically handles authentication
);Benefits:
- ✅ Zero secrets in code or configuration
- ✅ Automatic rotation - Azure handles it
- ✅ Full audit trail in Azure AD logs
- ✅ Centralized security - manage in Azure Portal
- ✅ Compliance ready - meets security standards
Analogy: Traditional authentication is like carrying multiple physical keys for different doors. Managed Identity is like having a security badge that automatically grants access based on who you are—no keys to lose, copy, or rotate.
sequenceDiagram
participant App as Your Application<br/>(Container App)
participant MI as Managed Identity<br/>(Azure AD)
participant KV as Key Vault
participant Storage as Azure Storage
participant DB as Azure SQL
App->>MI: Request access token<br/>(automatic)
MI->>MI: Verify identity<br/>(no password needed)
MI-->>App: Return token<br/>(valid 1 hour)
App->>KV: Get secret<br/>(using token)
KV->>KV: Check RBAC permissions
KV-->>App: Return secret value
App->>Storage: Upload blob<br/>(using token)
Storage->>Storage: Check RBAC permissions
Storage-->>App: Success
App->>DB: Query data<br/>(using token)
DB->>DB: Check SQL permissions
DB-->>App: Return results
Note over App,DB: All authentication passwordless!
graph TB
MI[Managed Identity]
SystemAssigned[System-Assigned Identity]
UserAssigned[User-Assigned Identity]
MI --> SystemAssigned
MI --> UserAssigned
SystemAssigned --> SA1[Lifecycle tied to resource]
SystemAssigned --> SA2[Automatic creation/deletion]
SystemAssigned --> SA3[Best for single resource]
UserAssigned --> UA1[Independent lifecycle]
UserAssigned --> UA2[Manual creation/deletion]
UserAssigned --> UA3[Shared across resources]
style SystemAssigned fill:#2196F3,stroke:#1976D2,stroke-width:2px,color:#fff
style UserAssigned fill:#4CAF50,stroke:#388E3C,stroke-width:2px,color:#fff
| Feature | System-Assigned | User-Assigned |
|---|---|---|
| Lifecycle | Tied to resource | Independent |
| Creation | Automatic with resource | Manual creation |
| Deletion | Deleted with resource | Persists after resource deletion |
| Sharing | One resource only | Multiple resources |
| Use Case | Simple scenarios | Complex multi-resource scenarios |
| AZD Default | ✅ Recommended | Optional |
You should already have these installed from previous lessons:
# Verify Azure Developer CLI
azd version
# ✅ Expected: azd version 1.0.0 or higher
# Verify Azure CLI
az --version
# ✅ Expected: azure-cli 2.50.0 or higher- Active Azure subscription
- Permissions to:
- Create managed identities
- Assign RBAC roles
- Create Key Vault resources
- Deploy Container Apps
You should have completed:
- Installation Guide - AZD setup
- AZD Basics - Core concepts
- Configuration Management - Environment variables
How it works:
# Connection string contains credentials
STORAGE_CONNECTION_STRING="DefaultEndpointsProtocol=https;AccountName=myaccount;AccountKey=xK7mN9pQ2wR5..."
COSMOS_CONNECTION_STRING="AccountEndpoint=https://myaccount.documents.azure.com:443/;AccountKey=C2x7..."
SQL_CONNECTION_STRING="Server=myserver.database.windows.net;User=admin;Password=P@ssw0rd..."Problems:
- ❌ Secrets visible in environment variables
- ❌ Logged in deployment systems
- ❌ Difficult to rotate
- ❌ No audit trail of access
When to use: Only for local development, never production.
How it works:
// Store secret in Key Vault
resource keyVault 'Microsoft.KeyVault/vaults@2023-02-01' = {
name: 'mykv'
properties: {
enableRbacAuthorization: true
}
}
// Reference in Container App
env: [
{
name: 'STORAGE_KEY'
secretRef: 'storage-key' // References Key Vault
}
]Benefits:
- ✅ Secrets stored securely in Key Vault
- ✅ Centralized secret management
- ✅ Rotation without code changes
Limitations:
⚠️ Still using keys/passwords⚠️ Need to manage Key Vault access
When to use: Transition step from connection strings to managed identity.
How it works:
// Enable managed identity
resource containerApp 'Microsoft.App/containerApps@2023-05-01' = {
name: 'myapp'
identity: {
type: 'SystemAssigned' // Automatically creates identity
}
}
// Grant permissions
resource roleAssignment 'Microsoft.Authorization/roleAssignments@2022-04-01' = {
scope: storageAccount
properties: {
roleDefinitionId: storageBlobDataContributorRole
principalId: containerApp.identity.principalId
}
}Application code:
// No secrets needed!
const { DefaultAzureCredential } = require('@azure/identity');
const { BlobServiceClient } = require('@azure/storage-blob');
const credential = new DefaultAzureCredential();
const blobServiceClient = new BlobServiceClient(
'https://mystorageaccount.blob.core.windows.net',
credential
);Benefits:
- ✅ Zero secrets in code/config
- ✅ Automatic credential rotation
- ✅ Full audit trail
- ✅ RBAC-based permissions
- ✅ Compliance ready
When to use: Always, for production applications.
Let's build a secure Container App that uses managed identity to access Azure Storage and Key Vault.
secure-app/
├── azure.yaml # AZD configuration
├── infra/
│ ├── main.bicep # Main infrastructure
│ ├── core/
│ │ ├── identity.bicep # Managed identity setup
│ │ ├── keyvault.bicep # Key Vault configuration
│ │ └── storage.bicep # Storage with RBAC
│ └── app/
│ └── container-app.bicep
└── src/
├── app.js # Application code
├── package.json
└── Dockerfile
name: secure-app
metadata:
template: secure-app@1.0.0
services:
api:
project: ./src
language: js
host: containerapp
# Enable managed identity (AZD handles this automatically)File: infra/main.bicep
targetScope = 'subscription'
param environmentName string
param location string = 'eastus'
var tags = { 'azd-env-name': environmentName }
// Resource group
resource rg 'Microsoft.Resources/resourceGroups@2021-04-01' = {
name: 'rg-${environmentName}'
location: location
tags: tags
}
// Storage Account
module storage './core/storage.bicep' = {
name: 'storage'
scope: rg
params: {
name: 'st${uniqueString(rg.id)}'
location: location
tags: tags
}
}
// Key Vault
module keyVault './core/keyvault.bicep' = {
name: 'keyvault'
scope: rg
params: {
name: 'kv-${uniqueString(rg.id)}'
location: location
tags: tags
}
}
// Container App with Managed Identity
module containerApp './app/container-app.bicep' = {
name: 'container-app'
scope: rg
params: {
name: 'ca-${environmentName}'
location: location
tags: tags
storageAccountName: storage.outputs.name
keyVaultName: keyVault.outputs.name
}
}
// Grant Container App access to Storage
module storageRoleAssignment './core/role-assignment.bicep' = {
name: 'storage-role'
scope: rg
params: {
principalId: containerApp.outputs.identityPrincipalId
roleDefinitionId: 'ba92f5b4-2d11-453d-a403-e96b0029c9fe' // Storage Blob Data Contributor
targetResourceId: storage.outputs.id
}
}
// Grant Container App access to Key Vault
module kvRoleAssignment './core/role-assignment.bicep' = {
name: 'kv-role'
scope: rg
params: {
principalId: containerApp.outputs.identityPrincipalId
roleDefinitionId: '4633458b-17de-408a-b874-0445c86b69e6' // Key Vault Secrets User
targetResourceId: keyVault.outputs.id
}
}
// Outputs
output AZURE_STORAGE_ACCOUNT_NAME string = storage.outputs.name
output AZURE_KEY_VAULT_NAME string = keyVault.outputs.name
output APP_URL string = containerApp.outputs.urlFile: infra/app/container-app.bicep
param name string
param location string
param tags object = {}
param storageAccountName string
param keyVaultName string
resource containerApp 'Microsoft.App/containerApps@2023-05-01' = {
name: name
location: location
tags: tags
identity: {
type: 'SystemAssigned' // 🔑 Enable managed identity
}
properties: {
configuration: {
ingress: {
external: true
targetPort: 3000
}
}
template: {
containers: [
{
name: 'api'
image: 'myregistry.azurecr.io/api:latest'
resources: {
cpu: json('0.5')
memory: '1Gi'
}
env: [
{
name: 'AZURE_STORAGE_ACCOUNT_NAME'
value: storageAccountName
}
{
name: 'AZURE_KEY_VAULT_NAME'
value: keyVaultName
}
// 🔑 No secrets - managed identity handles authentication!
]
}
]
}
}
}
// Output the identity for RBAC assignments
output identityPrincipalId string = containerApp.identity.principalId
output id string = containerApp.id
output url string = 'https://${containerApp.properties.configuration.ingress.fqdn}'File: infra/core/role-assignment.bicep
param principalId string
param roleDefinitionId string // Azure built-in role ID
param targetResourceId string
resource roleAssignment 'Microsoft.Authorization/roleAssignments@2022-04-01' = {
name: guid(principalId, roleDefinitionId, targetResourceId)
scope: resourceId('Microsoft.Resources/resourceGroups', resourceGroup().name)
properties: {
roleDefinitionId: subscriptionResourceId('Microsoft.Authorization/roleDefinitions', roleDefinitionId)
principalId: principalId
principalType: 'ServicePrincipal'
}
}
output id string = roleAssignment.idFile: src/app.js
const express = require('express');
const { DefaultAzureCredential } = require('@azure/identity');
const { BlobServiceClient } = require('@azure/storage-blob');
const { SecretClient } = require('@azure/keyvault-secrets');
const app = express();
const PORT = process.env.PORT || 3000;
// 🔑 Initialize credential (works automatically with managed identity)
const credential = new DefaultAzureCredential();
// Azure Storage setup
const storageAccountName = process.env.AZURE_STORAGE_ACCOUNT_NAME;
const blobServiceClient = new BlobServiceClient(
`https://${storageAccountName}.blob.core.windows.net`,
credential // No keys needed!
);
// Key Vault setup
const keyVaultName = process.env.AZURE_KEY_VAULT_NAME;
const secretClient = new SecretClient(
`https://${keyVaultName}.vault.azure.net`,
credential // No keys needed!
);
// Health check
app.get('/health', (req, res) => {
res.json({ status: 'healthy', authentication: 'managed-identity' });
});
// Upload file to blob storage
app.post('/upload', async (req, res) => {
try {
const containerClient = blobServiceClient.getContainerClient('uploads');
await containerClient.createIfNotExists();
const blobName = `file-${Date.now()}.txt`;
const blockBlobClient = containerClient.getBlockBlobClient(blobName);
await blockBlobClient.upload('Hello from managed identity!', 30);
res.json({
success: true,
blobName: blobName,
message: 'File uploaded using managed identity!'
});
} catch (error) {
console.error('Upload error:', error);
res.status(500).json({ error: error.message });
}
});
// Get secret from Key Vault
app.get('/secret/:name', async (req, res) => {
try {
const secretName = req.params.name;
const secret = await secretClient.getSecret(secretName);
res.json({
name: secretName,
value: secret.value,
message: 'Secret retrieved using managed identity!'
});
} catch (error) {
console.error('Secret error:', error);
res.status(500).json({ error: error.message });
}
});
// List blob containers (demonstrates read access)
app.get('/containers', async (req, res) => {
try {
const containers = [];
for await (const container of blobServiceClient.listContainers()) {
containers.push(container.name);
}
res.json({
containers: containers,
count: containers.length,
message: 'Containers listed using managed identity!'
});
} catch (error) {
console.error('List error:', error);
res.status(500).json({ error: error.message });
}
});
app.listen(PORT, () => {
console.log(`Secure API listening on port ${PORT}`);
console.log('Authentication: Managed Identity (passwordless)');
});File: src/package.json
{
"name": "secure-app",
"version": "1.0.0",
"dependencies": {
"express": "^4.18.2",
"@azure/identity": "^4.0.0",
"@azure/storage-blob": "^12.17.0",
"@azure/keyvault-secrets": "^4.7.0"
},
"scripts": {
"start": "node app.js"
}
}# Initialize AZD environment
azd init
# Deploy infrastructure and application
azd up
# Get the app URL
APP_URL=$(azd env get-values | grep APP_URL | cut -d '=' -f2 | tr -d '"')
# Test health check
curl $APP_URL/health✅ Expected output:
{
"status": "healthy",
"authentication": "managed-identity"
}Test blob upload:
curl -X POST $APP_URL/upload✅ Expected output:
{
"success": true,
"blobName": "file-1700404800000.txt",
"message": "File uploaded using managed identity!"
}Test container listing:
curl $APP_URL/containers✅ Expected output:
{
"containers": ["uploads"],
"count": 1,
"message": "Containers listed using managed identity!"
}| Service | Role Name | Role ID | Permissions |
|---|---|---|---|
| Storage | Storage Blob Data Reader | 2a2b9908-6b94-4a3d-8e5a-a7d8f8cc8a12 |
Read blobs and containers |
| Storage | Storage Blob Data Contributor | ba92f5b4-2d11-453d-a403-e96b0029c9fe |
Read, write, delete blobs |
| Storage | Storage Queue Data Contributor | 974c5e8b-45b9-4653-ba55-5f855dd0fb88 |
Read, write, delete queue messages |
| Key Vault | Key Vault Secrets User | 4633458b-17de-408a-b874-0445c86b69e6 |
Read secrets |
| Key Vault | Key Vault Secrets Officer | b86a8fe4-44ce-4948-aee5-eccb2c155cd7 |
Read, write, delete secrets |
| Cosmos DB | Cosmos DB Built-in Data Reader | 00000000-0000-0000-0000-000000000001 |
Read Cosmos DB data |
| Cosmos DB | Cosmos DB Built-in Data Contributor | 00000000-0000-0000-0000-000000000002 |
Read, write Cosmos DB data |
| SQL Database | SQL DB Contributor | 9b7fa17d-e63e-47b0-bb0a-15c516ac86ec |
Manage SQL databases |
| Service Bus | Azure Service Bus Data Owner | 090c5cfd-751d-490a-894a-3ce6f1109419 |
Send, receive, manage messages |
# List all built-in roles
az role definition list --query "[].{Name:roleName, ID:name}" --output table
# Search for specific role
az role definition list --query "[?contains(roleName, 'Storage Blob')].{Name:roleName, ID:name}" --output table
# Get role details
az role definition list --name "Storage Blob Data Contributor"Goal: Add managed identity to an existing Container App deployment
Scenario: You have a Container App using connection strings. Convert it to managed identity.
Starting Point: Container App with this configuration:
// ❌ Current: Using connection string
env: [
{
name: 'STORAGE_CONNECTION_STRING'
secretRef: 'storage-connection'
}
]Steps:
- Enable managed identity in Bicep:
resource containerApp 'Microsoft.App/containerApps@2023-05-01' = {
name: 'myapp'
identity: {
type: 'SystemAssigned' // Add this
}
// ... rest of configuration
}- Grant Storage access:
// Get storage account reference
resource storageAccount 'Microsoft.Storage/storageAccounts@2023-01-01' existing = {
name: storageAccountName
}
// Assign role
resource roleAssignment 'Microsoft.Authorization/roleAssignments@2022-04-01' = {
name: guid(containerApp.id, 'ba92f5b4-2d11-453d-a403-e96b0029c9fe', storageAccount.id)
scope: storageAccount
properties: {
roleDefinitionId: subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'ba92f5b4-2d11-453d-a403-e96b0029c9fe')
principalId: containerApp.identity.principalId
principalType: 'ServicePrincipal'
}
}- Update application code:
Before (connection string):
const { BlobServiceClient } = require('@azure/storage-blob');
const blobServiceClient = BlobServiceClient.fromConnectionString(
process.env.STORAGE_CONNECTION_STRING
);After (managed identity):
const { DefaultAzureCredential } = require('@azure/identity');
const { BlobServiceClient } = require('@azure/storage-blob');
const credential = new DefaultAzureCredential();
const blobServiceClient = new BlobServiceClient(
`https://${process.env.STORAGE_ACCOUNT_NAME}.blob.core.windows.net`,
credential
);- Update environment variables:
env: [
{
name: 'STORAGE_ACCOUNT_NAME'
value: storageAccountName // Just the name, no secrets!
}
// Remove STORAGE_CONNECTION_STRING
]- Deploy and test:
# Redeploy
azd up
# Test that it still works
curl https://myapp.azurecontainerapps.io/upload✅ Success Criteria:
- ✅ Application deploys without errors
- ✅ Storage operations work (upload, list, download)
- ✅ No connection strings in environment variables
- ✅ Identity visible in Azure Portal under "Identity" blade
Verification:
# Check managed identity is enabled
az containerapp show \
--name myapp \
--resource-group rg-myapp \
--query "identity.type"
# ✅ Expected: "SystemAssigned"
# Check role assignment
az role assignment list \
--assignee $(az containerapp show --name myapp --resource-group rg-myapp --query "identity.principalId" -o tsv) \
--scope /subscriptions/{sub-id}/resourceGroups/rg-myapp/providers/Microsoft.Storage/storageAccounts/mystorageaccount
# ✅ Expected: Shows "Storage Blob Data Contributor" roleTime: 20-30 minutes
Goal: Create a user-assigned identity shared across multiple Container Apps
Scenario: You have 3 microservices that all need access to the same Storage account and Key Vault.
Steps:
- Create user-assigned identity:
File: infra/core/identity.bicep
param name string
param location string
param tags object = {}
resource userAssignedIdentity 'Microsoft.ManagedIdentity/userAssignedIdentities@2023-01-31' = {
name: name
location: location
tags: tags
}
output id string = userAssignedIdentity.id
output principalId string = userAssignedIdentity.properties.principalId
output clientId string = userAssignedIdentity.properties.clientId- Assign roles to user-assigned identity:
// In main.bicep
module userIdentity './core/identity.bicep' = {
name: 'user-identity'
scope: rg
params: {
name: 'id-${environmentName}'
location: location
tags: tags
}
}
// Grant Storage access
resource storageRoleAssignment 'Microsoft.Authorization/roleAssignments@2022-04-01' = {
name: guid(userIdentity.outputs.principalId, 'storage-contributor')
scope: storageAccount
properties: {
roleDefinitionId: subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'ba92f5b4-2d11-453d-a403-e96b0029c9fe')
principalId: userIdentity.outputs.principalId
principalType: 'ServicePrincipal'
}
}
// Grant Key Vault access
resource kvRoleAssignment 'Microsoft.Authorization/roleAssignments@2022-04-01' = {
name: guid(userIdentity.outputs.principalId, 'kv-secrets-user')
scope: keyVault
properties: {
roleDefinitionId: subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '4633458b-17de-408a-b874-0445c86b69e6')
principalId: userIdentity.outputs.principalId
principalType: 'ServicePrincipal'
}
}- Assign identity to multiple Container Apps:
resource apiGateway 'Microsoft.App/containerApps@2023-05-01' = {
name: 'api-gateway'
identity: {
type: 'UserAssigned'
userAssignedIdentities: {
'${userIdentity.outputs.id}': {}
}
}
// ... rest of config
}
resource productService 'Microsoft.App/containerApps@2023-05-01' = {
name: 'product-service'
identity: {
type: 'UserAssigned'
userAssignedIdentities: {
'${userIdentity.outputs.id}': {}
}
}
// ... rest of config
}
resource orderService 'Microsoft.App/containerApps@2023-05-01' = {
name: 'order-service'
identity: {
type: 'UserAssigned'
userAssignedIdentities: {
'${userIdentity.outputs.id}': {}
}
}
// ... rest of config
}- Application code (all services use same pattern):
const { DefaultAzureCredential, ManagedIdentityCredential } = require('@azure/identity');
// For user-assigned identity, specify the client ID
const credential = new ManagedIdentityCredential(
process.env.AZURE_CLIENT_ID // User-assigned identity client ID
);
// Or use DefaultAzureCredential (auto-detects)
const credential = new DefaultAzureCredential();
const blobServiceClient = new BlobServiceClient(
`https://${process.env.STORAGE_ACCOUNT_NAME}.blob.core.windows.net`,
credential
);- Deploy and verify:
azd up
# Test all services can access storage
curl https://api-gateway.azurecontainerapps.io/upload
curl https://product-service.azurecontainerapps.io/upload
curl https://order-service.azurecontainerapps.io/upload✅ Success Criteria:
- ✅ One identity shared across 3 services
- ✅ All services can access Storage and Key Vault
- ✅ Identity persists if you delete one service
- ✅ Centralized permission management
Benefits of User-Assigned Identity:
- Single identity to manage
- Consistent permissions across services
- Survives service deletion
- Better for complex architectures
Time: 30-40 minutes
Goal: Store third-party API keys in Key Vault and access them using managed identity
Scenario: Your app needs to call an external API (OpenAI, Stripe, SendGrid) that requires API keys.
Steps:
- Create Key Vault with RBAC:
File: infra/core/keyvault.bicep
param name string
param location string
param tags object = {}
resource keyVault 'Microsoft.KeyVault/vaults@2023-02-01' = {
name: name
location: location
tags: tags
properties: {
enableRbacAuthorization: true // Use RBAC instead of access policies
sku: {
family: 'A'
name: 'standard'
}
tenantId: subscription().tenantId
enableSoftDelete: true
softDeleteRetentionInDays: 90
}
}
// Allow Container App to read secrets
output id string = keyVault.id
output name string = keyVault.name
output uri string = keyVault.properties.vaultUri- Store secrets in Key Vault:
# Get Key Vault name
KV_NAME=$(azd env get-values | grep AZURE_KEY_VAULT_NAME | cut -d '=' -f2 | tr -d '"')
# Store third-party API keys
az keyvault secret set \
--vault-name $KV_NAME \
--name "OpenAI-ApiKey" \
--value "sk-proj-xxxxxxxxxxxxx"
az keyvault secret set \
--vault-name $KV_NAME \
--name "Stripe-ApiKey" \
--value "sk_live_xxxxxxxxxxxxx"
az keyvault secret set \
--vault-name $KV_NAME \
--name "SendGrid-ApiKey" \
--value "SG.xxxxxxxxxxxxx"- Application code to retrieve secrets:
File: src/config.js
const { DefaultAzureCredential } = require('@azure/identity');
const { SecretClient } = require('@azure/keyvault-secrets');
class Config {
constructor() {
this.credential = new DefaultAzureCredential();
this.secretClient = new SecretClient(
`https://${process.env.AZURE_KEY_VAULT_NAME}.vault.azure.net`,
this.credential
);
this.cache = {};
}
async getSecret(secretName) {
// Check cache first
if (this.cache[secretName]) {
return this.cache[secretName];
}
try {
const secret = await this.secretClient.getSecret(secretName);
this.cache[secretName] = secret.value;
console.log(`✅ Retrieved secret: ${secretName}`);
return secret.value;
} catch (error) {
console.error(`❌ Failed to get secret ${secretName}:`, error.message);
throw error;
}
}
async getOpenAIKey() {
return this.getSecret('OpenAI-ApiKey');
}
async getStripeKey() {
return this.getSecret('Stripe-ApiKey');
}
async getSendGridKey() {
return this.getSecret('SendGrid-ApiKey');
}
}
module.exports = new Config();- Use secrets in application:
File: src/app.js
const express = require('express');
const config = require('./config');
const { OpenAI } = require('openai');
const app = express();
// Initialize OpenAI with key from Key Vault
let openaiClient;
async function initializeServices() {
const openaiKey = await config.getOpenAIKey();
openaiClient = new OpenAI({ apiKey: openaiKey });
console.log('✅ Services initialized with secrets from Key Vault');
}
// Call on startup
initializeServices().catch(console.error);
app.post('/chat', async (req, res) => {
try {
const completion = await openaiClient.chat.completions.create({
model: 'gpt-4.1',
messages: [{ role: 'user', content: 'Hello!' }]
});
res.json({
response: completion.choices[0].message.content,
authentication: 'Key from Key Vault via Managed Identity'
});
} catch (error) {
res.status(500).json({ error: error.message });
}
});
app.listen(3000, () => {
console.log('Secure API with Key Vault integration running');
});- Deploy and test:
azd up
# Test that API keys work
curl -X POST https://myapp.azurecontainerapps.io/chat \
-H "Content-Type: application/json" \
-d '{"message":"Hello AI"}'✅ Success Criteria:
- ✅ No API keys in code or environment variables
- ✅ Application retrieves keys from Key Vault
- ✅ Third-party APIs work correctly
- ✅ Can rotate keys without code changes
Rotate a secret:
# Update secret in Key Vault
az keyvault secret set \
--vault-name $KV_NAME \
--name "OpenAI-ApiKey" \
--value "sk-proj-NEW_KEY_HERE"
# Restart app to pick up new key
az containerapp revision restart \
--name myapp \
--resource-group rg-myappTime: 25-35 minutes
Test your understanding:
-
Q1: What are the three main authentication patterns?
- A: Connection strings (legacy), Key Vault references (transition), Managed Identity (best)
-
Q2: Why is managed identity better than connection strings?
- A: No secrets in code, automatic rotation, full audit trail, RBAC permissions
-
Q3: When would you use user-assigned identity instead of system-assigned?
- A: When sharing identity across multiple resources or when identity lifecycle is independent of resource lifecycle
Hands-On Verification:
# Check what type of identity your app uses
az containerapp show \
--name myapp \
--resource-group rg-myapp \
--query "identity.type"
# List all role assignments for the identity
az role assignment list \
--assignee $(az containerapp show --name myapp --resource-group rg-myapp --query "identity.principalId" -o tsv)Test your understanding:
-
Q1: What's the role ID for "Storage Blob Data Contributor"?
- A:
ba92f5b4-2d11-453d-a403-e96b0029c9fe
- A:
-
Q2: What permissions does "Key Vault Secrets User" provide?
- A: Read-only access to secrets (cannot create, update, or delete)
-
Q3: How do you grant a Container App access to Azure SQL?
- A: Assign "SQL DB Contributor" role or configure Azure AD authentication for SQL
Hands-On Verification:
# Find specific role
az role definition list --name "Storage Blob Data Contributor"
# Check what roles are assigned to your identity
PRINCIPAL_ID=$(az containerapp show --name myapp --resource-group rg-myapp --query "identity.principalId" -o tsv)
az role assignment list --assignee $PRINCIPAL_ID --output tableTest your understanding:
-
Q1: How do you enable RBAC for Key Vault instead of access policies?
- A: Set
enableRbacAuthorization: truein Bicep
- A: Set
-
Q2: What Azure SDK library handles managed identity authentication?
- A:
@azure/identitywithDefaultAzureCredentialclass
- A:
-
Q3: How long do Key Vault secrets stay in cache?
- A: Application-dependent; implement your own caching strategy
Hands-On Verification:
# Test Key Vault access
az keyvault secret show \
--vault-name $KV_NAME \
--name "OpenAI-ApiKey" \
--query "value"
# Check RBAC is enabled
az keyvault show \
--name $KV_NAME \
--query "properties.enableRbacAuthorization"
# ✅ Expected: true-
Always use managed identity in production
identity: { type: 'SystemAssigned' }
-
Use least-privilege RBAC roles
- Use "Reader" roles when possible
- Avoid "Owner" or "Contributor" unless necessary
-
Store third-party keys in Key Vault
const apiKey = await secretClient.getSecret('ThirdPartyApiKey');
-
Enable audit logging
diagnosticSettings: { logs: [{ category: 'AuditEvent', enabled: true }] }
-
Use different identities for dev/staging/prod
azd env new dev azd env new staging azd env new prod
-
Rotate secrets regularly
- Set expiration dates on Key Vault secrets
- Automate rotation with Azure Functions
-
Never hardcode secrets
// ❌ BAD const apiKey = "sk-proj-xxxxxxxxxxxxx";
-
Don't use connection strings in production
// ❌ BAD BlobServiceClient.fromConnectionString(process.env.STORAGE_CONNECTION_STRING)
-
Don't grant excessive permissions
// ❌ BAD - too much access roleDefinitionId: 'Owner' // ✅ GOOD - least privilege roleDefinitionId: 'Storage Blob Data Reader'
-
Don't log secrets
// ❌ BAD console.log('API Key:', apiKey); // ✅ GOOD console.log('API Key retrieved successfully');
-
Don't share production identities across environments
// ❌ BAD - same identity for dev and prod // ✅ GOOD - separate identities per environment
Symptoms:
Error: Unauthorized (403)
AuthorizationPermissionMismatch: This request is not authorized to perform this operation
Diagnosis:
# Check if managed identity is enabled
az containerapp show \
--name myapp \
--resource-group rg-myapp \
--query "identity.type"
# ✅ Expected: "SystemAssigned" or "UserAssigned"
# Check role assignments
PRINCIPAL_ID=$(az containerapp show --name myapp --resource-group rg-myapp --query "identity.principalId" -o tsv)
az role assignment list --assignee $PRINCIPAL_ID
# Expected: Should see "Storage Blob Data Contributor" or similar roleSolutions:
- Grant correct RBAC role:
STORAGE_ID=$(az storage account show --name mystorageaccount --resource-group rg-myapp --query "id" -o tsv)
az role assignment create \
--assignee $PRINCIPAL_ID \
--role "Storage Blob Data Contributor" \
--scope $STORAGE_ID- Wait for propagation (can take 5-10 minutes):
# Check role assignment status
az role assignment list --assignee $PRINCIPAL_ID --scope $STORAGE_ID- Verify application code uses correct credential:
// Make sure you're using DefaultAzureCredential
const credential = new DefaultAzureCredential();Symptoms:
Error: Forbidden (403)
The user, group or application does not have secrets get permission
Diagnosis:
# Check Key Vault RBAC is enabled
az keyvault show \
--name $KV_NAME \
--query "properties.enableRbacAuthorization"
# ✅ Expected: true
# Check role assignments
az role assignment list \
--assignee $PRINCIPAL_ID \
--scope /subscriptions/{sub-id}/resourceGroups/rg-myapp/providers/Microsoft.KeyVault/vaults/$KV_NAMESolutions:
- Enable RBAC on Key Vault:
az keyvault update \
--name $KV_NAME \
--enable-rbac-authorization true- Grant Key Vault Secrets User role:
KV_ID=$(az keyvault show --name $KV_NAME --query "id" -o tsv)
az role assignment create \
--assignee $PRINCIPAL_ID \
--role "Key Vault Secrets User" \
--scope $KV_IDSymptoms:
Error: DefaultAzureCredential failed to retrieve a token
CredentialUnavailableError: No credential available
Diagnosis:
# Check if you're logged in
az account show
# Check Azure CLI authentication
az ad signed-in-user showSolutions:
- Login to Azure CLI:
az login- Set Azure subscription:
az account set --subscription "Your Subscription Name"- For local development, use environment variables:
export AZURE_TENANT_ID="your-tenant-id"
export AZURE_CLIENT_ID="your-client-id"
export AZURE_CLIENT_SECRET="your-client-secret"- Or use different credential locally:
const { DefaultAzureCredential, AzureCliCredential } = require('@azure/identity');
// Use AzureCliCredential for local dev
const credential = process.env.NODE_ENV === 'production'
? new DefaultAzureCredential()
: new AzureCliCredential();Symptoms:
- Role assigned successfully
- Still getting 403 errors
- Intermittent access (sometimes works, sometimes doesn't)
Explanation: Azure RBAC changes can take 5-10 minutes to propagate globally.
Solution:
# Wait and retry
echo "Waiting for RBAC propagation..."
sleep 300 # Wait 5 minutes
# Test access
curl https://myapp.azurecontainerapps.io/upload
# If still failing, restart the app
az containerapp revision restart \
--name myapp \
--resource-group rg-myapp| Resource | Cost |
|---|---|
| Managed Identity | 🆓 FREE - No charge |
| RBAC Role Assignments | 🆓 FREE - No charge |
| Azure AD Token Requests | 🆓 FREE - Included |
| Key Vault Operations | $0.03 per 10,000 operations |
| Key Vault Storage | $0.024 per secret per month |
Managed identity saves money by:
- ✅ Eliminating Key Vault operations for service-to-service auth
- ✅ Reducing security incidents (no leaked credentials)
- ✅ Decreasing operational overhead (no manual rotation)
Example Cost Comparison (monthly):
| Scenario | Connection Strings | Managed Identity | Savings |
|---|---|---|---|
| Small app (1M requests) | ~$50 (Key Vault + ops) | ~$0 | $50/month |
| Medium app (10M requests) | ~$200 | ~$0 | $200/month |
| Large app (100M requests) | ~$1,500 | ~$0 | $1,500/month |
- ← Previous: Configuration Management
- → Next: First Project
- 🏠 Course Home
- Microsoft Foundry Models Chat Example - Uses managed identity for Microsoft Foundry Models
- Microservices Example - Multi-service authentication patterns
You've learned:
- ✅ Three authentication patterns (connection strings, Key Vault, managed identity)
- ✅ How to enable and configure managed identity in AZD
- ✅ RBAC role assignments for Azure services
- ✅ Key Vault integration for third-party secrets
- ✅ User-assigned vs system-assigned identities
- ✅ Security best practices and troubleshooting
Key Takeaways:
- Always use managed identity in production - Zero secrets, automatic rotation
- Use least-privilege RBAC roles - Grant only necessary permissions
- Store third-party keys in Key Vault - Centralized secret management
- Separate identities per environment - Dev, staging, prod isolation
- Enable audit logging - Track who accessed what
Next Steps:
- Complete the practical exercises above
- Migrate an existing app from connection strings to managed identity
- Build your first AZD project with security from day one: First Project