azure-mi-imds-token
Any code running on a host that has a managed identity bound can obtain a bearer token for that MI from the IMDS (or App Service identity) endpoint with no Entra credential.
match
A conjunctive graph pattern. Variables (?x) bind node ids; every clause must hold.
where
node_type(?mi) ==
ManagedIdentity
node_class(?host) == Compute
?host.identity.type != 'None' # MI binding must exist
emit
| source type | Compute |
|---|---|
| target type | ManagedIdentity |
| source | ?host |
| target | ?mi |
| conditions | service_state |
| state logic | ACTIVE when the host is running and has the MI binding. CONDITIONAL(service_state) when the host is stopped/deallocated (IMDS is only reachable from a running host). No Azure-side deny mechanism exists for IMDS reads — once the host runs, this is always ACTIVE regardless of RBAC; deny assignments on the MI's ARM resource do NOT block IMDS token retrieval. |
Narrative
Any code running on {host.name} can mint a bearer token for managed identity {mi.name} from the instance metadata service (IMDS at 169.254.169.254 for VMs, or the IDENTITY_ENDPOINT for App Service / Functions) with no Entra credential - the token is available to any process inside the host.
Raw rule rules/derived/azure/managedidentity.yaml
id: azure-mi-imds-token
emits: CanRetrieveToken
description: Any code running on a host that has a managed identity bound can obtain a bearer token for
that MI from the IMDS (or App Service identity) endpoint with no Entra credential.
match:
- - host: null
- ExecutesAs
- mi: null
where:
- node_type(?mi) == ManagedIdentity
- node_class(?host) == Compute
- '?host.identity.type != ''None'' # MI binding must exist'
emit:
source_type: Compute
target_type: ManagedIdentity
source: ?host
target: ?mi
permissions: []
conditions:
- service_state
state_logic: "ACTIVE when the host is running and has the MI binding. CONDITIONAL(service_state) when\
\ the host is stopped/deallocated (IMDS is only reachable from a running host). No Azure-side deny\
\ mechanism exists for IMDS reads \u2014 once the host runs, this is always ACTIVE regardless of RBAC;\
\ deny assignments on the MI's ARM resource do NOT block IMDS token retrieval."
confidence: 0.98
derived_from:
- ?host ExecutesAs ?mi
false_positive_note: "Do NOT emit if identity.type == 'None' (no MI bound). Do NOT emit for AKS workload\
\ identity paths \u2014 those use a projected federated OIDC token exchanged via the Azure OIDC endpoint,\
\ which is a federation-chains path (federation-maps-to-identity), not a direct IMDS call. Arc-enabled\
\ servers use http://localhost:40342/... instead of 169.254.169.254; model the same way (IMDS-local,\
\ no credential). App Service / Functions / Container Apps use the IDENTITY_ENDPOINT + IDENTITY_HEADER\
\ env vars injected by the platform \u2014 semantically identical but NOT on the 169.254 address.\n\
For user-assigned MIs: when multiple user-assigned MIs are bound to the same host, the IMDS caller\
\ must specify the correct client_id or mi_res_id to disambiguate. However, both are trivially enumerable\
\ from the environment (AZURE_CLIENT_ID env var injected by the platform, or from a prior IMDS /metadata/instance\
\ call). This is NOT a gate on the edge; the attacker can freely specify any of the attached MIs'\
\ IDs and retrieve its token. The false_positive_note in credential-chains or container-chains (if\
\ the MI is shared across hosts) discusses multi-MI/multi-host enumeration as a recon step, but does\
\ not block the core CanRetrieveToken fact."
narrative: "Any code running on {host.name} can mint a bearer token for managed identity {mi.name} from\
\ the instance metadata service (IMDS at 169.254.169.254 for VMs, or the IDENTITY_ENDPOINT for App\
\ Service / Functions) with no Entra credential \u2014 the token is available to any process inside\
\ the host."