The Capital One breach: an SSRF bug into 100M records
In March 2019, an attacker used a server-side request forgery (SSRF) bug
in a misconfigured web application firewall to reach a single internal
URL — http://169.254.169.254/ — running behind a Capital One-hosted
application in AWS. That one request was enough to pull temporary IAM
credentials for a role with broad S3 read access, and from there exfiltrate
more than 100 million credit applications and 140,000 Social Security
numbers (DOJ indictment). No malware, no phishing, no zero-day in AWS
itself — just a web app that could be tricked into fetching an internal
URL on the attacker’s behalf, and an internal URL that handed over live
credentials to anyone who asked. Capital One paid an $80 million OCC fine
and settled a $190 million class action over it (Reuters). Seven years
later, IMDSv1 — the version that made this possible — is still the
default reachable endpoint on plenty of EC2 instances, because “enabled”
is not the same as “enforced.”
Root cause
Every EC2 instance can reach a link-local address, 169.254.169.254,
which serves the Instance Metadata Service (IMDS) — instance ID, AMI
info, user-data, and critically, temporary security credentials for
whatever IAM role is attached to the instance. It’s how an EC2 instance
gets AWS credentials without anyone hardcoding a key. In the original
version of that service, IMDSv1, the endpoint answers any plain HTTP GET
request with no authentication step at all — if a request reaches that
IP from the instance, it gets an answer. That design was fine as long as
nothing running on the box could be tricked into making arbitrary
outbound requests on an attacker’s behalf. But web applications routinely
do exactly that: fetch a URL from a query parameter, proxy a request,
render a remote image, validate a webhook. The former engineer charged in
the case had reportedly discovered a misconfigured ModSecurity WAF rule
in front of a Capital One web application that could be coerced into
requesting an arbitrary URL and returning the response — a textbook
SSRF (indictment). Point that SSRF at
169.254.169.254/latest/meta-data/iam/security-credentials/<role-name>
and IMDSv1 hands back an access key, secret key, and session token for
whatever role the instance was running as — no extra proof that the
request came from a legitimate, non-hijacked process.
Blast radius
The stolen role’s permissions, not the SSRF bug itself, set the ceiling
on the damage. In this case the role had read access to S3 buckets used
for a range of Capital One’s credit-card and loan-application data,
compiled from six years of applications going back to 2005 — 106 million
individuals across the US and Canada, including Social Security numbers,
bank account numbers, and credit scores (Capital One’s disclosure).
Stolen IMDS credentials are functionally indistinguishable from
legitimate ones to everything downstream — CloudTrail logs the calls as
coming from the role, IAM enforces exactly the policy attached to that
role, and nothing about the request signature reveals it originated from
an SSRF payload rather than the application itself. That’s the general
shape of every IMDS-credential-theft incident: the SSRF or RCE is the
entry point, but the actual damage is bounded entirely by how
over-permissioned the instance role was. A role scoped to one bucket
prefix limits an attacker to that prefix; a role with s3:* across the
account hands over the account’s data.
Remediation
AWS’s direct answer, shipped in November 2019 a few months after this
breach became public, is IMDSv2 — it requires a session to be
established with a PUT request that returns a token, and every
subsequent metadata request must carry that token in a header
(AWS).
Critically, IMDSv2’s PUT requires HTTP with no redirects followed and
sets a configurable hop limit (default 1) on the response’s TTL — which
means a proxy or SSRF payload relaying a simple GET typically can’t
complete the handshake or forward the token past one network hop, while a
process running natively on the instance can. That single change would
have broken the Capital One attack path as described. Enforcement is the
part teams skip: IMDSv2 has been opt-in at the instance level since
launch, so an unpatched fleet stays exploitable indefinitely unless
someone acts. Three concrete steps: set HttpTokens: required (not just
optional) on every instance and in every launch template, ideally via
an AWS Config rule (ec2-imds-v2-check) that flags drift; use an
**SCP or Config rule to deny new instance launches without IMDSv2
enforced**, since a one-time fleet fix doesn’t stop the next Terraform
apply from reintroducing IMDSv1; and independently, scope every instance
role to least privilege — the hop-limit defense is layer two, not a
replacement for making sure a stolen credential is only ever worth as
much as the narrowest policy you could get away with attaching.
The bigger lesson
This wasn’t an AWS vulnerability — IMDS behaved exactly as designed, and
the WAF misconfiguration was Capital One’s, not Amazon’s. But it’s the
canonical case study for why cloud metadata endpoints deserve the same
scrutiny as any credential store: anything that can make an outbound
HTTP request from inside your VPC is a potential path to
169.254.169.254, and IMDSv1’s design assumed that path would never
exist. IMDSv2’s token-and-hop-limit model closes the SSRF-relay case
specifically, but it only helps the instances where someone actually
flipped HttpTokens to required — which is why the Config rule and the
SCP matter as much as the setting itself. If your fleet still allows
HttpTokens: optional, the difference between “we have IMDSv2 available”
and “we’re actually protected” is one unenforced launch template away
from mattering.
Join the discussion
Comments for this post live on social — reply to the thread.
Related posts
Cloud roundup: macOS Screen Sharing bug now under attack
A patched macOS Screen Sharing flaw is being exploited to plant crypto miners, a Windows Defender bypass has no fix yet, and EC2 gets built-in app health checks.
Cutting NAT gateway costs with VPC endpoints that actually help
How gateway and interface VPC endpoints replace NAT gateway traffic for AWS API calls, what they cost instead, and which traffic still has to go through NAT.
Cloud roundup: S3 finally names the policy that denied you
AWS S3 access-denied errors now name the exact policy ARN, Client VPN gets a scriptable CLI, and OpenAI ships authorized offensive-security models on Bedrock.