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.
Verified 2026-08-08
- Azure IMDS + Managed Identity: learn.microsoft.com/…/managed-identities-azure-resources/how-to-use-vm-token
- GCP metadata server: cloud.google.com/compute/docs/metadata/overview
- AWS IMDSv2: docs.aws.amazon.com/AWSEC2/latest/UserGuide/configuring-instance-metadata-service.html
- AWS IRSA (EKS): docs.aws.amazon.com/eks/latest/userguide/iam-roles-for-service-accounts.html
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: trueheader is required (SSRF mitigation — the endpoint refuses requests without it).resourcesets theaudclaim on the issued token. For a user-assigned MI on a multi-MI VM, also passclient_id/object_id/msi_res_id.expires_inis typically ~3600 s.refresh_tokenfield 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:
Metadata-Flavor: Googleheader is required (mirrors Azure's SSRF-mitigation rule).- The
defaultin 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 = requiredat 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.