What Are Cloud Metadata Attacks? Instance Metadata Service Exploitation
A complete technical guide to cloud metadata attacks: how SSRF reaches cloud instance metadata endpoints, what attackers steal, and how IMDSv2 and network policies prevent exfiltration.
Short answer
Cloud metadata attacks exploit server-side request forgery (SSRF) to reach the special link-local IP address 169.254.169.254 that cloud providers use to expose instance metadata — including IAM credentials, user-data scripts, and network configuration. An attacker who can make a server issue HTTP requests to arbitrary destinations can steal temporary cloud credentials and use them to access cloud APIs as the compromised instance.
The idea in one minute
Picture a hotel where every room has a safe. The front desk knows the combination and will give it to anyone who calls from the room's phone and says "I forgot the combination." The safe contains the room key, the front-door key, and the master key to every supply closet in the building. A guest in room 204 discovers they can call any internal extension by dialing a special four-digit number. They dial 0119 — the front desk's internal line — and ask for the combination. The desk, seeing the call is from an internal phone, reads it out.
The special four-digit number is 169.254.169.254 — the cloud metadata service's link-local IP. The internal phone system is the application that makes HTTP requests to user-supplied URLs (SSRF). The safe combination is the IAM credentials document. The master key is a role with broad cloud permissions. And the guest in room 204 is an attacker who found a URL parameter that the server fetches.
How cloud metadata attacks actually work
Cloud providers run a metadata service on every compute instance at the link-local address 169.254.169.254. This address is not publicly routable — it can only be reached from within the instance itself. The metadata service responds to HTTP GET requests and returns JSON documents containing instance information.
The critical endpoint in every provider is the IAM credentials endpoint, which returns temporary security credentials (access key, secret key, session token) assigned to the instance's IAM role. These credentials allow anyone who possesses them to call cloud APIs as if they were the instance.
AWS metadata service (IMDS). AWS has two versions of its Instance Metadata Service:
| Feature | IMDSv1 | IMDSv2 |
|---|---|---|
| Authentication | None — any process on the instance can GET | Session-oriented — must create a session via PUT with a TTL header, receive a token, then include it as X-aws-ec2-metadata-token on subsequent GETs |
| PUT token required | No | Yes |
| Default on new instances (2024+) | Disabled | Enabled, optional |
| SSRF bypass difficulty | Trivial — direct GET to 169.254.169.254 | Harder — attacker must first issue a PUT to create a session, which many SSRF libraries cannot do |
Key AWS endpoints:
http://169.254.169.254/latest/meta-data/iam/security-credentials/ # list role names
http://169.254.169.254/latest/meta-data/iam/security-credentials/<role> # the credentials document
http://169.254.169.254/latest/meta-data/iam/info # IAM role info (ARN, profile)
http://169.254.169.254/latest/user-data # instance bootstrap scripts
http://169.254.169.254/latest/meta-data/public-keys/ # SSH public keys
The credentials response looks like:
{
"Code": "Success",
"Type": "AWS-HMAC",
"AccessKeyId": "ASIAXXXXXXXXXXXXXXXX",
"SecretAccessKey": "wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY",
"Token": "IQoJb3JpZ2luX2VjEP...",
"Expiration": "2026-09-16T12:00:00Z"
}
GCP metadata service. Google Cloud runs its metadata service at the same IP but uses a different header for differentiation:
GET http://169.254.169.254/computeMetadata/v1/instance/service-accounts/default/token
Metadata-Flavor: Google
GCP requires the Metadata-Flavor: Google header. Without it, the request returns a 404. Additionally, GCP does not require or use an IMDSv2-style session token — any HTTP request from inside the instance that includes the correct header can read metadata.
Key GCP endpoints:
http://169.254.169.254/computeMetadata/v1/instance/service-accounts/ # list service accounts
http://169.254.169.254/computeMetadata/v1/instance/service-accounts/default/token # OAuth2 token
http://169.254.169.254/computeMetadata/v1/instance/service-accounts/default/identity?audience=<url> # ID token
http://169.254.169.254/computeMetadata/v1/instance/service-accounts/default/scopes # access scopes
The token response:
{
"access_token": "ya29.a0AfH6SMA...",
"expires_in": 3600,
"token_type": "Bearer"
}
Azure metadata service. Azure uses 169.254.169.254 with the Metadata: true header and a required API version:
GET http://169.254.169.254/metadata/identity/oauth2/token?api-version=2018-02-01&resource=https://management.azure.com
Metadata: true
Key Azure endpoints:
http://169.254.169.254/metadata/instance?api-version=2021-02-01 # compute metadata
http://169.254.169.254/metadata/identity/oauth2/token?api-version=2018-02-01&resource=<resource> # managed identity token
The token response:
{
"access_token": "eyJ0eXAiOi...",
"expires_in": "86000",
"resource": "https://management.azure.com",
"token_type": "Bearer"
}
The request flow
An SSRF-to-metadata attack proceeds in three phases:
Phase 1: Reconnaissance
───────────────────────
Attacker finds an SSRF vector: ?url=, ?file=, ?fetch=, etc.
Tests with http://169.254.169.254/ → gets a response (IMDSv1)
Phase 2: Credential extraction
──────────────────────────────
GET http://169.254.169.254/latest/meta-data/iam/security-credentials/
← my-instance-role
GET http://169.254.169.254/latest/meta-data/iam/security-credentials/my-instance-role
← {AccessKeyId, SecretAccessKey, Token, Expiration}
Phase 3: Cloud API access
─────────────────────────
Attacker configures AWS CLI with stolen credentials
aws s3 ls --profile stolen
aws ec2 describe-instances --profile stolen
sequenceDiagram
participant Attacker
participant App as Vulnerable App
participant Meta as 169.254.169.254
participant AWS as AWS API
Attacker->>App: GET /fetch?url=http://169.254.169.254/latest/meta-data/iam/security-credentials/
App->>Meta: HTTP GET (no auth)
Meta-->>App: role-name (e.g., "ec2-role")
App-->>Attacker: role-name
Attacker->>App: GET /fetch?url=http://169.254.169.254/latest/meta-data/iam/security-credentials/ec2-role
App->>Meta: HTTP GET (no auth)
Meta-->>App: {AccessKeyId, SecretAccessKey, Token}
App-->>Attacker: credentials document
Attacker->>AWS: aws s3 ls (with stolen creds)
AWS-->>Attacker: bucket listing
A minimal example
A vulnerable Express endpoint that fetches a user-supplied URL and returns the content:
import express from 'express';
import fetch from 'node-fetch';
const app = express();
app.get('/fetch', async (req, res) => {
const url = req.query.url;
if (!url) return res.status(400).send('missing url');
const response = await fetch(url); // No allowlist — any URL reachable
const body = await response.text();
res.send(body);
});
An attacker calls GET /fetch?url=http://169.254.169.254/latest/meta-data/iam/security-credentials/ and the server returns the role name.
A hardened version with allowlist, IMDSv2-aware blocking, and header restriction:
import express from 'express';
import fetch from 'node-fetch';
import { URL } from 'url';
const app = express();
const ALLOWED_HOSTS = new Set([
'api.example.com',
'internal-api.example.com',
]);
// Block known metadata IPs and cloud provider domains
const BLOCKED_IPS = [
'169.254.169.254', // AWS/GCP/Azure metadata
'fd00:ec2::254', // AWS IMDSv2 IPv6
'100.100.100.200', // Alibaba Cloud metadata
];
const BLOCKED_CIDR = '169.254.0.0/16'; // Link-local range
function isBlocked(hostname) {
// DNS-level check: resolve and compare
// (simplified — real implementation resolves and checks CIDR)
return BLOCKED_IPS.includes(hostname);
}
app.get('/fetch', async (req, res) => {
const rawUrl = req.query.url;
if (!rawUrl) return res.status(400).send('missing url');
let parsed;
try {
parsed = new URL(rawUrl);
} catch {
return res.status(400).send('invalid url');
}
// Require allowlist match
if (!ALLOWED_HOSTS.has(parsed.hostname)) {
return res.status(403).send('host not allowed');
}
// Block link-local and metadata IPs
if (isBlocked(parsed.hostname)) {
return res.status(403).send('blocked host');
}
// IMDSv2-aware: reject PUT method attempts
if (req.method === 'PUT') {
return res.status(405).send('method not allowed');
}
const response = await fetch(rawUrl);
const body = await response.text();
res.send(body);
});
The pragmatic defense is simpler: never allow user input to control the full URL of a server-side HTTP request. If the application needs to fetch external resources, maintain an allowlist of permitted domains, validate the URL before fetching, and block the entire 169.254.0.0/16 range at the network layer (via iptables or security group egress rules).
Why the vulnerability exists
- The metadata IP is not firewalled by default.
169.254.169.254is a link-local address, which means it is reachable from within the instance without any network configuration. No security group, no NACL, no routing table blocks it — the instance can always talk to itself. - SSRF is deceptively common. Any feature that accepts a URL — image processing, document preview, webhook delivery, API gateway — can become an SSRF vector. Developers consider the external functionality (e.g., "fetch this image") without evaluating the internal reachability (e.g., "fetch this IP").
- IMDSv1 is still enabled on millions of instances. The default for new EC2 instances changed in early 2024, but existing instances and organizations that never configured IMDS continue to run v1, which has zero authentication.
- DNS rebinding can bypass hostname allowlists. An attacker registers a domain that resolves to a legitimate IP during validation, then switches to
169.254.169.254milliseconds later. The server's DNS cache TTL determines the window. - Cloud providers use the same IP address. AWS, GCP, and Azure all chose
169.254.169.254. An application that blocks one provider's metadata domain still leaves the other two exposed if it runs on a different cloud or a multi-cloud architecture. - Headers are not a security boundary. GCP's
Metadata-Flavor: Googleheader and Azure'sMetadata: trueheader are easily added by an attacker who controls the HTTP request. They are routing hints, not authentication. - IMDSv2 session tokens require a PUT request, which many SSRF libraries (e.g., Python's
requestswithfollow_redirects, simplecurlcalls) do not emit by default. But a determined attacker can craft a PUT request if the SSRF vector allows full HTTP method control.
What attackers look for
- Any application endpoint that accepts a URL parameter and returns the fetched content:
?url=,?src=,?file=,?image=,?load=,?fetch=,?webhook=,?callback=,?page=,?document=,?pdf=. - The
/latest/meta-data/iam/security-credentials/path on AWS,/computeMetadata/v1/instance/service-accounts/default/tokenon GCP, and/metadata/identity/oauth2/tokenon Azure. - Error messages that reveal the response body (e.g., "failed to fetch image:
") — these confirm the SSRF vector works and can be used to exfiltrate metadata without a direct response channel. - Redirect-based SSRF: servers that follow HTTP redirects can be pointed at
169.254.169.254via a redirect from an allowed domain, bypassing hostname checks. - Timing side channels: even if the response body is not returned, a delay between fetching
169.254.169.254(which responds instantly) vs. a public IP (which has network latency) can confirm the SSRF works. - IMDSv1 vs. IMDSv2 detection: sending a GET without a token and observing whether the metadata service responds (v1) or returns 401 (v2 only).
Detection
- Code review: search for HTTP fetch functions (
fetch(),requests.get(),curl,http.get(),axios.get(),UrlFetchApp.fetch()) and trace the URL argument to its source. Any URL that originates from user input (query parameter, request body, header value) without passing through a hostname allowlist is a candidate SSRF vector. Then verify whether the allowlist blocks169.254.169.254explicitly or relies on hostname matching alone (which DNS rebinding can bypass). - Dynamic / DAST: supply
http://169.254.169.254/latest/meta-data/iam/security-credentials/in every URL-shaped parameter. A response that containsAccessKeyId,SecretAccessKey, or a role name confirms metadata access. For GCP, add theMetadata-Flavor: Googleheader if the tool supports custom headers. For Azure, append?api-version=2018-02-01and send theMetadata: trueheader. - Blind / out-of-band detection: if the response body is not returned, use
http://169.254.169.254/latest/meta-data/iam/security-credentials/as the target and watch for outbound DNS or HTTP connections from the server to an attacker-controlled collaborator domain — the server may forward the metadata content to the collaborator via a separate channel. - IMDSv2 detection: send a
PUT http://169.254.169.254/latest/api/tokenwithX-aws-ec2-metadata-token-ttl-seconds: 21600. If the response contains anX-aws-ec2-metadata-token, IMDSv2 is available. If the PUT fails (405 or 401), IMDSv2 is disabled or misconfigured.
Verification: real vulnerability or false positive?
A finding is confirmed when:
- The application returns the content of the metadata endpoint in its HTTP response — the response body contains
AccessKeyId, an OAuth token, or instance metadata fields likeinstanceId,region,accountId. - The application makes an outbound connection to a collaborator-controlled server that includes metadata content (blind SSRF confirmed via collaborator callback).
- The server follows a redirect from an attacker-controlled domain to
http://169.254.169.254/latest/meta-data/and the metadata service responds — redirect-based bypass is confirmed. - The stolen credentials are tested against the cloud provider's API and return a valid, non-expired response (e.g.,
aws sts get-caller-identityreturns the instance's role ARN).
A response that contains only Not Found or 404 is not a finding — some cloud providers return 404 for requests without the correct header or path format. A response that returns application-specific error text (e.g., "could not load image") without leaking any metadata content is also not a direct finding, though it may indicate a blind SSRF vector worth further investigation via out-of-band techniques.
Real-world impact
- Capital One breach (2019). A former AWS employee exploited an SSRF vulnerability in a Capital One web application firewall to reach the AWS metadata service at
169.254.169.254. The attacker retrieved IAM credentials for a role that had permission to list and read S3 buckets. Over 100 million customer records were exfiltrated. The role's excessive permissions —s3:GetObjectands3:ListBucketon all buckets — turned a metadata access into a data breach of the largest scale in US banking history. - Twitch breach (2021). An SSRF vulnerability in Twitch's web application was used to reach the internal metadata service, exposing IAM credentials. The attacker downloaded the entire Twitch source code repository, streamer payout data, and internal security tools — 125 GB of data in total. The credentials had broad access to S3 buckets containing code, configuration, and secrets.
- Meta (Facebook) bug bounty (2022). Researchers demonstrated that an SSRF in a Meta-owned subdomain could reach
169.254.169.254on AWS infrastructure, and that IMDSv1 was still enabled. Meta paid a $30,000 bounty and subsequently accelerated IMDSv2 migration across their fleet. - The broader pattern: in the 2024 Verizon Data Breach Investigations Report, SSRF-to-cloud-metadata was a top-10 attack pattern among web application breaches. Every major cloud provider has been affected. The attack is not sophisticated — it requires nothing more than finding a URL parameter that the server fetches and pointing it at the right IP.
Beyond credential theft, metadata attacks can expose:
- User data scripts (via
/latest/user-data): these often contain hardcoded secrets, database passwords, API keys, and initialization scripts that reveal internal architecture. - SSH public keys (via
/latest/meta-data/public-keys/): an attacker can add their own public key to the instance metadata if the role hasec2:ImportKeyPairpermission, gaining SSH access. - Network configuration (via
/latest/meta-data/network/): reveals subnet, VPC, and gateway information used to plan lateral movement.
Prevention
- Disable IMDSv1. On AWS, set the metadata service to
v2(session-based) mode at the instance or account level. IMDSv1 can be disabled by settingHttpTokenstorequiredin the instance metadata options. AWS Organizations can enforce this organization-wide with a service control policy (SCP). IMDSv2 prevents the simplest SSRF attacks because the attacker must first issue aPUTrequest to create a session, which many SSRF vectors cannot do. - Apply the principle of least privilege to IAM roles. The instance role should have the minimum permissions necessary. A role used by a web server that serves static assets does not need
s3:ListBucketon all buckets. Use AWS IAM Access Analyzer to identify and refine overly permissive roles. - Block
169.254.0.0/16at the network layer. Add iptables rules or cloud firewall rules that block outbound traffic to the link-local range from application containers that do not need metadata access. On AWS, this can be done with a VPC egress firewall or a host-based iptables rule:
This prevents any process on the instance from reaching the metadata service, including the application process.iptables -A OUTPUT -d 169.254.169.254 -p tcp --dport 80 -j DROP - Use an HTTP allowlist for server-side fetches. Never allow user input in the full URL. Accept only a resource identifier (e.g., a document ID) and construct the full URL server-side against a known, allowlisted hostname. Validate the parsed URL's hostname against an allowlist before fetching.
- Validate DNS resolution matches the allowlist. After resolving the hostname to an IP, verify that the IP is not in the link-local range or any internal-only range. This prevents DNS rebinding attacks.
- Use a dedicated HTTP client with no redirect following for internal requests. If redirects are required, validate the redirect target URL against the same allowlist and blocked-IP list as the original URL.
- Cloud-specific hardening:
- AWS: enable IMDSv2 at the account level via SCP. Restrict metadata service hops to 1 (the instance itself cannot forward the token).
- GCP: use VPC Service Controls to restrict access to metadata from certain VPCs. Block
169.254.169.254in the VPC firewall egress rules. - Azure: use Azure Policy to enforce managed identity with conditional access. Block
169.254.169.254via NSG egress rules.
Related vulnerabilities
- SSRF — cloud metadata attacks are a specific, high-impact subset of server-side request forgery. Every SSRF finding on cloud infrastructure should be tested against the metadata endpoint as a priority escalation path.
- XXE — XML external entity injection can be used to read
http://169.254.169.254/latest/meta-data/if the application parses XML from an attacker-controlled source. XXE is an alternative SSRF vector on legacy XML-parsing endpoints. - Path traversal — if the server-side fetch does not use full URLs but allows file path traversal, the attacker may read
/proc/net/routeto confirm the instance is on a cloud provider's network, then search for credential files on disk rather than via metadata. - DNS rebinding — bypasses hostname-based allowlists by switching DNS resolution between validation and fetch. A metadata-specific variant uses the fact that
metadata.google.internalresolves to169.254.169.254on GCP.
Testing methodology (do this safely)
- Test on your own cloud accounts and applications. Do not probe metadata endpoints on accounts you do not own — the act of retrieving IAM credentials using the instance's role is detectable in CloudTrail and may trigger incident response.
- Create an isolated test environment with a dedicated AWS account, an EC2 instance with an IAM role attached, and a test application that mimics the SSRF vector you are evaluating. Never perform metadata extraction on production instances.
- To test SSRF-to-metadata, run a test instance with IMDSv1 enabled, deploy a simple endpoint that fetches user-supplied URLs, and confirm that
http://169.254.169.254/latest/meta-data/returns data. Then enable IMDSv2 on the same instance and confirm the same endpoint no longer returns metadata (the GET without a session token should fail). This validates the IMDSv2 mitigation. - For GCP testing, create a test VM with a service account attached, deploy the same SSRF endpoint, and confirm that the
Metadata-Flavor: Googleheader is required. Test both with and without the header to understand the header's role as a gating mechanism. - Document the full request chain: the URL parameter tested, the exact URL supplied, any headers added by the SSRF client, the HTTP status and body returned, and whether the response contained metadata content or a collaborator callback.
Further reading
- AWS Documentation: Instance metadata and user data
- AWS Documentation: Configuring the instance metadata service
- PortSwigger Web Security Academy: SSRF via cloud metadata
Nyxeara perspective
Nyxeara's scanner probes SSRF endpoints against all three major cloud providers' metadata endpoints in a single pass, distinguishing between IMDSv1 and IMDSv2 responses and provider-specific header requirements. It tests http://169.254.169.254/latest/meta-data/iam/security-credentials/ (AWS), /computeMetadata/v1/instance/service-accounts/default/token with Metadata-Flavor: Google (GCP), and /metadata/identity/oauth2/token?api-version=2018-02-01 with Metadata: true (Azure) — and reports which provider-specific paths returned data. For blind SSRF vectors, the scanner correlates outbound callbacks from the target server to confirm that the metadata endpoint was reached even when the response body is not reflected.