Secrets, Managed Identity & Config

Why this matters

Real apps need connection strings, API keys, and registry credentials — and none of them belong in plaintext env vars or your shell history. Container Apps gives you app-scoped secrets, Key Vault references resolved at runtime, and managed identities so your app can authenticate to Azure services without any secret at all. This is also where a single typo — a bad secret reference or a missing role assignment — turns into a crash loop, so learning to diagnose it matters as much as setting it up.

Concepts

App secrets and secret references

A container app can hold named secrets (--secrets name=value), stored encrypted by the platform, not visible in the plain template. You then reference a secret from an environment variable using the secretref: syntax: --env-vars "DB_CONN=secretref:db-conn" injects the value of the secret named db-conn into the DB_CONN env var at runtime. This is the same pattern as a Kubernetes Secret referenced by a pod's valueFrom.secretKeyRef — you already know the shape. Secrets are also what scale-rule auth and registry auth point at (you saw --scale-rule-auth connection=queue-conn in module 03).

Key Vault references

Better than storing the secret value in the app: store it in Azure Key Vault and have the app reference it. A secret's value can be a Key Vault secret URI, and ACA resolves it at runtime using the app's managed identity. So the app config contains only a pointer (the Key Vault secret URI) and an identity — the actual secret never sits in the container app resource. Rotating the secret in Key Vault propagates without changing the app. This is the recommended pattern for anything sensitive.

Managed identity: system-assigned vs user-assigned

A managed identity is an Azure AD (Entra) identity your app is, so it can call Azure services (Key Vault, Storage, ACR, Service Bus) using RBAC instead of a stored credential. System-assigned identity is created with the app, tied to its lifecycle, and unique to it (--system-assigned). User-assigned identity is a standalone resource you create once and attach to many apps (--user-assigned <id>) — better when several apps need the same permissions or you want the identity to outlive any one app. Conceptually this is like a Kubernetes ServiceAccount bound to a cloud IAM role (workload identity), but managed for you.

Role assignments: the part everyone forgets

Having an identity is necessary but not sufficient — the identity needs RBAC role assignments granting it access to the target resource. To read Key Vault secrets, the app's identity needs a role like Key Vault Secrets User on the vault (or an access policy, on vaults using the legacy model). To pull from ACR, it needs AcrPull on the registry. Miss the role assignment and the identity authenticates but is denied — the app crash-loops or fails to start with an authorization error, not a "missing secret" error. The diagnosis skill is recognizing "403/forbidden from the identity" as "missing role assignment," not "wrong secret."

Registry authentication with managed identity

Pulling a private image is a special case of the same idea. Instead of storing registry username/password as secrets, you attach a managed identity with AcrPull on the Azure Container Registry and tell the app to use it for pulls (az containerapp registry set --identity <system-or-user-id>). No registry password is ever stored on the app. If the role assignment is missing, the very first thing that fails is the image pull — the revision never leaves provisioning.

Command reference

Command What it does Example
az containerapp secret set Add/update app secrets az containerapp secret set --name web --resource-group rg-aca-m06 --secrets "db-conn=Server=..."
az containerapp secret list List secret names (not values) az containerapp secret list --name web --resource-group rg-aca-m06 -o table
az containerapp identity assign Attach a managed identity az containerapp identity assign --name web --resource-group rg-aca-m06 --system-assigned
az containerapp identity show Show the app's identities az containerapp identity show --name web --resource-group rg-aca-m06 -o jsonc
az keyvault create / az keyvault secret set Create a vault / store a secret az keyvault secret set --vault-name kv-m06 --name db-conn --value "Server=..."
az role assignment create Grant the identity a role az role assignment create --assignee <principalId> --role "Key Vault Secrets User" --scope <vaultId>
az containerapp registry set Configure ACR pull auth az containerapp registry set --name web --resource-group rg-aca-m06 --server myacr.azurecr.io --identity system

Flag-by-flag breakdowns:

az containerapp secret set --name web --resource-group rg-aca-m06 --secrets "db-conn=Server=..." "api-key=abc123"

  • --secrets — space-separated name=value pairs; stored encrypted, referenced later via secretref:.

az containerapp identity assign --name web --resource-group rg-aca-m06 --system-assigned

  • --system-assigned — create/attach a system-assigned identity (lifecycle tied to the app). Use --user-assigned <resourceId> instead to attach a standalone identity.

az role assignment create --assignee <principalId> --role "Key Vault Secrets User" --scope <vaultId>

  • --assignee <principalId> — the identity's principal (object) ID; for a system-assigned identity get it from identity show --query principalId.
  • --role "Key Vault Secrets User" — the RBAC role granting secret-read.
  • --scope <vaultId> — the resource the grant applies to (the Key Vault). Scope narrowly, not at subscription level.

az containerapp update --name web --resource-group rg-aca-m06 --set-env-vars "DB_CONN=secretref:db-conn"

  • --set-env-vars "DB_CONN=secretref:db-conn" — inject the secret named db-conn into env var DB_CONN at runtime. A typo in the secret name (secretref:db-con) yields an unresolved reference and often a crash loop.

az containerapp registry set --name web --resource-group rg-aca-m06 --server myacr.azurecr.io --identity system

  • --server — the ACR login server.
  • --identity system — use the system-assigned identity to authenticate pulls (needs AcrPull on the registry). Use a user-assigned identity's resource ID for --identity instead if applicable.

Hands-on exercises

  1. Set up group, Environment, app.

    az group create --name rg-aca-m06 --location eastus
    az containerapp env create --name env-m06 --resource-group rg-aca-m06 --location eastus
    az containerapp create --name web --resource-group rg-aca-m06 --environment env-m06 `
      --image mcr.microsoft.com/k8se/quickstart:latest --target-port 80 --ingress external
    
  2. Set a plain secret and reference it.

    az containerapp secret set --name web --resource-group rg-aca-m06 --secrets "greeting=hello-from-secret"
    az containerapp update --name web --resource-group rg-aca-m06 --set-env-vars "GREETING=secretref:greeting"
    az containerapp secret list --name web --resource-group rg-aca-m06 -o table
    

    Verify the secret name lists (never the value), and the env var references it. Confirm the app is still running after the update.

  3. Enable a system-assigned identity.

    az containerapp identity assign --name web --resource-group rg-aca-m06 --system-assigned
    $pid = az containerapp identity show --name web --resource-group rg-aca-m06 --query principalId -o tsv
    $pid
    

    Verify a principal (object) ID prints.

  4. Create a Key Vault and store a secret.

    $kv = "kvm06$((Get-Random -Max 9999))"
    az keyvault create --name $kv --resource-group rg-aca-m06 --location eastus --enable-rbac-authorization true
    az keyvault secret set --vault-name $kv --name db-conn --value "Server=example;Password=p@ss"
    $vaultId = az keyvault show --name $kv --resource-group rg-aca-m06 --query id -o tsv
    
  5. Grant the app's identity read access to the vault.

    az role assignment create --assignee $pid --role "Key Vault Secrets User" --scope $vaultId
    

    Verify with az role assignment list --assignee $pid --scope $vaultId -o table — expect the role to appear. Role propagation can take a minute or two.

  6. Reference the Key Vault secret from the app.

    $secretUri = az keyvault secret show --vault-name $kv --name db-conn --query id -o tsv
    az containerapp secret set --name web --resource-group rg-aca-m06 `
      --secrets "db-conn=keyvaultref:$secretUri,identityref:system"
    az containerapp update --name web --resource-group rg-aca-m06 --set-env-vars "DB_CONN=secretref:db-conn"
    

    Verify the app starts a new revision and stays healthy — the value came from Key Vault via the identity, never stored in the app. (Syntax for the Key Vault reference secret may vary slightly by CLI version; confirm with az containerapp secret set --help.)

  7. Use a user-assigned identity instead (contrast).

    az identity create --name uami-m06 --resource-group rg-aca-m06
    $uamiId = az identity show --name uami-m06 --resource-group rg-aca-m06 --query id -o tsv
    $uamiPid = az identity show --name uami-m06 --resource-group rg-aca-m06 --query principalId -o tsv
    az containerapp identity assign --name web --resource-group rg-aca-m06 --user-assigned $uamiId
    az role assignment create --assignee $uamiPid --role "Key Vault Secrets User" --scope $vaultId
    

    Verify both identities now show on the app (az containerapp identity show ... -o jsonc). Note the user-assigned one could be attached to other apps too.

  8. Diagnose and fix: secret reference typo → crash loop. Break it deliberately:

    az containerapp update --name web --resource-group rg-aca-m06 --set-env-vars "DB_CONN=secretref:db-con"
    az containerapp revision list --name web --resource-group rg-aca-m06 -o table
    

    The new revision references a secret name that doesn't exist (db-con vs db-conn); the container fails to resolve it and can crash-loop or fail provisioning. Diagnose by listing secret names (secret list) and comparing to the secretref: value. Fix by updating with the correct name secretref:db-conn and confirm the revision goes healthy.

  9. Diagnose and fix: missing role assignment. Remove the role and observe the failure mode:

    az role assignment delete --assignee $pid --role "Key Vault Secrets User" --scope $vaultId
    az containerapp update --name web --resource-group rg-aca-m06 --set-env-vars "TRIGGER=rev$((Get-Random))"
    az containerapp logs show --name web --resource-group rg-aca-m06 --tail 40
    

    The identity now authenticates but is denied reading the Key Vault secret — a forbidden/authorization error, not a "missing secret" error. Fix by re-creating the role assignment (exercise 5) and rolling a fresh revision; wait for RBAC propagation. Lesson: identity present + role missing = authorization failure.

  10. Cleanup.

    az group delete --name rg-aca-m06 --yes --no-wait
    

    (Deletes the Key Vault too. Note: soft-delete may retain the vault name for a while; that's expected and free.)

Independent challenge

Deploy an app that reads a sensitive value from Key Vault using a user-assigned managed identity (not system-assigned), with the correct role assignment scoped to just the vault — and prove the secret value never appears in the app's plaintext config. Then deliberately introduce a missing role assignment, capture the exact failure signature in the logs, and fix it. Combine this module with module 05: do the fix as a new revision and use traffic splitting to keep the last-good revision serving while the fixed one comes up. Clean up the resource group afterward (the Key Vault is billable-ish and soft-deletes on removal).

Stuck? One hint

The tell for "missing role assignment" vs "wrong secret" is in the logs/ provisioning error: a role problem reads as forbidden/authorization/denied from the identity against the vault, while a bad secretref: reads as an unresolved/unknown secret. Check az role assignment list --assignee <pid> --scope <vaultId> — an empty list is your smoking gun.

Common mistakes & troubleshooting

  • Secret name typos in secretref:. The reference must exactly match a secret name (secret list). A typo yields an unresolved value and often a crash loop — with a confusing error that doesn't say "typo."
  • Identity without a role. Attaching an identity does nothing on its own; it needs an RBAC role assignment on the target resource. Symptom is a forbidden error, not a missing-credential error.
  • RBAC propagation lag. Role assignments can take a couple of minutes to take effect; rolling a revision immediately after granting can still fail. Wait and retry.
  • Wrong scope on the role. Granting at subscription scope "to be safe" over-permissions the identity; scope to the specific vault/registry.
  • RBAC vs access-policy Key Vaults. On an access-policy vault, roles won't work — you set an access policy instead. Prefer RBAC-authorization vaults (--enable-rbac-authorization true) for consistency.
  • Cost pitfall. Key Vault operations and (for private setups) any networking you add are billable, and a forgotten Environment still carries its Log Analytics workspace. Delete the whole resource group.

Checkpoint quiz

Write down your answer to each question before expanding it — checking without attempting first is the single easiest way to fool yourself into thinking you've learned this.

  1. What's the secretref: syntax for and what Kubernetes construct is it like?
  2. Why is a Key Vault reference better than storing the secret value directly on the app?
  3. System-assigned vs user-assigned identity — give one reason to pick each.
  4. You attached a managed identity and referenced a Key Vault secret, but the app crash-loops with a "forbidden" error. What's missing?
  5. How do you get the principal ID needed for a role assignment on a system-assigned identity?
  6. How does an app pull from a private ACR without storing a registry password?
  7. You fixed a missing role assignment but the next revision still fails immediately. Most likely reason?
Show answers
  1. It injects the value of a named app secret into an environment variable at runtime; like a pod referencing a Kubernetes Secret via valueFrom.secretKeyRef.
  2. The app config holds only a pointer (the Key Vault secret URI) resolved via managed identity — the secret value never lives in the app resource, and rotating it in Key Vault propagates without editing the app.
  3. System-assigned: simplest, lifecycle tied to the app, unique to it. User-assigned: reusable across multiple apps and outlives any single app; good for shared permissions.
  4. A role assignment granting that identity access to the vault (e.g. Key Vault Secrets User scoped to the vault). Identity present but unauthorized = forbidden.
  5. az containerapp identity show --name <app> --resource-group <rg> --query principalId -o tsv.
  6. Attach a managed identity with AcrPull on the registry and configure az containerapp registry set --identity <system|user-id>; no password is stored.
  7. RBAC propagation lag — the new assignment hasn't taken effect yet. Wait a minute or two and roll another revision.

Next

07-monitoring-and-log-analytics — see what your apps are actually doing: console vs system logs, live streaming, KQL queries, metrics, and alerts.