Skip to content
Back to research

Capital One: how cloud access exposed 106 million people

A plain-English account of the 2019 Capital One breach, checked against the bank’s own statements, the US indictment and the court outcome, followed by the IMDSv2, least-privilege and alerting controls that break the same chain in AWS today, with commands to check your own account.

YouTubeCapital One: how cloud access exposed 106 million peopleOpen the related video

What happened

The part most people miss

Copying the data took two days, 22 and 23 March 2019. Capital One found out on 17 July, almost four months later, and only because someone outside the bank emailed it [1].

On 17 July 2019 someone emailed Capital One’s responsible disclosure address, the inbox a company keeps for outsiders to report security problems. The email said the bank’s data seemed to be on GitHub, a public site for sharing code [1][4]. On 19 July the bank confirmed that an outsider had got into its systems [1].

The bank went public on 29 July 2019 [2]. The same day, FBI agents arrested Paige Thompson, a software engineer in Seattle [5][9]. In the months before, she had talked openly online, on Twitter and in a Slack group, about finding large stores of data on badly secured Amazon cloud servers [5].

WhatHow much
People affectedAbout 100 million in the US and about 6 million in Canada
Credit card applicationsFrom 2005 to early 2019: names, addresses, postcodes, phone numbers, email addresses, dates of birth and self-reported income
Customer recordsCredit scores, credit limits, balances, payment history and contact details
Transaction dataFragments from 23 days in 2016, 2017 and 2018
US Social Security numbersAbout 140,000, plus about 4,700 more found in January 2021
Linked bank account numbersAbout 80,000, from secured credit card customers
Canadian Social Insurance NumbersAbout 1 million
Not taken, the bank saysCredit card account numbers and log-in credentials
What Capital One says was taken [1][2]

Bradley’s video puts it as a bouncer on the front door. The bouncer checks everyone who walks in. This attacker never walked in. They rang the front desk, claimed to be the manager, and asked staff to read out what was in the vault.

How the attack worked, step by step

Four terms that make the rest easier

A web application firewall (WAF) sits in front of a website and blocks requests that look like attacks. Server-side request forgery (SSRF) tricks a server into making a web request for the attacker, to somewhere the attacker can’t reach directly. The instance metadata service (IMDS) is an address inside every AWS server, 169.254.169.254, that tells the server about itself, including temporary keys. An IAM role is a set of permissions given to a server, so it can use other AWS services without a stored password.

1. Scan for a weak firewall

The indictment says the attacker used and built scanners to find cloud servers whose web application firewall was misconfigured, so that commands sent from outside could reach the server and run [3].

2. Make the server fetch its own keys

Commands sent through that firewall obtained the security credentials for a role [3]. The complaint names a role ending in WAF-ROLE [4]. Reporting at the time said this was SSRF against the metadata service [6]. In 2019 that service answered any request from inside the server, with no token needed [8]. Our separate deep dive walks through that request step by step [7].

3. Use the keys from outside

Temporary keys work from anywhere until they expire. With them, the attacker listed the folders, or buckets, in the bank’s storage service, S3 [3]. The complaint describes a file holding three commands and a list of 700 folders [4].

4. Copy the data home

The role was allowed to read the data, so the attacker copied it to a server at home, hiding behind the IPredator VPN service and Tor [3][4]. Capital One dates the copying to 22 and 23 March 2019 [1].

5. Talk about it

The attacker posted about the finds on Twitter and in Slack, and a file about the bank’s storage sat on GitHub [4][5]. That file is what the tipster reported [4].

Once the keys were out, the firewall stopped mattering. The attacker was making ordinary AWS requests with real credentials, so whatever the role could do, the attacker could do. The indictment says the role had permission to view and copy the stored data [3].

Capital One was not the only victim. The indictment says data was copied from more than 30 organisations, and that some victims’ servers were used to mine cryptocurrency, which is known as cryptojacking [3].

What was proven, and what wasn’t

The usual telling of this breach is more certain than the court record. This is where each part comes from.

PointWhat is said, and whereHow firm
The way inA misconfigured web application firewall let outside commands reach the server and run. The indictment [3] and the complaint [4]. Amazon said the same [6]Charged, and the jury convicted on the computer fraud counts [9]
The keysThe commands fetched a role’s credentials, which were used to list and copy buckets. The indictment [3]Charged, and part of the case that ended in conviction [9]
SSRFThe trick was SSRF against the metadata service (IMDSv1). An unnamed source quoted by KrebsOnSecurity [6], and later analysesNot named in the court papers. It fits the indictment, but was never tested in court
PermissionsThe role had far more permission than a firewall needs. The same source [6]. The indictment says the role could view and copy the data [3]Well supported, but no court ruled on it
The datesThe copying happened on 22 and 23 March 2019. Capital One [1]. Count 1 of the indictment names 22 March [3]Stated by the bank. The indictment puts the wider access between 12 March and 17 July 2019 [3]
ScaleAbout 106 million people were affected. Capital One [1][2]Stated by the bank
RecoveryThe data was recovered and not used for fraud or shared. Capital One, citing the US government [1]Can’t be checked from outside
Each claim, its source and how firm it is

In June 2022 a federal jury convicted Thompson of seven crimes, including wire fraud, unauthorised access to a protected computer and damaging a protected computer. It acquitted her of access device fraud and aggravated identity theft. On 4 October 2022 US District Judge Robert S. Lasnik sentenced her to time served and five years’ probation. Prosecutors had asked for seven years in prison [9].

The regulator acted too. On 6 August 2020 the Office of the Comptroller of the Currency fined Capital One $80 million for failing to assess the risks properly before moving significant IT operations to the public cloud, and for not fixing the gaps in time [10].

Was it the cloud’s fault?

Capital One says the weakness was a configuration mistake, and that the elements involved are common to both cloud and on-premises systems [1]. AWS did change its design afterwards: in November 2019 it launched IMDSv2, built to resist open firewalls, open reverse proxies and most SSRF [8]. Bradley’s view in the video is that blaming AWS misses the point. The role on a firewall should never have been able to read customer data.

What defenders could have seen

Every step after the firewall was an AWS API call, so it could leave a record in CloudTrail, AWS’s log of activity in an account. The complaint says the bank’s logs showed the commands coming from IPredator VPN addresses and Tor exit nodes [4]. These are the signals that would stand out today.

SignalWhere you see itWhat it looked like here
Keys used outside AWSGuardDuty finding UnauthorizedAccess:IAMUser/InstanceCredentialExfiltration.OutsideAWS [11]The firewall role’s keys used through a VPN and Tor [4]
Calls from TorGuardDuty findings Recon:IAMUser/TorIPCaller and UnauthorizedAccess:IAMUser/TorIPCaller [11]Tor exit nodes in the bank’s logs [4]
Unusual bucket listingCloudTrail ListBuckets events, and GuardDuty Discovery:IAMUser/AnomalousBehavior [11]A list of 700 folders [4]
Large reads from storageCloudTrail S3 data events [12], or the S3 request metric BytesDownloaded [13]Nearly 30 GB of credit application data [5]
Signals from this attack, and where AWS shows them now
The gap in most accounts

CloudTrail records bucket listing by default. Reading the files themselves is a data event, which is only logged if you switch it on, and it costs extra [12]. Without it you can see that someone looked, but not what they took.

GuardDuty is AWS’s managed threat detection service. It reads CloudTrail and network logs and raises findings like the ones above [11]. We don’t know which of today’s findings existed in 2019, so treat this as what you can have now, not as what Capital One missed.

How to stop it

Bradley gives three fixes in the video: require IMDSv2, give every role only what it needs, and build loud alarms for odd behaviour. Here is each one as real settings. Swap the example account number, Region, names and IDs for your own.

Fix 1: require IMDSv2. IMDSv2 makes a program on the server ask for a session token with a PUT request before it can read metadata, and it won’t hand a token to a request carrying an X-Forwarded-For header, which proxies and firewalls add. By default the token can only travel one network hop. Together these stop open firewalls, open proxies and most SSRF from reaching the keys [8]. Datadog’s 2025 report found IMDSv2 enforced on 49% of EC2 instances, up from 32% a year earlier [14]. So about half still allowed the old version.

Before you switch an instance over, check that nothing on it still uses IMDSv1. The MetadataNoToken metric counts IMDSv1 calls. If it stays at zero, the change is safe [15]. Then require IMDSv2 on the instance [16].

Bash
# The instance to check (its id from the EC2 console)
INSTANCE_ID="<instance id>"

# Count IMDSv1 calls on that instance over the last 7 days (want: 0)
aws cloudwatch get-metric-statistics \
  --namespace AWS/EC2 --metric-name MetadataNoToken \
  --dimensions Name=InstanceId,Value="$INSTANCE_ID" \
  --start-time 2026-11-06T00:00:00Z --end-time 2026-11-13T00:00:00Z \
  --period 86400 --statistics Sum

# Require IMDSv2 on that instance
aws ec2 modify-instance-metadata-options \
  --instance-id "$INSTANCE_ID" \
  --http-tokens required --http-endpoint enabled

Then make IMDSv2 the default for new instances in every Region you use, and refuse any launch without it [17]. A hop limit of 2 lets containers on the instance still reach the service.

Bash
# Account default for this Region (repeat for each Region you use)
aws ec2 modify-instance-metadata-defaults --region eu-west-2 \
  --http-tokens required --http-put-response-hop-limit 2

# Refuse any launch in this Region that doesn't require IMDSv2
aws ec2 modify-instance-metadata-defaults --region eu-west-2 \
  --http-tokens-enforced enabled

If you use AWS Organizations, a service control policy (SCP) holds the line across every account. This one joins three of AWS’s published examples: no launch without IMDSv2, no switching it back, and no use of role keys that came from IMDSv1 [18]. Where it says <every EC2 instance>, use the instance pattern from those examples, the one that ends in instance/*.

JSON
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Sid": "RequireImdsV2",
      "Effect": "Deny",
      "Action": "ec2:RunInstances",
      "Resource": "<every EC2 instance>",
      "Condition": { "StringNotEquals": { "ec2:MetadataHttpTokens": "required" } }
    },
    {
      "Sid": "DenyImdsV1SwitchBack",
      "Effect": "Deny",
      "Action": "ec2:ModifyInstanceMetadataOptions",
      "Resource": "<every EC2 instance>",
      "Condition": {
        "StringNotEquals": { "ec2:Attribute/HttpTokens": "required" },
        "Null": { "ec2:Attribute/HttpTokens": false }
      }
    },
    {
      "Sid": "RequireAllEc2RolesToUseV2",
      "Effect": "Deny",
      "Action": "*",
      "Resource": "*",
      "Condition": { "NumericLessThan": { "ec2:RoleDelivery": "2.0" } }
    }
  ]
}

Fix 2: least privilege. Least privilege means each role gets only what its job needs. A firewall or web server might need to write its own logs to one place. It should not be able to list every bucket or read customer files. If the stolen keys could only do this, as Bradley puts it in the video, the trick would have led straight to a dead end.

JSON
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Sid": "WriteOwnLogsOnly",
      "Effect": "Allow",
      "Action": "s3:PutObject",
      "Resource": "<waf-logs bucket ARN>/waf/*"
    }
  ]
}

Fix 3: loud alarms. Prevention can fail, so the last fix is noticing. Switch GuardDuty on in every Region you use [11]. For the buckets that hold personal data, log S3 data events. Advanced event selectors replace a trail’s existing selectors, so include management events in the same call [12].

Bash
# Turn GuardDuty on in this Region (repeat for each Region you use)
aws guardduty create-detector --enable --region eu-west-2

# Log reads on the customer-data bucket, and keep management events
aws cloudtrail put-event-selectors --trail-name org-trail \
  --advanced-event-selectors '[
    {"Name": "Management events",
     "FieldSelectors": [{"Field": "eventCategory", "Equals": ["Management"]}]},
    {"Name": "Customer data reads and writes",
     "FieldSelectors": [
       {"Field": "eventCategory", "Equals": ["Data"]},
       {"Field": "resources.type", "Equals": ["AWS::S3::Object"]},
       {"Field": "resources.ARN", "StartsWith": ["<customer-data bucket ARN>/"]}
     ]}
  ]'

Then alarm on volume. S3 request metrics are opt-in and billed at the standard CloudWatch rate. Once they are on, BytesDownloaded shows how much was read from the bucket each minute [13]. Set the threshold well above a normal busy day and send it to a channel someone watches.

Bash
# Turn on request metrics for the whole bucket
aws s3api put-bucket-metrics-configuration \
  --bucket example-customer-data --id EntireBucket \
  --metrics-configuration '{"Id":"EntireBucket"}'

# Alarm when more than 5 GB is downloaded in 5 minutes
aws cloudwatch put-metric-alarm \
  --alarm-name customer-data-large-download \
  --namespace AWS/S3 --metric-name BytesDownloaded \
  --dimensions Name=BucketName,Value=example-customer-data Name=FilterId,Value=EntireBucket \
  --statistic Sum --period 300 --evaluation-periods 1 \
  --threshold 5000000000 --comparison-operator GreaterThanThreshold \
  --treat-missing-data notBreaching \
  --alarm-actions "<security-alerts topic ARN>"

If an alarm does fire and you think a role’s keys are out, the role’s Revoke active sessions option in the IAM console denies every token issued before that moment [20]. Then fix the cause, or the attacker can simply fetch new keys.

Check your own account

These checks only read settings. They change nothing, so they are safe to run in production.

Read-only check 1

Find instances that still accept IMDSv1

Run this in each Region you use. It lists every instance where IMDSv2 is optional, with the role it carries.

Instances with IMDSv1 still on
aws ec2 describe-instances \
  --filters "Name=metadata-options.http-tokens,Values=optional" \
  --query "Reservations[].Instances[].[InstanceId,IamInstanceProfile.Arn]" \
  --output table

ResultSafe answer: an empty list

What to do about it

Every instance listed still answers IMDSv1. Those with a role and a public-facing app come first. Check their MetadataNoToken count, then require IMDSv2 as in Fix 1.

Control that stops it
IMDSv2 required on every instance, plus the account default and the SCP [16][17][18].
Read-only check 2

Ask what an internet-facing role can really do

Pick the role on each public-facing server and ask the IAM policy simulator about storage actions. It answers allowed or denied without running anything [19].

Simulate storage actions for one role
ROLE_ARN=$(aws iam get-role --role-name example-waf-role \
  --query Role.Arn --output text)
aws iam simulate-principal-policy \
  --policy-source-arn "$ROLE_ARN" \
  --action-names s3:ListAllMyBuckets s3:ListBucket s3:GetObject \
  --query "EvaluationResults[].[EvalActionName,EvalDecision]" \
  --output table

ResultSafe answer: implicitDeny or explicitDeny on every line

What to do about it

With no resource named, the simulator tests against every resource. An allowed result means the role can do that to all your storage, which is the Capital One pattern.

Control that stops it
A role policy scoped to the one bucket and action the server needs.
Read-only check 3

Confirm GuardDuty is running

GuardDuty works per Region. A Region without a detector raises nothing.

Detectors in one Region
aws guardduty list-detectors --region eu-west-2

ResultSafe answer: one detector ID in every Region you use

What to do about it

An empty list means no findings at all in that Region, including server keys used outside AWS. Turn it on as in Fix 3.

Control that stops it
GuardDuty enabled in all Regions, with findings sent somewhere a person reads them [11].
Read-only check 4

Check that file reads on personal data are logged

Look at the event selectors on your main trail.

Event selectors on the trail
aws cloudtrail get-event-selectors --trail-name org-trail

ResultSafe answer: a Data selector covering each bucket that holds personal data

What to do about it

If there is only a management selector, nobody could say afterwards which files were read. Add data events as in Fix 3.

Control that stops it
S3 data events on sensitive buckets, plus a BytesDownloaded alarm [12][13].

Bradley ends the video on this: you might not be able to stop every mistake, but you want to know when one starts turning into a breach. Capital One’s mistake ran for almost four months before anyone told them.

Sources and further reading

References

Numbered sources support the practical points in this article. Video timestamps will appear when the related media is published.

  1. AdvisoryCapital One
    2019 Capital One Cyber Incident: What Happenedhttps://www.capitalone.com/digital/facts2019/

    The bank’s own account: the dates, what data was taken, how many people, and its updates up to January 2021.

  2. AdvisoryCapital One
    Capital One Announces Data Security Incidenthttps://www.capitalone.com/about/newsroom/capital-one-announces-data-security-incident/

    The 29 July 2019 announcement, including the 100 million US and 6 million Canadian figures.

  3. ReportUS Department of Justice
    United States v. Paige A. Thompson, superseding indictment (CR19-0159, W.D. Wash., filed 17 June 2021)https://www.justice.gov/media/1147181/dl

    The charges as tried: misconfigured web application firewalls, stolen role credentials, listing and copying buckets, VPN and Tor use, more than 30 victims.

  4. ArticleGovInfoSecurity
    Woman Arrested in Massive Capital One Data Breachhttps://www.govinfosecurity.com/woman-arrested-in-massive-capital-one-data-breach-a-12852

    Quotes the July 2019 criminal complaint: the GitHub file, the WAF role, the 700 folders and the IPredator and Tor addresses.

  5. ArticleKrebsOnSecurity
    Capital One Data Theft Impacts 106M Peoplehttps://krebsonsecurity.com/2019/07/capital-one-data-theft-impacts-106m-people/

    The email tip on 17 July, the arrest on 29 July, nearly 30 GB of application data and the attacker’s posts on Twitter and Slack.

  6. ArticleKrebsOnSecurity
    What We Can Learn from the Capital One Hackhttps://krebsonsecurity.com/2019/08/what-we-can-learn-from-the-capital-one-hack/

    Where the SSRF and metadata-service detail first appeared, from an unnamed source, with Amazon’s statement that a misconfigured firewall caused the intrusion.

  7. ArticleInvicta Group Cyber
    AWS SSRF Credential Theft: Attack, Detection and IMDSv2 Defencehttps://invictagroupcyber.com/blog/aws-ssrf-imdsv1-credential-theft

    Our step-by-step explanation of how SSRF makes a server hand over its IMDSv1 role credentials, and how IMDSv2 blocks it.

  8. Vendor DocAWS Security Blog
    Add defense in depth against open firewalls, reverse proxies, and SSRF vulnerabilities with enhancements to the EC2 Instance Metadata Servicehttps://aws.amazon.com/blogs/security/defense-in-depth-open-firewalls-reverse-proxies-ssrf-vulnerabilities-ec2-instance-metadata-service/

    The November 2019 launch of IMDSv2 and the attacks it was designed to stop.

  9. ArticleCBS News (Associated Press)
    Seattle software engineer gets probation for 2019 Capital One hackhttps://www.cbsnews.com/news/paige-thompson-capital-one-hack-probation-doj/

    The June 2022 verdict and the October 2022 sentence.

  10. AdvisoryOffice of the Comptroller of the Currency
    OCC Assesses $80 Million Civil Money Penalty Against Capital Onehttps://www.occ.gov/news-issuances/news-releases/2020/nr-occ-2020-101.html

    The regulator’s penalty of 6 August 2020 and its reasons.

  11. Vendor DocAWS
    GuardDuty IAM finding typeshttps://docs.aws.amazon.com/guardduty/latest/ug/guardduty_finding-types-iam.html

    Defines InstanceCredentialExfiltration.OutsideAWS, the Tor caller findings and the anomalous discovery finding.

  12. Vendor DocAWS
    Logging data eventshttps://docs.aws.amazon.com/awscloudtrail/latest/userguide/logging-data-events-with-cloudtrail.html

    S3 object reads are data events: not logged by default, and charged when switched on.

  13. Vendor DocAWS
    Creating a CloudWatch metrics configuration for all the objects in your buckethttps://docs.aws.amazon.com/AmazonS3/latest/userguide/configure-request-metrics-bucket.html

    S3 request metrics are opt-in, reported each minute and billed at the standard CloudWatch rate.

  14. ReportDatadog
    State of Cloud Securityhttps://www.datadoghq.com/state-of-cloud-security-2025/

    The 2025 edition, with the share of EC2 instances that enforce IMDSv2 and how it changed in a year.

  15. Vendor DocAWS
    Transition to using Instance Metadata Service Version 2https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/instance-metadata-transition-to-version-2.html

    How to find IMDSv1 use with the MetadataNoToken metric before switching it off.

  16. Vendor DocAWS
    Modify instance metadata options for existing instanceshttps://docs.aws.amazon.com/AWSEC2/latest/UserGuide/configuring-IMDS-existing-instances.html
  17. Vendor DocAWS
    Configure instance metadata options for new instanceshttps://docs.aws.amazon.com/AWSEC2/latest/UserGuide/configuring-IMDS-new-instances.html

    Account-level IMDS defaults per Region, and enforcing IMDSv2 at launch.

  18. Vendor DocAWS
    Example policies to control access the Amazon EC2 APIhttps://docs.aws.amazon.com/AWSEC2/latest/UserGuide/ExamplePolicies_EC2.html

    The source of the three IMDSv2 policy statements used below.

  19. Vendor DocAWS
    IAM policy testing with the IAM policy simulatorhttps://docs.aws.amazon.com/IAM/latest/UserGuide/access_policies_testing-policies.html
  20. Vendor DocAWS
    Revoke IAM role temporary security credentialshttps://docs.aws.amazon.com/IAM/latest/UserGuide/id_roles_use_revoke-sessions.html
Seen something like this in your own estate?

Find the same weaknesses before an attacker does

Talk to us