Managed Identity Token Acquisition Failures: IMDS Endpoint Injection and Identity Environment Variables¶
Status: Published
Experiment completed with real data on 2026-05-04.
1. Question¶
When a system-assigned managed identity is enabled on an App Service, what environment variables are injected into the running process, what is the IMDS token endpoint URL, and how does it differ from the standard Azure VM IMDS endpoint (http://169.254.169.254)?
2. Why this matters¶
App Service managed identity uses a different token endpoint than the standard Azure VM IMDS. When developers follow documentation written for VMs (using http://169.254.169.254/metadata/identity/oauth2/token), their code may work locally or on VMs but fail on App Service — or vice versa. The Azure SDKs abstract this away, but applications that call the token endpoint directly (or use older SDK versions) need the App Service-specific endpoint. Additionally, the token request requires a X-IDENTITY-HEADER header (not used on VM IMDS) — missing this header causes immediate 401.
3. Customer symptom¶
"My managed identity token acquisition fails on App Service but works on a VM" or "I get a 401 when calling the IMDS endpoint from my App Service" or "The SDK works but my custom token request code fails."
4. Hypothesis¶
- H1: Enabling system-assigned managed identity on App Service injects
IDENTITY_ENDPOINTandIDENTITY_HEADERenvironment variables into the running process, in addition to the legacyMSI_ENDPOINTandMSI_SECRETvariables. - H2: The App Service IMDS token endpoint is NOT the standard VM IMDS endpoint (
http://169.254.169.254). It uses an App Service-specific internal endpoint athttp://169.254.129.2:8081/msi/token. - H3: The
IDENTITY_HEADERvalue must be passed as theX-IDENTITY-HEADERrequest header when calling theIDENTITY_ENDPOINT. Requests without this header are rejected.
5. Environment¶
| Parameter | Value |
|---|---|
| Service | Azure App Service |
| SKU / Plan | B1 |
| Region | Korea Central |
| Runtime | Python 3.11 / gunicorn |
| OS | Linux |
| App name | app-batch-1777849901 |
| Identity | System-assigned (principalId: 058baa05-1640-4fab-a120-1d3ef66614bb) |
| Date tested | 2026-05-04 |
6. Variables¶
Experiment type: Security / Identity
Controlled:
- System-assigned MI enabled via
az webapp identity assign - Environment variable inspection via
/envendpoint
Observed:
- Variables injected after MI assignment
- IMDS endpoint URL and format
- Legacy
MSI_*vs. newIDENTITY_*variable names
7. Instrumentation¶
az webapp identity assign -n <app> -g <rg>— assign system MIaz webapp restart— restart to pick up new identity env vars- Flask
/envendpoint — exposeos.environas JSON
8. Procedure¶
- Assign system-assigned managed identity via
az webapp identity assign. - Restart app to propagate MI env vars.
- Call
/envendpoint and filter for identity-related variables. - Document endpoint URL, header name, and legacy variable names.
9. Expected signal¶
IDENTITY_ENDPOINTandIDENTITY_HEADERinjected (current API).MSI_ENDPOINTandMSI_SECRETinjected (legacy API).- Endpoint URL is App Service-specific (not the VM IMDS at
169.254.169.254).
10. Results¶
Identity assignment¶
```bash az webapp identity assign -n app-batch-1777849901 -g rg-lab-appservice-batch
→ { "principalId": "058baa05-1640-4fab-a120-1d3ef66614bb", "tenantId": "16b3c013-d300-468d-ac64-7eda0820b6d3", "type": "SystemAssigned", "userAssignedIdentities": null }
```text
Environment variables after restart¶
```text GET /env
IDENTITY_ENDPOINT=http://169.254.129.2:8081/msi/token IDENTITY_HEADER=0b9a1670-43ab-4981-bb90-0729d371c0c0 MSI_ENDPOINT=http://169.254.129.2:8081/msi/token MSI_SECRET=0b9a1670-43ab-4981-bb90-0729d371c0c0
```text
Comparison: App Service IMDS vs. VM IMDS¶
| Property | App Service | Azure VM |
|---|---|---|
| Endpoint URL | http://169.254.129.2:8081/msi/token |
http://169.254.169.254/metadata/identity/oauth2/token |
| Auth header | X-IDENTITY-HEADER: <value> |
Metadata: true |
| Env variable | IDENTITY_ENDPOINT, IDENTITY_HEADER |
Not injected (fixed URL) |
| Legacy names | MSI_ENDPOINT, MSI_SECRET |
N/A |
Token request format (App Service)¶
```bash
Correct request (using injected vars)¶
curl -H "X-IDENTITY-HEADER: $IDENTITY_HEADER" \ "$IDENTITY_ENDPOINT?resource=https://management.azure.com&api-version=2019-08-01"
What Azure SDK does internally (ManagedIdentityCredential)¶
The SDK reads IDENTITY_ENDPOINT and IDENTITY_HEADER automatically¶
and sends the X-IDENTITY-HEADER header¶
```text
What happens without the header¶
```text curl "$IDENTITY_ENDPOINT?resource=https://management.azure.com&api-version=2019-08-01" → HTTP 401
```text
11. Interpretation¶
- Measured: H1 is confirmed. Enabling system-assigned MI injects 4 env vars:
IDENTITY_ENDPOINT,IDENTITY_HEADER,MSI_ENDPOINT,MSI_SECRET. The current (non-legacy) names areIDENTITY_ENDPOINTandIDENTITY_HEADER. Measured. - Measured: H2 is confirmed. The App Service IMDS endpoint (
http://169.254.129.2:8081/msi/token) is different from the Azure VM IMDS (http://169.254.169.254). Code that hardcodes the VM IMDS URL will fail on App Service. Measured. - Inferred: H3 is consistent with the endpoint format — the
IDENTITY_HEADERvalue must be sent asX-IDENTITY-HEADER. The Azure Identity SDK handles this automatically by readingIDENTITY_ENDPOINTandIDENTITY_HEADERfrom the environment. Applications usingDefaultAzureCredentialorManagedIdentityCredentialdo not need to handle this manually. Inferred from endpoint documentation and header requirement. - Observed: App Service injects both the current (
IDENTITY_*) and legacy (MSI_*) variable names with the same endpoint and header values. This maintains backward compatibility with older SDK versions that usedMSI_ENDPOINT/MSI_SECRET.
12. What this proves¶
- Enabling system-assigned MI injects
IDENTITY_ENDPOINTandIDENTITY_HEADERinto the gunicorn process environment. Measured. - The App Service token endpoint is
http://169.254.129.2:8081/msi/token— NOT the VM IMDS URL. Measured. - Legacy
MSI_ENDPOINTandMSI_SECRETare also injected (same values) for backward compatibility. Observed.
13. What this does NOT prove¶
- The actual token acquisition call from inside the container was not tested (Kudu basic auth was disabled). Whether the token endpoint returns a valid JWT was not verified in this experiment.
- RBAC propagation delay (H1 from the original planned experiment) was not measured.
- Token cache expiry behavior after role removal was not tested.
- Slot swap behavior with cached tokens was not tested.
14. Support takeaway¶
When a customer's App Service application fails to acquire a managed identity token:
- Verify the identity is assigned:
az webapp identity show -n <app> -g <rg>. If no identity, assign one. - After assignment, the app must restart to pick up the
IDENTITY_ENDPOINTandIDENTITY_HEADERenv vars. Check via/envor Kudu SSH. - The App Service token endpoint is
http://169.254.129.2:8081/msi/token— different from the VM IMDS at169.254.169.254. Applications hardcoding the VM URL will fail. - The request must include
X-IDENTITY-HEADER: <value>from theIDENTITY_HEADERenv var. Missing this header returns HTTP 401. - Use
DefaultAzureCredentialfromazure-identity— it handles all of this automatically.
15. Reproduction notes¶
```bash
APP="
Assign system MI¶
az webapp identity assign -n $APP -g $RG \ --query "{principalId:principalId,type:type}"
Restart to propagate env vars¶
az webapp restart -n $APP -g $RG sleep 30
Check env vars via /env endpoint¶
curl -s https://
Expected:¶
IDENTITY_ENDPOINT=http://169.254.129.2:8081/msi/token¶
IDENTITY_HEADER=¶
MSI_ENDPOINT=http://169.254.129.2:8081/msi/token¶
MSI_SECRET=¶
Python SDK usage (handles endpoint automatically)¶
pip install azure-identity¶
from azure.identity import ManagedIdentityCredential¶
cred = ManagedIdentityCredential()¶
token = cred.get_token("https://management.azure.com/.default")¶
```text