“Is anything in our AWS account public?” It’s the question that arrives before every audit, and after every news story about a leaked bucket. The console answers it one bucket at a time. The CLI can answer it for the whole account in a few seconds.

The catch is that “public” has more than one meaning, and checking only one of them gives you a false all-clear.

Three Ways a Bucket Becomes Public

  • A bucket policy that grants access to Principal: "*" without a restricting condition.
  • ACL grants to the AllUsers or AuthenticatedUsers groups.
  • Block Public Access (BPA) switched off. BPA is the override: with all four settings on, policy and ACL exposure is neutralized. With it off, the first two routes take effect.

A good audit checks all three.

Check the Account-Level Safety Net First

ACCOUNT_ID=$(aws sts get-caller-identity --query Account --output text)
aws s3control get-public-access-block --account-id "$ACCOUNT_ID"

If all four flags are true, account-wide BPA is on and the rest of the audit is mostly confirmation. A NoSuchPublicAccessBlockConfiguration error means nothing is set at account level, so continue to the loop.

Audit Every Bucket in One Loop

for b in $(aws s3api list-buckets --query 'Buckets[].Name' --output text); do
  policy=$(aws s3api get-bucket-policy-status --bucket "$b" \
    --query 'PolicyStatus.IsPublic' --output text 2>/dev/null || echo "no-policy")
  bpa=$(aws s3api get-public-access-block --bucket "$b" \
    --query 'PublicAccessBlockConfiguration.[BlockPublicAcls,IgnorePublicAcls,BlockPublicPolicy,RestrictPublicBuckets]' \
    --output text 2>/dev/null || echo "no-bpa")
  acl=$(aws s3api get-bucket-acl --bucket "$b" \
    --query "length(Grants[?Grantee.URI=='http://acs.amazonaws.com/groups/global/AllUsers' || Grantee.URI=='http://acs.amazonaws.com/groups/global/AuthenticatedUsers'])" \
    --output text 2>/dev/null || echo "error")
  echo "$b policy_public=$policy public_acl_grants=$acl bpa=[$bpa]"
done

How to read each line:

  • policy_public=True: the bucket policy makes it public.
  • public_acl_grants above 0: an ACL grants access to everyone.
  • bpa=[False ...] or no-bpa: nothing stops either of the above from taking effect.
  • error or no-policy: the call failed or there is no policy. Re-check any bucket that errors, since it may sit in another region.

The loop prints bucket names only. It checks bucket-level exposure; individual object ACLs are why IgnorePublicAcls matters.

No policy is not the same as private.

A bucket with no policy can still be public through an ACL. Read all three columns, not just the first.

Separate Intentional From Accidental

Before changing anything, answer these for every hit:

  • Is it a static website, a public dataset, or a CloudFront origin?
  • Who owns it? Check the tags and the CreateBucket event in CloudTrail.
  • What’s in it? List key names and prefixes only, and don’t download contents.
  • Is the whole bucket meant to be public, or just one prefix?

With many accounts, IAM Access Analyzer for S3 gives the same view across an organization without looping.

Lock It Down Without Breaking Things

For exposure nobody can justify, turn BPA on for the whole account:

aws s3control put-public-access-block --account-id "$ACCOUNT_ID" \
  --public-access-block-configuration \
  BlockPublicAcls=true,IgnorePublicAcls=true,BlockPublicPolicy=true,RestrictPublicBuckets=true

This will break buckets that are public on purpose, such as a static site served straight from S3. Put those behind CloudFront with origin access control first, or exempt them at bucket level, then enable it account-wide.

GCP and Azure Equivalents

The same question, different switches:

gcloud storage buckets get-iam-policy gs://BUCKET_NAME --format=json | grep -E 'allUsers|allAuthenticatedUsers'
az storage account list --query "[].{name:name, publicBlobAccess:allowBlobPublicAccess}" -o table

On GCP you’re looking for a public principal in the bucket’s IAM policy. On Azure, for the account-level public blob access setting left on.


Check the account-level safety net, loop over every bucket for policy, ACL and BPA, decide what’s intentional, then turn on Block Public Access. Run it on a schedule, or enable account-level BPA and Access Analyzer, so exposure is caught when it happens and not at the next audit.