Skip to content

App → services (workload identity token acquisition)

Every cloud runs a local metadata endpoint that the workload can hit to mint an access token for a target service. This page shows the exact wire per cloud, the token lifetime, and the "use the SDK, not the raw endpoint" rule.

The three metadata endpoints

The raw endpoints are useful to know and avoid: production code should call them through the vendor SDK, which handles retry / caching / refresh. Show the raw shape so the pattern is visible; don't reach past the SDK.

IMDS (Instance Metadata Service) — reachable from any VM / VMSS / App Service / Container Apps / AKS pod with a Managed Identity attached.

GET http://169.254.169.254/metadata/identity/oauth2/token
    ?api-version=2018-02-01
    &resource=https://vault.azure.net
Metadata: true

Response:

{
  "access_token": "eyJ0eXAi...",
  "expires_in": "3599",
  "expires_on": "1728...",
  "resource":   "https://vault.azure.net",
  "token_type": "Bearer"
}
  • Metadata: true header is required (SSRF mitigation — the endpoint refuses requests without it).
  • resource sets the aud claim on the issued token. For a user-assigned MI on a multi-MI VM, also pass client_id/object_id/msi_res_id.
  • expires_in is typically ~3600 s. refresh_token field is empty by design — you don't refresh, you re-request.

Recommended (Python) — DefaultAzureCredential picks up MI automatically:

from azure.identity import DefaultAzureCredential

credential = DefaultAzureCredential()
token = credential.get_token("https://vault.azure.net/.default")
# token.token, token.expires_on — the SDK caches until ~5 min before expiry
# then re-hits IMDS on the next get_token() call.

The SDK handles the retry-with-exponential-backoff strategy IMDS docs recommend (attempt 1 delay 0s, 2s, 6s, 14s, 30s over 5 attempts on 404/429/5xx).

Metadata server — reachable from any Compute Engine VM / GKE node / Cloud Run / Cloud Functions container with a service account attached (default or explicit).

GET http://metadata.google.internal/computeMetadata/v1/instance/service-accounts/default/token
Metadata-Flavor: Google

Response:

{
  "access_token": "ya29.c.b0Aa...",
  "expires_in":   3599,
  "token_type":   "Bearer"
}
  • Metadata-Flavor: Google header is required (mirrors Azure's SSRF-mitigation rule).
  • The default in the URL is the SA email alias — use the SA's actual email address for a specific SA on a multi-SA VM.
  • Tokens are ~1 hour.

Recommended (Python) — google.auth.default():

import google.auth
from google.auth.transport.requests import Request

credentials, project = google.auth.default(
    scopes=["https://www.googleapis.com/auth/cloud-platform"],
)
credentials.refresh(Request())
# credentials.token, credentials.expiry — the client library refreshes
# automatically on the next google-cloud-* SDK call when close to expiry.

On GKE, prefer Workload Identity Federation (K8s SA ↔ Google SA binding). The metadata server proxy inside the pod exposes the same URL; no per-pod key file.

IMDSv2 (session-token protected) — reachable from any EC2 instance / ECS task / EKS pod (via IRSA — see below).

# 1. Get a session token (PUT with a TTL header)
curl -X PUT "http://169.254.169.254/latest/api/token" \
  -H "X-aws-ec2-metadata-token-ttl-seconds: 21600"

# 2. Use it to fetch role credentials
curl -H "X-aws-ec2-metadata-token: $TOKEN" \
  http://169.254.169.254/latest/meta-data/iam/security-credentials/<role-name>

Response:

{
  "AccessKeyId":     "ASIA…",
  "SecretAccessKey": "…",
  "Token":           "…",
  "Expiration":      "2026-08-08T14:22:18Z"
}
  • IMDSv2 is mandatory for hardened setups — reject IMDSv1 with MetadataOptions.HttpTokens = required at instance-launch time.
  • Credentials rotate roughly every 6 hours by default; the SDK re-fetches before expiry.

On EKS, IRSA (IAM Roles for Service Accounts) replaces IMDS with a projected OIDC token. The pod SA is annotated with eks.amazonaws.com/role-arn; the AWS SDK does sts:AssumeRoleWithWebIdentity transparently.

Recommended (Python) — boto3 handles everything:

import boto3

# Default provider chain finds IRSA / IMDSv2 automatically.
s3 = boto3.client("s3")
s3.list_buckets()
# boto3 caches the credential set and refreshes ~15 min before expiry.

The moto3 credential-provider chain: env vars → assumed-role file → EC2/ECS/EKS metadata. On EKS with IRSA, the projected AWS_WEB_IDENTITY_TOKEN_FILE variable wins over IMDS.

Token lifetime — the rule

Provider Typical service-token lifetime Refresh strategy
Azure Entra (via IMDS) ~1 hour (expires_in in the JSON) DefaultAzureCredential.get_token() — cache in the credential object, re-hit IMDS on demand near expiry
GCP metadata server ~1 hour google.auth credential — auto-refresh on next SDK use
AWS IMDSv2 role creds ~6 hours (rotated) boto3 credential provider — 15-min pre-expiry refresh
AWS IRSA (EKS) projected token 1 hour (serviceAccountToken projection default) kubelet rotates the projected file; boto3 re-does AssumeRoleWithWebIdentity

The rule for your code: never manage the lifecycle yourself. Instantiate the credential object once at process start; call it every time you need a token. The SDK owns the cache-and-refresh loop.

Anti-pattern:

# NEVER: hoist the token string; it becomes stale silently.
TOKEN = credential.get_token("https://vault.azure.net/.default").token
def call_service():
    requests.get(url, headers={"Authorization": f"Bearer {TOKEN}"})

Correct:

def call_service():
    token = credential.get_token("https://vault.azure.net/.default").token
    requests.get(url, headers={"Authorization": f"Bearer {token}"})

The credential object caches internally; the second version is just as cheap and can't go stale.

Where the arc goes next

  • App → database — same workload identity, one hop further: the token becomes the DB password on each new connect.
  • Later — cross-account/tenant federation and RBAC composition on top.