Infrastructure

Azure App Service: diagnose a Managed Identity token failure before bringing back a client secret

A production runbook for qualifying an Azure App Service Managed Identity failure with IMDS endpoint, Entra ID, RBAC, Key Vault or target API evidence, logs, validation and rollback before reintroducing a client secret.

16 Jul 2026 azureapp-servicemanaged-identityidentityentra-idkey-vaultobservabilitykqlautomationrunbookrollbackproduction

When an Azure App Service application can no longer obtain a token through Managed Identity, the fastest recovery idea is often to put a client secret back into configuration. It may restore the call, but it also turns an operations incident into security debt: a secret to store, rotate, monitor, scope and eventually remove, while the original identity failure remains unclear.

The use case is an API running on App Service that calls Key Vault, Storage, Microsoft Graph or an internal API protected by Entra ID. After a deployment, identity rotation, RBAC change or slot swap, the application starts returning 401, 403, CredentialUnavailableException, ManagedIdentityCredential authentication unavailable or token acquisition timeouts. The runbook goal is to decide whether to fix the App Service identity, the target authorization scope, the credential chain, the target service, or use a tightly bounded temporary workaround.

Freeze the expected identity contract

Start by writing down the identity that should have acted. A managed identity is not only a portal toggle. It defines an Entra principal, a calling resource, a token audience, an authorization scope and an audit trail on the target service.

text managed-identity-contract.txt
Application
App Service: app-orders-prod
Slot: production or staging
Resource group: rg-prod-app
Runtime: production code, WebJob or Function in the same plan

Expected identity
Type: system-assigned or user-assigned
Expected client ID when user-assigned
Expected object ID / principal ID
Token audience: https://vault.azure.net, https://storage.azure.com or api://...
Target resource: Key Vault, Storage, internal API, Graph or third-party service
Expected permission: RBAC, access policy, app role or API scope

Questions before bringing back a client secret
Is the identity still enabled on the right slot ?
Does the code request a token for the right audience ?
Does the principal have access on the exact target scope ?
Is the target rejecting the identity or is no token being issued ?
Did a slot, role assignment, Key Vault, API or SDK change just happen ?

If this contract is not explicit, a client secret will hide the outage without explaining which runtime identity was supposed to own the action.

Separate token acquisition from target authorization

A 403 from Key Vault or an API does not prove Managed Identity is broken. It may mean the token was issued correctly, but the target rejected the principal, role, scope, audience or network condition.

text identity-failure-classification.txt
Failure families
Token unavailable
  Managed Identity endpoint unreachable
  Identity disabled on App Service or on the slot
  Wrong client ID for a user-assigned identity
  Credential chain ignores ManagedIdentityCredential

Token issued but target rejects the call
  Role assignment missing or at the wrong scope
  Key Vault access policy still in use
  App role not assigned on the internal API
  Token audience is wrong
  RBAC propagation or service-side cache delay

Application or network failure
  App calls the wrong URL
  DNS or route to private API fails
  TLS, proxy or firewall breaks the path
  Code reuses an expired token or bad cache entry

Blocker
Reintroducing a client secret before locating the failure family

This separation avoids two common mistakes: widening target permissions while no token is being requested, or adding a secret when only a target-side role is missing.

Capture the identity exposed by App Service

First verify the real application and slot state. Incidents often follow a swap, a user-assigned identity change, or an IaC script that recreated the identity but did not restore the role assignments.

bash 01-app-service-identity-state.sh
SUBSCRIPTION="00000000-0000-0000-0000-000000000000"
RG="rg-prod-app"
APP="app-orders-prod"
SLOT="production"

az account set --subscription "$SUBSCRIPTION"

az webapp identity show --resource-group "$RG" --name "$APP" --query "{type:type,principalId:principalId,tenantId:tenantId,userAssignedIdentities:userAssignedIdentities}" --output json

az webapp config appsettings list --resource-group "$RG" --name "$APP" --query "[?contains(name, 'IDENTITY') || contains(name, 'AZURE_CLIENT_ID') || contains(name, 'KEYVAULT') || contains(name, 'AUTH')].[name,value]" --output table

When the application uses a user-assigned identity, confirm that AZURE_CLIENT_ID points to the intended client ID. After a swap, slot settings and attached identities can drift independently.

Test the token from the same runtime

Testing from an administrator laptop only proves the administrator path. The useful test runs from the same App Service, with the same variables, slot and credential chain.

bash 02-runtime-token-probe.sh
# Run from an App Service console, a controlled diagnostic endpoint
# or a temporary revision that uses the same slot and identity.

TOKEN_ENDPOINT="$IDENTITY_ENDPOINT"
TOKEN_HEADER="$IDENTITY_HEADER"
RESOURCE="https://vault.azure.net"

curl -sS -H "X-IDENTITY-HEADER: $TOKEN_HEADER" "$TOKEN_ENDPOINT?api-version=2019-08-01&resource=$RESOURCE" | jq '{expires_on, resource, client_id, access_token_present: (.access_token != null)}'

The expected result is not to print the full token. It is to prove the audience, client ID and endpoint availability. If this fails, fix the App Service identity before changing target permissions.

Read denials on Entra, Key Vault or the API

When a token is issued, evidence must come from the target. For Key Vault, Storage, API Management, an internal API or Microsoft Graph, look for the principal, audience and rejection reason in the available logs.

kusto 03-target-denials.kql
let StartTime = datetime(2026-07-16T08:00:00Z);
let EndTime = datetime(2026-07-16T09:00:00Z);
let AppPrincipalId = "00000000-0000-0000-0000-000000000000";
AzureDiagnostics
| where TimeGenerated between (StartTime .. EndTime)
| where ResourceProvider has_any ("MICROSOFT.KEYVAULT", "MICROSOFT.APIMANAGEMENT", "MICROSOFT.STORAGE")
| where ResultType has_any ("401", "403", "Forbidden", "Unauthorized")
 or OperationName has_any ("SecretGet", "SecretList", "GetBlob", "GatewayLogs")
| extend Raw = tostring(Properties)
| where Raw has AppPrincipalId or Identity has AppPrincipalId or CallerIPAddress != ""
| project TimeGenerated, ResourceProvider, Resource, OperationName, ResultType, ResultSignature, Identity, CallerIPAddress, Raw
| order by TimeGenerated desc

Adapt the query to the tables that are actually enabled. The important point is to tie the denial to the App Service principal, not only to observe an application-side 403.

Check roles at the right scope

Roles are rarely missing everywhere. They are missing at the relevant scope: exact vault, exact secret, storage account, API app registration, resource group or subscription. Also check recent removals.

bash 04-role-scope-check.sh
PRINCIPAL_ID="00000000-0000-0000-0000-000000000000"
TARGET_SCOPE="/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/rg-prod-sec/providers/Microsoft.KeyVault/vaults/kv-prod"

az role assignment list --assignee "$PRINCIPAL_ID" --all --query "[].{role:roleDefinitionName,scope:scope,principalType:principalType,createdOn:createdOn}" --output table

az role assignment list --assignee "$PRINCIPAL_ID" --scope "$TARGET_SCOPE" --include-inherited --query "[].{role:roleDefinitionName,scope:scope}" --output table

If the target still uses Key Vault access policies or Entra app roles, RBAC commands are not enough. The diagnosis must follow the authorization model that is really active.

Control the application credential chain

Many incidents come from a credential chain that behaves differently in local, staging and production. DefaultAzureCredential can try several sources. In production, the application should fail clearly when the expected identity is unavailable, instead of silently falling back to another credential.

text credential-chain-controls.txt
Check in code and configuration
ManagedIdentityCredential explicitly uses the expected client ID when user-assigned
No residual client secret exists in production app settings
Logs show which credential was selected
Token errors are not hidden by endless application retries
Token cache is invalidated after identity or slot changes
Startup tests call a representative dependency with the runtime identity

Drift signals
AZURE_CLIENT_SECRET reappears in configuration
AZURE_CLIENT_ID points to a staging identity
Production and staging slots do not have the same identities
Logs do not distinguish token acquisition from target call

A client secret workaround often becomes permanent because it is invisible. The durable fix starts by making the credential chain observable.

Decide fix, workaround or rollback

The decision should state which evidence is still missing and which risk surface is accepted. A client secret is acceptable only as a bounded temporary workaround, with an owner, expiry and removal change.

yaml managed-identity-decision.yml
decision:
fix_app_service_identity:
  when:
    - managed_identity_endpoint_fails_from_runtime
    - identity_disabled_on_app_or_slot
    - wrong_user_assigned_client_id
  validation:
    - token_probe_returns_expected_client_id
    - app_restart_or_slot_swap_keeps_identity

fix_target_authorization:
  when:
    - token_is_issued_for_expected_audience
    - target_logs_show_401_or_403_for_app_principal
    - role_assignment_or_app_role_is_missing
  validation:
    - target_accepts_runtime_identity
    - audit_log_contains_app_principal

rollback_recent_change:
  when:
    - identity_or_role_changed_in_recent_deployment
    - slot_swap_changed_effective_identity
    - no_safe_equivalent_signal_exists
  validation:
    - previous_identity_path_works
    - removed_change_is_documented

temporary_client_secret:
  allowed_only_if:
    - user_impact_is_active
    - managed_identity_fix_needs_external_delay
    - secret_is_scoped_to_same_or_narrower_permissions
    - expiry_owner_and_removal_change_are_created
  blocked_if:
    - root_cause_is_unknown
    - secret_would_need_broader_permissions
    - audit_would_no_longer_identify_the_application

A secret workaround without a removal date is an architecture regression. Treat it as a production exception, not a new default.

Validate before closure

Incident closure must prove three things: the runtime obtains the expected token, the target accepts the principal, and the application runs without a residual client secret.

text identity-validation-checklist.txt
Final validation
Token obtained from production slot with the expected client ID
Token audience matches the target
Role, access policy or app role confirmed at the right scope
Target log shows App Service principal and successful operation
Production app settings contain no temporary client secret
Representative application test passes after restart or swap
Alert or KQL query added to detect client secret reintroduction

Rollback ready
Previous identity documented
Previous role assignment known
Temporary secret expired or removed
IaC change aligned with the corrected state

If the fix was applied manually in the portal, it remains incomplete until IaC, the runbook and monitoring reflect the expected state.

Conclusion

A Managed Identity failure does not automatically justify bringing back a client secret. First locate the break: identity exposed by App Service, token audience, credential chain, target authorization, audit logs or slot change.

The right runbook restores Managed Identity when possible, fixes target scope when the token is valid, rolls back the recent change when production is exposed, and allows a client secret only as a temporary, bounded and monitored workaround. The final decision should leave the application operable without losing control of its identity.