Cloud & Container Security · intermediate · ~11 min

Secrets, metadata, and cloud privilege escalation

**What you will learn** - Identify the common places cloud credentials leak: source code, CI logs, container images, environment variables, and the instance metadata service. - Explain how Server-Side Request Forgery (SSRF) reaches the metadata endpoint `169.254.169.254` and steals an instance's IAM role credentials. - Describe the difference between IMDSv1 and IMDSv2 and why the session-token requirement of IMDSv2 blocks most SSRF credential theft. - Recognise the dangerous IAM permissions that enable privilege escalation: `iam:PassRole`, `iam:CreatePolicyVersion`, `iam:AttachUserPolicy`, and `sts:AssumeRole` chaining. - Apply layered defences: secrets managers, IMDSv2, least privilege, short-lived credentials, logging, and rotation. - Build a simple mental threat model that traces how a small foothold becomes full account compromise.

Overview

Most people picture a cloud breach as someone "hacking the server." In practice, the more common and more damaging pattern is quieter: an attacker finds a credential they were never supposed to see, then uses ordinary, documented cloud features to turn that credential into more access. No exotic exploit required — just leaked secrets and over-generous permissions.

This lesson builds directly on The cloud model and IAM. There you learned that every action in a cloud account is performed by a principal (a user, a role, or a service) and is allowed or denied by IAM policies. Here we look at what happens when an attacker gets to act as one of those principals — and how a principal with seemingly minor permissions can climb to administrator.

There are two halves to the story:

  1. Getting credentials. Cloud credentials are just strings: an access key, a token, a connection string. They leak into code, logs, images, and — critically — the instance metadata service, a special internal endpoint that hands out a machine's role credentials automatically.
  2. Using credentials to escalate. Once an attacker can call the cloud API as some principal, they look for IAM permissions that let them grant themselves more. Security professionals map these as privilege-escalation paths, much like attack paths in an Active Directory network.

This is defensive material. The goal is to understand these paths well enough to close them in your own accounts, detect abuse in your logs, and reason about blast radius. Everything here applies only to systems you own or are explicitly authorised to test in an isolated lab.

Key terms you will meet: IMDS (Instance Metadata Service), IMDSv2 (the token-protected version), SSRF (Server-Side Request Forgery), STS (Security Token Service, which issues temporary credentials), PassRole (handing a role to a compute service), and least privilege (granting only the permissions actually needed).

Why it matters

Credential theft and IAM privilege escalation are how a small cloud foothold becomes full account compromise. An attacker rarely needs to break encryption or find a zero-day; they need one leaked key and one over-broad policy.

The single most common path in real assessments is stealing role credentials from the metadata service through an SSRF bug in a web application. The app is tricked into fetching http://169.254.169.254/..., the metadata service returns the instance's temporary credentials, and the attacker now acts as that instance — sometimes with far more power than the application ever needed.

These IAM escalation paths are the cloud equivalent of Active Directory path-finding. They are a core focus of cloud penetration tests and security reviews because they convert quietly: a developer grants iam:PassRole to make a deployment script work, and months later that single permission is the rung an attacker uses to reach admin. Understanding the paths is what lets defenders see the risk before an attacker does — and the cost of missing one is often the entire account: data exfiltration, resource abuse (crypto-mining on your bill), and persistence that is hard to evict.

Core concepts

Authorization and ethics note

Everything below describes attacker techniques so you can defend against them. Only ever test these ideas against accounts you own or have written authorization to assess, and only in an isolated lab (a personal sandbox account, a local emulator, or a CTF). Never point any of this at third-party systems, and never use real production credentials in examples.

A simple threat model

Before the concepts, fix the picture of what we are protecting and where attacks enter.

ASSETS                         ENTRY POINTS              IMPACT
------                         ------------              ------
IAM role credentials   <--SSRF-- web app (user input)    act as the role
Secrets (DB pw, API)   <--leak-- git repo / CI logs      read/modify data
Admin permissions      <--IAM--  over-broad policy        full account

        Trust boundary: the cloud control plane (the IAM/STS API)
  +---------------------------------------------------------------+
  |  Inside: principals act via API calls, gated by IAM policy.   |
  |  An attacker who can make authenticated API calls is INSIDE.  |
  +---------------------------------------------------------------+

The central idea: the trust boundary is the ability to make authenticated API calls. The moment an attacker holds working credentials, they are inside the control plane and limited only by what IAM allows that principal to do.


Concept 1 — Where cloud credentials leak

Definition. A credential leak is any path by which a secret (access key, token, password, connection string) becomes readable by someone who should not have it.

Plain language. Cloud credentials are short text strings. Text strings end up everywhere humans and machines store text: source files, commit history, build logs, container layers, process environments.

Where they hide:

Location How it leaks
Source code / config Hardcoded keys committed to a repo
Git history A key was removed in a later commit but still lives in history
CI/CD logs A script echoes a secret; the log is world-readable
Container images A secret baked into an early image layer survives even if a later layer deletes the file
Environment variables Readable by any code in the process, and often dumped in error pages
Metadata service Hands out role credentials to anything that can reach 169.254.169.254

How it works internally (container layers). A Docker image is a stack of layers. Deleting a file in a later layer hides it from the final filesystem but the earlier layer still contains it — anyone who pulls the image can read it.

Layer 3 (top):  rm secrets.env        <- file no longer visible
Layer 2:        RUN build using key
Layer 1:        COPY secrets.env .    <- secret STILL stored here

When to worry / when not. Worry whenever a secret could touch version control, logs, or an image. You do not need to hide a value that is genuinely public (a public bucket URL, a published client ID). The pitfall is treating a long random-looking string as harmless because it "doesn't look like a password."

Knowledge check (find-the-bug): A teammate says "I committed an AWS key by accident but I deleted it in the next commit and force-pushed, so we're fine." Why are they probably not fine, and what is the only real fix?


Concept 2 — The instance metadata service (IMDS) and SSRF

Definition. IMDS is a link-local HTTP endpoint at 169.254.169.254 that a cloud VM can query to learn about itself — including, crucially, the temporary credentials for the IAM role attached to the instance.

Plain language. It is a vending machine inside every instance. Software running on the box asks it "what are my credentials?" and it answers — no authentication, because it assumes only the instance's own code can reach it. That assumption breaks when an application can be tricked into making the request on an attacker's behalf.

SSRF (Server-Side Request Forgery). An SSRF bug lets a user control a URL that the server fetches. If a feature like "enter an image URL and we'll fetch a preview" doesn't validate the URL, an attacker supplies http://169.254.169.254/latest/meta-data/iam/security-credentials/. The server fetches it and the response — temporary role credentials — comes back through the app.

Attacker -> Web app ("fetch this URL for me")
               |  url = http://169.254.169.254/.../security-credentials/ROLE
               v
          IMDS (169.254.169.254)  -> returns AccessKeyId, SecretAccessKey, Token
               |
               v
         Response flows back to attacker -> attacker now acts as ROLE

How IMDSv2 defends. IMDSv1 answers any GET request. IMDSv2 requires a two-step handshake: first a PUT to obtain a short-lived session token, then GET requests that include that token in a header. Most SSRF primitives can only issue simple GETs, cannot set custom headers, and cannot do the PUT — so IMDSv2 blocks them. Setting a low hop limit on the token response also stops it from being proxied off the host.

When to use / not. Always prefer IMDSv2 and least-privilege instance roles. The pitfall is leaving IMDSv1 enabled "for compatibility" while exposing a URL-fetching feature.

Knowledge check (predict-the-output): An app on an IMDSv2-only instance has an SSRF bug that can only send a plain GET with no custom headers. The attacker GETs http://169.254.169.254/latest/meta-data/iam/security-credentials/. What does the metadata service return, and why?

Knowledge check (explain-in-your-own-words): Why is 169.254.169.254 a link-local address rather than a public one, and how does that property both enable the convenience of IMDS and create the SSRF risk?


Concept 3 — IAM privilege escalation

Definition. Privilege escalation is using the permissions you already have to obtain permissions you should not have.

Plain language. IAM policies describe who can do what. Some "what"s are special because they let a principal modify the rules themselves or borrow another identity. Hold one of those and you can often promote yourself to admin.

The classic dangerous permissions:

  • iam:PassRole + a compute service. PassRole lets you hand an existing role to a service (Lambda, EC2, ECS). If you can pass an admin role to a function you control, your code now runs with admin power. The danger is a wildcard PassRole (any role) combined with permission to create compute.
  • iam:CreatePolicyVersion / iam:AttachUserPolicy / iam:PutUserPolicy. These let a principal edit or attach policies. An attacker simply writes themselves an Action: "*", Resource: "*" policy.
  • sts:AssumeRole chaining. A role's trust policy says who may assume it. An over-trusting role ("anyone in the account may assume me") lets a low-privileged principal hop into a higher-privileged one, sometimes through several roles in a chain.
low-priv user --PassRole--> [admin role] --attached to--> Lambda you create
      |                                                        |
      |                                                        v
      +----------------- runs your code as admin <------------- invoke

How it works internally. None of this is a bug in the cloud provider. Each step is an allowed API call. The vulnerability is the combination of permissions a single principal holds — which is exactly why automated analyzers map permission graphs.

When to grant / not. Grant iam:* only to genuine administrators. Never use wildcard PassRole; scope it to the specific role ARN a workload needs. The common pitfall is attaching a broad managed policy (or iam:*) to a service account "to make deploys work," creating a silent escalation path.

Knowledge check (concept): A CI role has iam:PassRole on * and permission to create Lambda functions. There is also an unused OrgAdmin role in the account. Describe, in defensive terms, the escalation path an attacker would take, and the one configuration change that closes it.


Concept 4 — Layered defences

Definition. Defence in depth means no single control is the only thing standing between an attacker and the assets.

  • Secrets managers (AWS Secrets Manager, Azure Key Vault, GCP Secret Manager): store secrets outside code; fetch at runtime; get automatic rotation and access logging.
  • IMDSv2 enforced account-wide, with a hop limit, so SSRF cannot harvest role credentials.
  • Least privilege: no iam:*, no wildcard PassRole, scoped resources, narrow trust policies.
  • Short-lived credentials: prefer roles and STS tokens over long-lived access keys; rotate any remaining static keys.
  • Logging and alerting: record IAM and STS calls so abuse is visible (covered in the next lesson, Cloud logging and serverless security).

Tools such as ScoutSuite, Prowler, and cloud-native IAM analyzers help defenders enumerate these paths before attackers do.

Syntax notes

There is no programming syntax here, but the configuration and request shapes matter. The skeleton of an SSRF-driven metadata read versus the IMDSv2 handshake that defeats it:

# IMDSv1 (vulnerable to simple-GET SSRF) — single unauthenticated request
GET http://169.254.169.254/latest/meta-data/iam/security-credentials/ROLE
  -> { "AccessKeyId": "...", "SecretAccessKey": "...", "Token": "..." }

# IMDSv2 (requires a token first — a plain GET alone fails)
PUT  http://169.254.169.254/latest/api/token
     Header: X-aws-ec2-metadata-token-ttl-seconds: 21600   # step 1: get token
GET  http://169.254.169.254/latest/meta-data/iam/security-credentials/ROLE
     Header: X-aws-ec2-metadata-token: <token-from-step-1> # step 2: use it

The defensive shape that matters most is a scoped IAM permission. Note the exact resource ARN instead of *:

{
  "Effect": "Allow",
  "Action": "iam:PassRole",
  "Resource": "arn:aws:iam::123456789012:role/app-runtime-role"
}

The single difference between safe and dangerous here is Resource: a specific role ARN cannot be abused to pass an admin role; "Resource": "*" can.

Lesson

Once inside a cloud environment, attackers hunt for credentials and abuse IAM to escalate. This is the cloud equivalent of privilege escalation on a single machine.

Where cloud credentials leak

  • Hardcoded in code, repos, CI logs, container images, and config files.
  • Instance metadata service (169.254.169.254). A compromised app, often via SSRF (see the web track), reads the instance's role credentials.
    • Defense: IMDSv2, which requires a session token, plus least-privilege instance roles.
  • Environment variables of running processes and functions.
  • Over-shared access to Secrets Manager or Key Vault.

Cloud privilege escalation (IAM)

These are like Active Directory attack paths, but for IAM policies. A principal with one seemingly minor permission can often reach admin.

  • iam:PassRole + a compute service lets an attacker run code as a more privileged role.
  • iam:CreatePolicyVersion or AttachUserPolicy lets an attacker grant themselves admin.
  • Role chaining via sts:AssumeRole abuses over-trusting roles.

Tools such as ScoutSuite, Pacu, and cloud IAM analyzers enumerate these paths.

Defenses

  • Use secrets managers instead of hardcoded secrets.
  • Enforce IMDSv2.
  • Apply least privilege: no iam:*, no wildcard PassRole.
  • Enable logging (next lesson) to detect abuse.
  • Rotate credentials regularly.

Code examples

Below is a defensive lab walkthrough: a tiny insecure URL-fetch endpoint that demonstrates the SSRF-to-metadata path, then the secure version, then how to verify the fix. Run only on localhost against a fake metadata server you control — never against a real cloud account.

WARNING: Intentionally vulnerable training example — use only in a local, isolated, authorized lab. Do not deploy.

# insecure_preview.py  -- VULNERABLE TRAINING ONLY
import requests
from flask import Flask, request

app = Flask(__name__)

@app.route("/preview")
def preview():
    # BUG: the user fully controls the URL the server will fetch.
    url = request.args.get("url", "")
    r = requests.get(url, timeout=2)        # SSRF: no validation at all
    return r.text

# An attacker calls:
#   /preview?url=http://169.254.169.254/latest/meta-data/iam/security-credentials/
# and the server happily fetches internal credentials and returns them.

Why it is unsafe: the server fetches any URL the client supplies, including the link-local metadata address that only the host should ever reach. On an IMDSv1 instance this returns live role credentials.

Secure version — validate the destination against an allowlist, resolve the hostname, and refuse private/link-local IP ranges:

# safe_preview.py  -- SECURE
import ipaddress
import socket
from urllib.parse import urlparse

import requests
from flask import Flask, request, abort

app = Flask(__name__)
ALLOWED_HOSTS = {"images.example-cdn.com"}     # explicit allowlist

def is_blocked_ip(host: str) -> bool:
    # Resolve and reject loopback, private, and link-local (169.254/16) addresses.
    for info in socket.getaddrinfo(host, None):
        ip = ipaddress.ip_address(info[4][0])
        if ip.is_private or ip.is_loopback or ip.is_link_local:
            return True
    return False

@app.route("/preview")
def preview():
    url = request.args.get("url", "")
    parsed = urlparse(url)
    if parsed.scheme not in ("http", "https"):
        abort(400, "scheme not allowed")
    if parsed.hostname not in ALLOWED_HOSTS:    # only known-good hosts
        abort(400, "host not allowed")
    if is_blocked_ip(parsed.hostname):          # defence in depth vs DNS tricks
        abort(400, "address not allowed")
    r = requests.get(url, timeout=2, allow_redirects=False)  # no redirect to bypass checks
    return r.text

What it does / expected output. The insecure app returns whatever the supplied URL contains; pointed at a local fake-metadata server it returns the fake credential JSON, demonstrating the leak. The secure app returns the page only for images.example-cdn.com; any other host — including 169.254.169.254 — returns HTTP 400.

Edge cases to handle: redirects (an allowed host can 302 to the metadata IP — that is why allow_redirects=False), DNS rebinding (resolve and re-check at fetch time), IPv6 forms of link-local/loopback, and decimal/octal IP encodings of 169.254.169.254. An allowlist plus IP-range checks beats trying to blocklist every encoding.

Test the fix (mitigation verification): start a local server that imitates the metadata endpoint, then confirm the secure app refuses it:

# 1. Fake metadata server on localhost (lab only)
python3 -m http.server 8000
# 2. The secure app must REJECT it with HTTP 400:
curl -s -o /dev/null -w "%{http_code}\n" \
  "http://localhost:5000/preview?url=http://127.0.0.1:8000/"   # expect 400

Detection / logging guidance. Log the requested host and the resolved IP for every fetch, and alert when an internal app makes any outbound request to a link-local or private address. In the cloud, monitor IAM/STS calls (e.g. AssumeRole, CreatePolicyVersion, PassRole) and credential use from unexpected IPs. Never log the credential values, tokens, or secret strings themselves — log the event, not the secret.

Line by line

Tracing the vulnerable-vs-secure /preview request:

Step Insecure app Secure app
1. Read url param Takes it as-is Takes it, then validates
2. Check scheme (none) Reject anything but http/https
3. Check host (none) Must be in ALLOWED_HOSTS
4. Resolve + check IP (none) getaddrinfo then reject private/loopback/link-local
5. Fetch requests.get(url) for any URL allow_redirects=False, only after checks pass
6. Result for metadata URL Returns role credentials HTTP 400

Step by step for the attack request ?url=http://169.254.169.254/latest/meta-data/iam/security-credentials/:

  1. Insecure path. The handler reads the URL and immediately calls requests.get. The server — which does have network access to 169.254.169.254 — performs the GET. On an IMDSv1 instance the metadata service responds with JSON containing AccessKeyId, SecretAccessKey, and Token. That JSON is returned to the attacker. The attacker exports those three values and now makes cloud API calls as the instance role. From here the IAM escalation concepts apply: if the role can PassRole or edit policies, the foothold widens.

  2. Secure path. urlparse extracts the scheme (http, allowed) and hostname (169.254.169.254). The hostname is not in ALLOWED_HOSTS, so the handler abort(400) before any fetch happens — the metadata service is never contacted. Even if an allowed host were used, is_blocked_ip resolves it and rejects link-local results, and allow_redirects=False stops an allowed host from bouncing the request to the metadata IP.

  3. Why IMDSv2 matters in parallel. Independently of the app fix, an IMDSv2-only instance would also defeat the insecure app: the metadata service would reject the credential-less GET because no X-aws-ec2-metadata-token header is present. Two independent layers (app validation + IMDSv2) is the point of defence in depth.

Common mistakes

Mistake 1 — "I deleted the committed key, so it's gone."

  • Wrong: removing a secret in a new commit. The secret still lives in git history and in any clone, fork, or CI cache.
  • Right: treat the key as compromised the instant it touched a shared repo. Rotate/revoke it immediately, then optionally scrub history. Rotation is the real fix; history rewriting is cleanup.
  • Recognise it: a secret scanner (gitleaks, trufflehog, GitHub secret scanning) flags it, or the provider sends a leaked-key alert.

Mistake 2 — Blocklisting the string 169.254.169.254 to stop SSRF.

  • Wrong: if "169.254.169.254" in url: reject. Attackers bypass it with 169.254.169.254 written in decimal/octal/hex, with DNS names that resolve to it, or via a redirect.
  • Right: allowlist destination hosts, resolve the IP, reject all private/loopback/link-local ranges, and disable redirects.
  • Recognise it: your test that fetches http://2852039166/ (decimal form of the metadata IP) still succeeds.

Mistake 3 — Wildcard PassRole to "make the deploy work."

  • Wrong: Action: iam:PassRole, Resource: "*". Now the principal can pass any role, including admin, to a compute service it controls.
  • Right: scope to the exact role ARN the workload needs: Resource: arn:aws:iam::<acct>:role/app-runtime-role.
  • Recognise it: an IAM analyzer (or Prowler/ScoutSuite) flags a privilege-escalation path from the principal to admin.

Mistake 4 — Leaving IMDSv1 enabled because "an old SDK needs it."

  • Wrong: keeping IMDSv1 reachable indefinitely. One SSRF bug then yields credentials.
  • Right: upgrade the SDK, enforce IMDSv2-only, set the hop limit to 1. Track exceptions explicitly.
  • Recognise it: a config check reports instances with HttpTokens: optional.

Debugging tips

"My secure preview endpoint rejects a legitimate URL." Check the allowlist exact-match (subdomain vs apex, trailing dot), and confirm urlparse returns the hostname you expect — userinfo like http://169.254.169.254@evil.com/ parses with hostname evil.com, which is itself a reason to validate carefully.

"The metadata read returns nothing / 401 in my lab." On IMDSv2 a credential-less GET is supposed to fail (that's the defence). If you are reproducing the insecure case in a lab, point the app at your own local fake-metadata HTTP server, not a real endpoint.

"Stolen credentials don't work / are expired." Role credentials from IMDS are temporary and include a Token (session token) and an expiry. All three (AccessKeyId, SecretAccessKey, Token) are required, and they stop working after expiry — which is also why short-lived credentials limit blast radius.

Questions to ask when an SSRF mitigation "doesn't work":

  • Did I resolve the hostname to an IP and check the IP, or only string-match the URL?
  • Are redirects disabled? An allowed host can redirect to the metadata IP.
  • Did I cover IPv6 and alternate IP encodings?
  • Is the check applied at fetch time to defeat DNS rebinding?

Logic-error giveaways: an IAM "deny" you expected is actually allowed because a broader Allow (or a wildcard managed policy) wins — read the effective permissions, not just the policy you wrote. Use the provider's policy simulator to test before deploying.

Memory safety

Security & safety

This is security content, so the safety notes are about credentials and authorization rather than C memory.

Risks covered:

  • Credential exposure: secrets in code, logs, images, env vars, and the metadata service.
  • SSRF to IMDS: server-side fetches reaching 169.254.169.254 and leaking role credentials.
  • IAM privilege escalation: PassRole, policy editing, and AssumeRole chaining turning a foothold into admin.

Defensive practices (do these):

  • Store secrets in a managed secrets store; never in source or images. Rotate on any suspicion of exposure.
  • Enforce IMDSv2 account-wide with a hop limit of 1; remove long-lived static keys in favour of roles and STS.
  • Apply least privilege: no iam:* for workloads, no wildcard PassRole, narrow trust policies, scoped resource ARNs.
  • Validate all server-side fetches with allowlists plus private/link-local IP rejection and no redirects.
  • Log IAM/STS activity and outbound requests to internal ranges; alert on anomalies. Never log the secret values, tokens, or keys themselves.

Ethics/authorization: the offensive paths are described only to close them. Test exclusively on systems you own or are authorized to assess, in isolated labs (sandbox account, local emulator, CTF). Never use real targets or real production secrets — use placeholders like API_KEY=<development-placeholder>.

Robustness note: assume any credential that could have leaked has leaked, and design so its blast radius is small (least privilege + short lifetime). Defence in depth means the app fix and the IMDSv2 setting and the scoped IAM policy each independently reduce risk.

Real-world uses

Concrete real-world use case. Multiple high-profile cloud breaches followed the same shape: a web application with an SSRF flaw was tricked into querying the instance metadata service, the attacker harvested the instance's IAM role credentials, and because that role was over-privileged, they reached storage and exfiltrated large volumes of customer data. The fix in each case is the same combination this lesson teaches: enforce IMDSv2, tighten the role to least privilege, validate server-side fetches, and monitor IAM/STS calls. This is also exactly what cloud penetration testers and security reviewers check first, and what tools like Prowler, ScoutSuite, and cloud-native access analyzers automate.

Professional best-practice habits.

Beginner rules:

  • Never hardcode a secret; pull from a secrets manager or injected runtime config.
  • Never echo secrets in logs or error messages.
  • Prefer roles over long-lived keys; if you must use a key, rotate it on a schedule.
  • Treat any user-supplied URL the server will fetch as dangerous until validated.

Advanced habits:

  • Enforce IMDSv2-only and hop-limit 1 via account policy / SCPs, not per-instance hope.
  • Continuously map IAM privilege-escalation paths and remediate them as findings, not one-offs.
  • Use short-lived, scoped STS credentials with conditions (source IP, MFA) where feasible.
  • Build detections: alert on CreatePolicyVersion, broad AttachUserPolicy, unexpected AssumeRole, and credential use from new IPs/ASNs.
  • Run secret scanning in CI to block leaks before merge, and rotate automatically on detection.

Practice tasks

Beginner 1 — Secret-hunt checklist. Write a checklist of at least six places a cloud credential can leak (e.g. repo, git history, CI logs, image layers, env vars, metadata service). For each, note one detection method and one prevention. Concepts: credential leaks. Hint: one location can be both an entry point and a defence (the metadata service).

Beginner 2 — Classify the IAM permission. Given this list — s3:GetObject, iam:PassRole, ec2:DescribeInstances, iam:CreatePolicyVersion, sts:AssumeRole, logs:PutLogEvents — label each as "escalation-relevant" or "benign," and for the escalation-relevant ones write one sentence on how it could be abused. Concepts: IAM escalation. Constraint: no tooling; reason from the meaning of each action.

Intermediate 1 — Patch the SSRF. Take the insecure /preview endpoint from this lesson and harden it: scheme check, host allowlist, IP-range rejection (private/loopback/link-local), and no redirects. Input/output: ?url=http://images.example-cdn.com/x.png → 200; ?url=http://169.254.169.254/... → 400; ?url=http://2852039166/ (decimal metadata IP) → 400. Concepts: SSRF defence. Hint: resolve the hostname before deciding; don't string-match.

Intermediate 2 — IMDSv2 handshake explainer. Without writing exploit code, document the exact two requests IMDSv2 requires (the PUT for a token, then the GET with the token header) and explain, step by step, why a simple-GET-only SSRF cannot complete it. Include what a hop limit of 1 adds. Concepts: IMDS/IMDSv2. Constraint: describe requests at the protocol level, not as runnable attacks.

Challenge — Map and break an escalation path (lab only). In a personal sandbox account (or a local IAM emulator), create a low-privileged principal with iam:PassRole on * plus permission to create a Lambda, and an unused admin role. On paper, diagram the escalation path from the low-priv principal to admin. Then apply the single fix that closes it (scope PassRole to one non-admin role ARN) and re-diagram to show the path is gone. Concepts: PassRole escalation, least privilege, threat modelling. Constraints: your own account only; no real production roles; do not actually exfiltrate anything — the deliverable is the before/after diagram and a written explanation. Hint: the fix changes one field: Resource.

Summary

Main concepts. Cloud compromise usually has two stages: get a credential, then use IAM to escalate. Credentials leak through code, git history, CI logs, container image layers, environment variables, and especially the instance metadata service at 169.254.169.254, which an SSRF bug can reach to steal role credentials. Privilege escalation then exploits dangerous IAM permissions — iam:PassRole (with a compute service), iam:CreatePolicyVersion/AttachUserPolicy, and sts:AssumeRole chaining — none of which are provider bugs; they are over-broad permission combinations.

Most important configuration to remember. Enforce IMDSv2 (token handshake + hop limit) so a simple-GET SSRF can't harvest credentials, and scope IAM tightly — the difference between safe and dangerous PassRole is "Resource": "<specific-role-arn>" versus "Resource": "*".

Common mistakes. Thinking a deleted-then-force-pushed key is safe (rotate it — it isn't); blocklisting the metadata IP string instead of allowlisting hosts and rejecting IP ranges; granting wildcard PassRole; and leaving IMDSv1 enabled.

What to remember. The trust boundary is the ability to make authenticated API calls — once an attacker holds working credentials they are inside. Defend in depth: secrets managers, IMDSv2, least privilege, short-lived credentials, validated server-side fetches, and logging of IAM/STS activity (never the secrets themselves). All testing is defensive, authorized, and lab-only.