Skip to content
network-securityintermediate#cloud-security#aws#iam#s3#misconfiguration

Cloud Misconfigurations: The Settings That Leak Data

The cloud settings that actually cause breaches: public object storage, over-broad IAM, exposed metadata services, open security groups and public snapshots, with the console and CLI checks for each.

A cloud misconfiguration has no patch. The service is doing what its documentation says it does, the setting says allow, and there is no vulnerable version to upgrade away from. That is what makes this class different from every other topic on this site: the fix is a configuration change, the detection is a comparison against intended state, and a vulnerability scanner looking for software versions walks straight past it.

The good news is that the set that actually causes incidents is small. Across public breach reports the same five settings recur: object storage readable by anyone, identity policies broader than the job needs, a metadata service that hands out credentials to whoever asks, network rules open to the internet, and resources marked shareable by a flag that appears on no dashboard. This guide covers each one, what the misconfiguration looks like in the console and the CLI, and the check that finds it.

Scope: Cloud Security Fundamentals owns the shared responsibility model and the architecture, and this page assumes it. SSRF Explained owns the application bug that reaches the metadata service, and Least Privilege Explained owns the principle that the IAM section applies. What follows is the settings themselves.

The five that matter

MisconfigurationWhat an attacker getsThe one control that closes it
Public object storageDirect read, and often write, of whatever is in the bucketAccount-level Block Public Access
Over-broad IAMEverything that identity can reach, which is usually more than intendedNamed actions on named resources, reviewed by Access Analyzer
Reachable IMDSRole credentials from any SSRF in any app on the hostIMDSv2 required, hop limit 1
Open security groupDirect network access to the service on that portNo 0.0.0.0/0 on management ports
Public snapshot or imageAn offline copy of the disk, including its secretsSharing audit on snapshots, AMIs and registries

The ordering is by how often each appears in an incident report rather than by severity. The metadata service is the most damaging of the five, because it converts an application-layer bug into account-level access, and it is third on the list only because the other two are more common.

Public object storage

The words "public bucket" describe an outcome that several independent mechanisms can produce, which is why teams are surprised by buckets they were sure were private.

Bucket policy. A resource policy on the bucket with "Principal": "*" and no condition grants the named actions to the internet. This is the mechanism behind most reported leaks, because it is the one people write deliberately for a legitimate reason (a static site, a public download) and then reuse as a template for a bucket that should not be public.

Object ACLs. Before April 2023, an object could be made public independently of the bucket by an ACL on the object itself, so a bucket could read as private while individual objects inside it were world-readable. New buckets now have ACLs disabled by default via the Object Ownership setting BucketOwnerEnforced, which makes object ACLs inert. Buckets created before that change, or buckets where ACLs were re-enabled to support an old client, still carry the exposure.

Presigned URLs. A presigned URL is a legitimate, signed grant of access to one object for a stated period. The maximum lifetime is seven days when signing with the CLI or an SDK, and a URL signed with temporary credentials dies when those credentials expire, which is often sooner. Either way a URL generated once and pasted into a ticket, a chat message or a mobile app bundle is a standing grant to anyone who reads it. Nothing about the bucket configuration reflects that the URL exists.

A distribution in front. A CloudFront distribution, an Azure Front Door endpoint or a load balancer can serve objects from private storage. The storage is genuinely private; the path to it is not.

The check

The account-level setting is the one that matters, because it overrides everything beneath it:

# Account level: this wins over any bucket policy or ACL below it
aws s3control get-public-access-block --account-id 123456789012
 
# Per bucket, for the exceptions
aws s3api get-public-access-block --bucket example-bucket
aws s3api get-bucket-policy-status --bucket example-bucket
aws s3api get-bucket-ownership-controls --bucket example-bucket

All four sub-settings should read true: BlockPublicAcls, IgnorePublicAcls, BlockPublicPolicy and RestrictPublicBuckets. They do different jobs. The first two concern ACLs, blocking new public ACLs and ignoring existing ones. The second two concern policies, rejecting a policy that grants public access and restricting access to a bucket that already has one. Turning on two of the four is a common half-fix.

On Google Cloud the equivalent is public access prevention, enforced at the organisation or bucket level. Watch specifically for the principal allAuthenticatedUsers, which reads like it means "authenticated to my organisation" and actually means any principal with a Google account, so it is functionally public. allUsers is the honest version of the same grant. On Azure, the relevant flag is allowBlobPublicAccess on the storage account, plus the container's own public access level.

Identity policies broader than the job

The IAM failure has a specific shape, and it is worth stating precisely because "least privilege" as a slogan does not tell anyone what to type.

A policy statement grants an effect on actions against resources, optionally under conditions. The two wildcards that cause the damage are "Action": "*" and "Resource": "*". Either one alone is sometimes defensible: a read-only auditing role legitimately reads everything, and a policy for one narrow action may legitimately apply account-wide. Both together in an Allow statement is an administrator.

The resource wildcard also has a subtlety that produces silently wrong policies. In S3, these two ARNs are different resources:

arn:aws:s3:::example-bucket        # the bucket itself
arn:aws:s3:::example-bucket/*      # the objects inside it

s3:ListBucket is a bucket action and needs the first form. s3:GetObject is an object action and needs the second. A policy that grants s3:GetObject on arn:aws:s3:::example-bucket grants an object action on a bucket resource, matches nothing, and denies by default; the application fails and somebody widens the policy to * to make the error go away. A policy meant to grant listing that uses the /* form fails the same way in the opposite direction. Both mistakes end in a wildcard that was never intended.

The other reliable source of surprise is the difference between a permission and an effective permission. What an identity can actually do is the intersection of its identity policies, any resource policy on the target, any permissions boundary, any service control policy from the organisation, and any session policy. Reading one document tells you what was granted, and it does not tell you what is possible.

# What can this role actually do against this resource, including all boundaries
aws iam simulate-principal-policy \
  --policy-source-arn arn:aws:iam::123456789012:role/app-role \
  --action-names s3:GetObject \
  --resource-arns "arn:aws:s3:::example-bucket/*"
 
# What is reachable from outside the account at all
aws accessanalyzer list-findings --analyzer-arn "$ANALYZER_ARN"

simulate-principal-policy is the answer to "would this work", and Access Analyzer is the answer to "who outside the account can reach this". Between them they cover the two questions a policy review is trying to ask.

A third check is worth scheduling: unused permissions. IAM records the last time each service was accessed by a principal, so a role granted 40 services and using 3 is visible as data rather than as an opinion.

JOB=$(aws iam generate-service-last-accessed-details \
  --arn arn:aws:iam::123456789012:role/app-role --query JobId --output text)
aws iam get-service-last-accessed-details --job-id "$JOB"

The metadata service

This is the misconfiguration that turns a routine web bug into an account compromise, and it deserves the detail.

Every EC2 instance can query a link-local address, 169.254.169.254, for information about itself. One of the paths under that address returns temporary credentials for the IAM role attached to the instance:

http://169.254.169.254/latest/meta-data/iam/security-credentials/<role-name>

The response is JSON containing an access key, a secret key and a session token. Those credentials carry every permission the role has.

Under IMDSv1, that is a plain GET with no headers required. Any component on the instance that can be made to issue an HTTP request to an attacker-chosen URL will fetch it: a server-side request forgery bug in the application, a URL preview feature, a webhook tester, a PDF renderer that follows remote images, a misconfigured forward proxy. The application never intended to talk to the metadata service and does not need to; it only needs to fetch a URL somebody else chose.

This is the Capital One breach of 2019. A server-side request forgery against a misconfigured web application firewall running on EC2 reached the metadata service, returned the credentials of the role attached to that instance, and those credentials could list and read S3 buckets. Roughly 100 million US and 6 million Canadian applicants' records left the account. Every individual component behaved as designed.

IMDSv2 breaks the chain by requiring a session token obtained through a PUT:

TOKEN=$(curl -sX PUT "http://169.254.169.254/latest/api/token" \
  -H "X-aws-ec2-metadata-token-ttl-seconds: 21600")
curl -s -H "X-aws-ec2-metadata-token: $TOKEN" \
  http://169.254.169.254/latest/meta-data/iam/security-credentials/

Three properties of that exchange matter. It is a PUT, and most SSRF primitives can only issue a GET. It requires setting a request header, which most SSRF primitives cannot do. And the service rejects any request carrying an X-Forwarded-For header, which closes the proxy path specifically.

Enforce it at the instance and, better, at the account:

# Per instance
aws ec2 modify-instance-metadata-options --instance-id i-0abc123 \
  --http-tokens required --http-put-response-hop-limit 1 --http-endpoint enabled
 
# Account default for every new instance in the region
aws ec2 modify-instance-metadata-defaults --http-tokens required

The hop limit is the second half of the control. It sets the TTL on the metadata response packet, so a limit of 1 means the response cannot survive a hop to a container on a bridged network or to a machine behind the instance. Containers on the default Docker bridge sit one hop away, which is exactly the case the setting exists for. If the role attached to an instance is only needed by one process, the metadata endpoint can also be disabled entirely with --http-endpoint disabled.

Azure and GCP have the same service at the same address with a different guard: both require a header (Metadata: true on Azure, Metadata-Flavor: Google on GCP) on every request, which provides the same protection against header-less SSRF that IMDSv2 provides on AWS.

Network rules open to the internet

A security group is a stateful allow-list attached to an instance or an interface. It has no deny rule. That single fact explains most of the confusion: a permissive rule cannot be overridden by a restrictive one, so the widest rule wins by definition and the only fix is removing it.

The rules that show up in incident reports are narrow and boring:

  • 0.0.0.0/0 on port 22 or 3389, exposing SSH or RDP to internet-wide scanning. Both are scanned continuously, and credential stuffing against them is fully automated. SSH Hardening Guide and RDP Security Guide cover what to do when the port genuinely must be reachable.
  • 0.0.0.0/0 on a database port, most often 3306, 5432, 27017, 6379 or 9200. Managed databases with a public endpoint and an open group are found by internet-wide scans within hours.
  • ::/0 left open when the IPv4 equivalent was tightened. The two address families are separate rules and are routinely fixed one at a time.
  • A group that references itself for east-west traffic and then also carries a wide ingress rule, so the intended internal path and an unintended external one both exist.

Because there is no deny, a network ACL is the only place a subnet-wide block can be written. NACLs are stateless, which means return traffic needs its own rule, and this is why teams that reach for them to fix a security group problem often break the application instead.

# Every group with a rule open to the world, and the port it opens
aws ec2 describe-security-groups --query \
  "SecurityGroups[?IpPermissions[?IpRanges[?CidrIp=='0.0.0.0/0']]].\
[GroupId,GroupName,IpPermissions[].FromPort]" --output table

Network Segmentation Guide covers what the internal rules should look like once the external ones are closed.

The sharing flags nobody reads

The last group is the most easily missed, because the resources involved do not appear on a list of internet-facing assets and their sharing state is one field deep in an API response.

EBS snapshots. A snapshot has a createVolumePermission attribute. Set it to all and any AWS account can create a volume from it and mount it. The volume contains whatever the instance's disk contained: application secrets, SSH keys, database files, the .env file nobody remembers writing.

AMIs. The same story with launchPermission. A public AMI built from a configured production instance ships that configuration, and often its credentials, to anyone who launches it.

RDS snapshots. A database snapshot can be shared with all accounts, which hands over the entire database offline. The live database can be locked down perfectly while a snapshot from three months ago is public.

Container registries. An ECR repository policy with a * principal, or a public registry entry, exposes the images. Images routinely contain build-time secrets in intermediate layers even when the final layer looks clean.

aws ec2 describe-snapshots --owner-ids self \
  --query "Snapshots[].SnapshotId" --output text | tr '\t' '\n' | \
  while read -r id; do
    aws ec2 describe-snapshot-attribute --snapshot-id "$id" \
      --attribute createVolumePermission \
      --query "CreateVolumePermissions[?Group=='all']" --output text | \
      grep -q all && echo "PUBLIC: $id"
  done

Run the equivalent for describe-images --owners self with launchPermission, and for describe-db-snapshots with --snapshot-type shared.

Why these persist

Knowing the list does not stop the list recurring, and the reasons are structural rather than educational.

Defaults changed, accounts did not. S3 defaults are materially safer than they were in 2019, and no default change is retroactive. An account created in 2017 carries 2017's defaults on every resource created since.

The permission that unblocks a deploy is permanent. An engineer debugging a failing job at 6pm widens a policy to *, the job succeeds, and nothing ever narrows it again. This is the single largest source of over-broad IAM in practice, and it is why unused-permission data matters more than policy review.

Infrastructure as code moves the misconfiguration rather than removing it. A Terraform module with a permissive default replicates that default across every environment that calls it, faster and more consistently than a human could. The scanning has to happen against the code as well as the live account.

Multi-account structures multiply the surface. Every account has its own Block Public Access setting, its own default security group, and its own metadata defaults. A control applied by hand in the account someone was thinking about is absent in the other forty.

Detection that keeps working

A one-off audit finds today's state. What is needed is a comparison against intended state that runs continuously.

  • A posture scanner on a schedule. Prowler and Scout Suite both run read-only against AWS, Azure and GCP and report against the CIS Benchmark for the provider. Read-only credentials, output to a place a human reads weekly.
  • Provider-native drift detection. AWS Config rules, Azure Policy and GCP Organization Policy evaluate resources as they change rather than when a scan runs. Organization Policy and service control policies additionally make some misconfigurations impossible to create, which beats detecting them.
  • Access Analyzer for the identity half, because "who outside this account can reach this resource" is the question that policy review keeps failing to answer.
  • Policy checks in the pipeline. Scanning Terraform or CloudFormation before apply catches the permissive module default once instead of catching its output in forty accounts.

The order matters. Making a misconfiguration impossible to create beats detecting it after the fact, detecting it in the pipeline beats detecting it in production, and detecting it in production beats reading about it in a breach report.

The verdict

Five settings account for most of the damage: object storage exposure, over-broad identity policies, a reachable metadata service, network rules open to the world, and public snapshots and images. Four of the five are closed by a single account-level control each, and all five are detectable with read-only credentials in an afternoon.

The metadata service is the one to fix first. Everything else on the list leaks the data in one resource, and that one converts any application bug on any host into the permissions of the role attached to it.

Sources & further reading