Web Security

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.

Advanced13 min·Nyxeara Security Research·2026-09-16·CWE-918
cloud-securityssrfmetadataawsiamimdsv2
Prerequisites
what-is-httpwhat-is-a-url

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.254 is 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.254 milliseconds 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: Google header and Azure's Metadata: true header 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 requests with follow_redirects, simple curl calls) 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/token on GCP, and /metadata/identity/oauth2/token on 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.254 via 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 blocks 169.254.169.254 explicitly 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 contains AccessKeyId, SecretAccessKey, or a role name confirms metadata access. For GCP, add the Metadata-Flavor: Google header if the tool supports custom headers. For Azure, append ?api-version=2018-02-01 and send the Metadata: true header.
  • 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/token with X-aws-ec2-metadata-token-ttl-seconds: 21600. If the response contains an X-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 like instanceId, 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-identity returns 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:GetObject and s3:ListBucket on 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.254 on 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 has ec2:ImportKeyPair permission, 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 setting HttpTokens to required in 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 a PUT request 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:ListBucket on all buckets. Use AWS IAM Access Analyzer to identify and refine overly permissive roles.
  • Block 169.254.0.0/16 at 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:
    iptables -A OUTPUT -d 169.254.169.254 -p tcp --dport 80 -j DROP
    
    This prevents any process on the instance from reaching the metadata service, including the application process.
  • 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.254 in the VPC firewall egress rules.
    • Azure: use Azure Policy to enforce managed identity with conditional access. Block 169.254.169.254 via 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/route to 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.internal resolves to 169.254.169.254 on 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: Google header 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

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.

Published 2026-09-16 · Updated 2026-09-16