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.

derived azure emits CanRetrieveToken

match

A conjunctive graph pattern. Variables (?x) bind node ids; every clause must hold.

{'host': None} ExecutesAs {'mi': None}

where

node_type(?mi) == ManagedIdentity node_class(?host) == Compute ?host.identity.type != 'None' # MI binding must exist

emit

source typeCompute
target typeManagedIdentity
source?host
target?mi
conditionsservice_state
state logicACTIVE 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."
move · open · esc close