AWS changed the default posture for new S3 buckets in April 2023, turning on Block Public Access and disabling ACLs across the console, CLI, SDKs, and CloudFormation for every bucket created from that point forward. That single default-flip closed off the most common path to accidental data exposure — for new buckets. Every bucket created before April 2023 kept whatever configuration it already had, and most organizations have thousands of those. That gap between "secure by default going forward" and "secure right now" is where most real-world AWS misconfigurations still live, and it is not limited to S3. The 2019 Capital One breach — 106 million customer records exposed — was not caused by one mistake but by three stacked ones: a misconfigured web application firewall, an EC2 instance still running the original Instance Metadata Service (IMDSv1) with no request authentication, and an IAM role attached to that instance with far more S3 access than the workload needed. Open security groups round out the list — port 22 or 3389 opened to 0.0.0.0/0 remains one of the most frequently flagged findings in any cloud security posture review. This cheat sheet covers all three, with the CLI commands to find and fix them.
Why is public S3 exposure still a top misconfiguration despite AWS's 2023 default change?
Because the 2023 change only protects buckets created after it shipped. AWS's own "What's New" announcement, previewed in December 2022 and rolled out in April 2023, enabled S3 Block Public Access and disabled ACLs by default for all newly created buckets — but existing buckets retain whatever access settings they had before the change, silently, forever, until someone touches them. A bucket created in 2019 with a permissive bucket policy or public ACL is exactly as exposed today as it was then; the default change does nothing to it. Audit every bucket rather than assuming the fleet is covered:
aws s3api list-buckets --query 'Buckets[].Name' to enumerate, then for each bucket aws s3api get-public-access-block --bucket <name> to check its current state. Remediate any bucket missing full lockdown with:
aws s3api put-public-access-block --bucket <name> --public-access-block-configuration BlockPublicAcls=true,IgnorePublicAcls=true,BlockPublicPolicy=true,RestrictPublicBuckets=true
Run this as a fleet-wide script, not a one-off — new accounts, cross-account replication targets, and Terraform-managed buckets are common blind spots that predate the 2023 default or override it explicitly.
How did IAM over-permissioning turn one SSRF bug into 106 million exposed records?
The Capital One breach, documented independently by Krebs on Security, security research firm Appsecco, and later academic case-study analysis, shows exactly how a chain of misconfigurations compounds. Attacker Paige Thompson exploited a misconfigured ModSecurity WAF sitting in front of a Capital One web application to achieve server-side request forgery (SSRF). The SSRF request reached the EC2 instance metadata service at 169.254.169.254 — at the time running IMDSv1, which returns IAM role credentials over plain HTTP with no session token or request signing required. Those credentials belonged to an IAM role named ISRM-WAF-Role, which had been granted S3 list and read permissions well beyond what the WAF workload actually needed. The attacker used those temporary credentials to list and exfiltrate data from more than 700 S3 buckets. No single fix would have stopped this chain, but removing any one link would have: a same-origin-restricted WAF, IMDSv2, or a role scoped to only the buckets the WAF actually touched.
How do you stop SSRF from turning into credential theft on EC2?
Enforce IMDSv2 fleet-wide. IMDSv1's stateless, header-free HTTP requests are exactly what let the Capital One SSRF reach the metadata endpoint; IMDSv2 requires a session token fetched via a PUT request first, which most SSRF payloads (limited to GET-style redirects) cannot construct. Check current exposure with aws ec2 describe-instances --query 'Reservations[].Instances[].[InstanceId,MetadataOptions.HttpTokens]' and enforce the token requirement with:
aws ec2 modify-instance-metadata-options --instance-id <id> --http-tokens required --http-endpoint enabled
Apply this at the account level going forward too, via aws ec2 modify-instance-metadata-defaults --http-tokens required --http-put-response-hop-limit 1, so newly launched instances inherit the safer default instead of relying on every team remembering to set it per-launch.
Why does IAM over-permissioning keep happening even when teams know least privilege is the goal?
Because most IAM policies are written once, at launch, based on what the developer believes the workload needs — and almost never revisited as usage patterns settle. Wildcard actions (s3:*) and wildcard resources ("Resource": "*") are the fastest way to unblock a deploy under deadline pressure, and they rarely get tightened afterward because nothing breaks when a policy is too permissive — only when it's too restrictive, which is what gets noticed and fixed. AWS IAM Access Analyzer generates findings specifically for this pattern by comparing granted permissions against actual CloudTrail usage. Before attaching a new or modified policy, simulate it against the specific actions and resources the role should need:
aws iam simulate-principal-policy --policy-source-arn <role-arn> --action-names s3:GetObject s3:PutObject --resource-arns arn:aws:s3:::<bucket>/*
Pair this with aws accessanalyzer list-findings run on a schedule, since Access Analyzer flags unused permissions and external-account access grants that a one-time simulation at policy-creation time won't catch six months later.
Why do open security groups remain a persistent audit finding?
Because a security group opened to 0.0.0.0/0 for administrative access — SSH on 22, RDP on 3389 — usually gets created for a one-time debugging session and then never closed, since nothing forces its removal and it doesn't show up in application logs or cost reports. It sits quietly until a port scan finds it. Find every rule with unrestricted ingress on sensitive ports:
aws ec2 describe-security-groups --filters Name=ip-permission.cidr,Values=0.0.0.0/0 --query 'SecurityGroups[].[GroupId,GroupName]'
Then revoke the overly broad rule directly:
aws ec2 revoke-security-group-ingress --group-id <sg-id> --protocol tcp --port 22 --cidr 0.0.0.0/0
The durable fix is architectural, not just a revoked rule: replace inbound SSH/RDP entirely with AWS Systems Manager Session Manager, which requires no open inbound port at all and logs every session centrally, or at minimum scope the CIDR to a corporate VPN range and enforce it with an SCP that blocks 0.0.0.0/0 ingress rule creation account-wide.