Skip to main content

Challenge 09: Secure Azure AI Resources

Estimated Time

45-60 min | Cost: ~$1.00 (Key Vault, Private Endpoint) | Domain: Plan & Manage AI Solutions (20-25%)

Exam skills covered

  • Manage and protect account keys
  • Manage authentication for Azure AI services
  • Configure network security for Azure AI resources
  • Implement managed identity for secure access

Overview

Securing Azure AI resources involves multiple layers: protecting access keys, implementing network isolation, using managed identities for keyless authentication, and enforcing role-based access control. A compromise of AI service keys can lead to unauthorized usage, data exfiltration, and significant financial impact.

In this challenge, you'll implement a comprehensive security posture for Azure AI services. You'll store keys in Azure Key Vault, implement zero-downtime key rotation, configure network rules with private endpoints, and transition from key-based authentication to managed identity — the recommended approach for production workloads.

The defense-in-depth approach combines identity (managed identity + RBAC), network (private endpoints + IP rules), and secrets management (Key Vault + rotation) to create a robust security boundary around your AI services.

Architecture

The secure architecture uses Key Vault for secrets management, managed identity for authentication, and private endpoints for network isolation.

Challenge 09 topology

Prerequisites

  • Azure subscription with Contributor role
  • Azure CLI installed
  • An Azure AI services resource (or will create one)
  • Permissions to create Key Vault and Private Endpoints
  • A virtual network (or will create one)

Implementation

Task 1: Store AI Service Key in Azure Key Vault

from azure.identity import DefaultAzureCredential
from azure.keyvault.secrets import SecretClient
from azure.mgmt.cognitiveservices import CognitiveServicesManagementClient
import os

credential = DefaultAzureCredential()
subscription_id = "<your-subscription-id>"

# Get AI services key
cs_client = CognitiveServicesManagementClient(credential, subscription_id)
keys = cs_client.accounts.list_keys(
resource_group_name="rg-ai102-challenge09",
account_name="ai-secure-demo"
)

# Store key in Key Vault
vault_url = "https://kv-ai102-secure.vault.azure.net/"
secret_client = SecretClient(vault_url=vault_url, credential=credential)

# Store both keys for rotation purposes
secret_client.set_secret(
name="ai-services-key1",
value=keys.key1,
content_type="text/plain",
tags={"service": "cognitive-services", "key-number": "1"}
)

secret_client.set_secret(
name="ai-services-key2",
value=keys.key2,
content_type="text/plain",
tags={"service": "cognitive-services", "key-number": "2"}
)

# Store endpoint
secret_client.set_secret(
name="ai-services-endpoint",
value="https://ai-secure-demo.cognitiveservices.azure.com/",
content_type="text/plain",
tags={"service": "cognitive-services"}
)

print("Secrets stored in Key Vault:")
print(f" ai-services-key1: ****{keys.key1[-4:]}")
print(f" ai-services-key2: ****{keys.key2[-4:]}")
print(f" ai-services-endpoint: stored")

# Retrieve key from Key Vault for use
retrieved_key = secret_client.get_secret("ai-services-key1")
print(f"\nRetrieved key from vault: ****{retrieved_key.value[-4:]}")

Task 2: Implement Zero-Downtime Key Rotation

from azure.identity import DefaultAzureCredential
from azure.keyvault.secrets import SecretClient
from azure.mgmt.cognitiveservices import CognitiveServicesManagementClient
import time

credential = DefaultAzureCredential()
subscription_id = "<your-subscription-id>"

cs_client = CognitiveServicesManagementClient(credential, subscription_id)
secret_client = SecretClient(
vault_url="https://kv-ai102-secure.vault.azure.net/",
credential=credential
)

resource_group = "rg-ai102-challenge09"
account_name = "ai-secure-demo"

def rotate_key_zero_downtime(key_to_rotate: str = "Key1"):
"""
Zero-downtime key rotation strategy:
1. Applications use Key1 (active)
2. Regenerate Key2 (inactive) → new Key2
3. Update applications to use Key2
4. Regenerate Key1 → new Key1
5. Applications now use Key2, Key1 is fresh backup
"""
print(f"=== Starting Zero-Downtime Key Rotation ===")

# Step 1: Determine which key is currently active
active_secret = secret_client.get_secret("ai-services-active-key")
active_key_name = active_secret.value # "key1" or "key2"
inactive_key_name = "key2" if active_key_name == "key1" else "key1"
print(f"Step 1: Active key is {active_key_name}")

# Step 2: Regenerate the INACTIVE key
print(f"Step 2: Regenerating inactive {inactive_key_name}...")
regenerated = cs_client.accounts.regenerate_key(
resource_group_name=resource_group,
account_name=account_name,
parameters={"keyName": inactive_key_name.capitalize()}
)
new_key_value = regenerated.key2 if inactive_key_name == "key2" else regenerated.key1
print(f" New {inactive_key_name} generated: ****{new_key_value[-4:]}")

# Step 3: Update Key Vault with new inactive key
print(f"Step 3: Updating Key Vault with new {inactive_key_name}...")
secret_client.set_secret(
f"ai-services-{inactive_key_name}",
new_key_value
)

# Step 4: Switch active key pointer to the newly regenerated key
print(f"Step 4: Switching active key to {inactive_key_name}...")
secret_client.set_secret("ai-services-active-key", inactive_key_name)

# Step 5: Allow time for applications to pick up new key
print("Step 5: Waiting for cache expiry (applications refresh)...")
time.sleep(5) # In production: wait for app cache TTL

# Step 6: Now regenerate the old active key (it's no longer in use)
print(f"Step 6: Regenerating old active {active_key_name}...")
cs_client.accounts.regenerate_key(
resource_group_name=resource_group,
account_name=account_name,
parameters={"keyName": active_key_name.capitalize()}
)

print(f"\n✓ Rotation complete. Active key: {inactive_key_name}")

# Initialize active key tracking
secret_client.set_secret("ai-services-active-key", "key1")

# Perform rotation
rotate_key_zero_downtime()

Task 3: Configure Network Security and Private Endpoint

from azure.identity import DefaultAzureCredential
from azure.mgmt.cognitiveservices import CognitiveServicesManagementClient
from azure.mgmt.network import NetworkManagementClient

credential = DefaultAzureCredential()
subscription_id = "<your-subscription-id>"
resource_group = "rg-ai102-challenge09"

cs_client = CognitiveServicesManagementClient(credential, subscription_id)
network_client = NetworkManagementClient(credential, subscription_id)

# Step 1: Configure network rules - deny public access by default
print("Configuring network rules...")
cs_client.accounts.begin_update(
resource_group_name=resource_group,
account_name="ai-secure-demo",
account={
"properties": {
"publicNetworkAccess": "Disabled",
"networkAcls": {
"defaultAction": "Deny",
"ipRules": [
{"value": "203.0.113.0/24"} # Allow specific IP range
],
"virtualNetworkRules": []
}
}
}
).result()
print(" Public access disabled, IP whitelist configured")

# Step 2: Create VNet and Subnet for Private Endpoint
print("\nCreating VNet and subnet...")
vnet = network_client.virtual_networks.begin_create_or_update(
resource_group_name=resource_group,
virtual_network_name="vnet-ai102",
parameters={
"location": "eastus",
"properties": {
"addressSpace": {"addressPrefixes": ["10.0.0.0/16"]},
"subnets": [
{
"name": "snet-ai-services",
"properties": {
"addressPrefix": "10.0.1.0/24",
"privateEndpointNetworkPolicies": "Disabled"
}
}
]
}
}
).result()
print(f" VNet created: {vnet.name}")

# Step 3: Create Private Endpoint
print("\nCreating Private Endpoint...")
ai_resource_id = (
f"/subscriptions/{subscription_id}/resourceGroups/{resource_group}"
f"/providers/Microsoft.CognitiveServices/accounts/ai-secure-demo"
)

pe = network_client.private_endpoints.begin_create_or_update(
resource_group_name=resource_group,
private_endpoint_name="pe-ai-secure",
parameters={
"location": "eastus",
"properties": {
"subnet": {
"id": f"{vnet.id}/subnets/snet-ai-services"
},
"privateLinkServiceConnections": [
{
"name": "ai-services-connection",
"properties": {
"privateLinkServiceId": ai_resource_id,
"groupIds": ["account"]
}
}
]
}
}
).result()
print(f" Private Endpoint created: {pe.name}")
print(f" Private IP: {pe.custom_dns_configs[0].ip_addresses[0] if pe.custom_dns_configs else 'pending'}")

Task 4: Assign RBAC Roles and Configure Managed Identity

from azure.identity import DefaultAzureCredential, ManagedIdentityCredential
from azure.mgmt.authorization import AuthorizationManagementClient
from azure.mgmt.msi import ManagedServiceIdentityClient
from azure.ai.textanalytics import TextAnalyticsClient
import os
import uuid

credential = DefaultAzureCredential()
subscription_id = "<your-subscription-id>"
resource_group = "rg-ai102-challenge09"

auth_client = AuthorizationManagementClient(credential, subscription_id)

ai_resource_id = (
f"/subscriptions/{subscription_id}/resourceGroups/{resource_group}"
f"/providers/Microsoft.CognitiveServices/accounts/ai-secure-demo"
)

# Create a user-assigned managed identity
msi_client = ManagedServiceIdentityClient(credential, subscription_id)
identity = msi_client.user_assigned_identities.create_or_update(
resource_group_name=resource_group,
resource_name="id-ai-services-reader",
parameters={"location": "eastus"}
)
print(f"Managed Identity created: {identity.name}")
print(f" Client ID: {identity.client_id}")
print(f" Principal ID: {identity.principal_id}")

# Assign "Cognitive Services User" role to the managed identity
# This role allows calling AI service APIs without needing keys
COGNITIVE_SERVICES_USER_ROLE = "a97b65f3-24c7-4388-baec-2e87135dc908"

role_assignment = auth_client.role_assignments.create(
scope=ai_resource_id,
role_assignment_name=str(uuid.uuid4()),
parameters={
"properties": {
"roleDefinitionId": f"/subscriptions/{subscription_id}/providers/Microsoft.Authorization/roleDefinitions/{COGNITIVE_SERVICES_USER_ROLE}",
"principalId": identity.principal_id,
"principalType": "ServicePrincipal"
}
}
)
print(f"\nRole assigned: Cognitive Services User")
print(f" Scope: {ai_resource_id}")

# Use managed identity to authenticate (no keys needed!)
# In a VM or App Service with the identity assigned:
endpoint = os.environ["AZURE_AI_ENDPOINT"]

# With managed identity - no key required
mi_credential = ManagedIdentityCredential(client_id=identity.client_id)
client = TextAnalyticsClient(
endpoint=endpoint,
credential=mi_credential # Uses token-based auth, not API key
)

# This is the recommended production pattern
documents = ["Azure AI services support managed identity authentication."]
response = client.detect_language(documents=documents)
for doc in response:
if not doc.is_error:
print(f"\nLanguage detected: {doc.primary_language.name}")
print(" ✓ Authenticated via Managed Identity (keyless)")

Expected Output

Secrets stored in Key Vault:
ai-services-key1: ****a1b2
ai-services-key2: ****c3d4
ai-services-endpoint: stored

=== Starting Zero-Downtime Key Rotation ===
Step 1: Active key is key1
Step 2: Regenerating key2...
New key2 generated: ****x7y8
Step 3: Updating Key Vault with new key2...
Step 4: Switching active key to key2...
Step 5: Waiting for cache expiry...
Step 6: Regenerating old key1...
✓ Rotation complete. Active key: key2

Managed Identity created: id-ai-services-reader
Client ID: 12345678-abcd-efgh-ijkl-123456789012
Principal ID: 87654321-dcba-hgfe-lkji-210987654321
Role assigned: Cognitive Services User

Language detected: English
✓ Authenticated via Managed Identity (keyless)

Break & fix

ScenarioSymptomRoot CauseFix
Key Vault access denied403 Forbidden when reading secretsMissing Key Vault RBAC role or access policyAssign Key Vault Secrets User role to the calling identity
Private endpoint not resolvingDNS resolution returns public IPMissing Private DNS Zone or VNet linkCreate privatelink.cognitiveservices.azure.com DNS zone and link to VNet
Managed identity auth fails401 "InvalidAuthenticationToken"RBAC role assignment not propagated (up to 5 min)Wait 5 minutes for role assignment propagation; verify correct principalId
Key rotation causes downtimeRequests fail with 401 during rotationApplication caches keys and doesn't refreshImplement key caching with TTL; rotate inactive key first (two-key strategy)
Network rule blocks legitimate traffic403 from allowed IP addressIP is behind NAT/proxy with different egress IPAdd the actual egress IP to the allowlist; use curl ifconfig.me to find it

Knowledge Check

1. Which Azure RBAC role allows an identity to call Azure AI service APIs without requiring an API key?

2. During zero-downtime key rotation, which key should you regenerate FIRST?

3. What DNS zone name is required for private endpoint resolution of Azure Cognitive Services?

4. What is the recommended authentication method for production Azure AI workloads running in Azure?

5. When you set 'publicNetworkAccess' to 'Disabled' on an Azure AI resource, what happens to existing API key-based requests from the internet?

Cleanup

az group delete --name rg-ai102-challenge09 --yes --no-wait

Learn More