HackBlue

Tools · Cloud security · 08 / 24

How to review an S3 bucket policy for public and cross-account access

Read a bucket policy line by line, spot Principal "*" and missing conditions, enforce TLS and organization-only access, and test with the policy checker.

10 min read · practice only with authorization

What to look for

A bucket policy is a resource-based policy. The Principal element says who it applies to. "*" with an Allow means anyone, unless a Condition narrows it. An AWS account ARN means any principal in that account the account allows. Look at Action for s3:* or s3:Put* and at Resource for the bucket or every object.

Common good conditions are aws:SecureTransport to require TLS, aws:PrincipalOrgID to limit access to your AWS organization, and aws:SourceVpce to limit access to a VPC endpoint.

A defensible policy

The policy below denies any non-TLS request and denies access to principals outside the organization, then allows a specific role to read. Deny statements with conditions act as guardrails that later Allow statements cannot accidentally undo.

Use aws:PrincipalOrgID instead of listing every account, so new accounts in the organization work and outside accounts never do.

Snippet
{
  "Version": "2012-10-17",
  "Statement": [
    { "Sid": "TLSOnly", "Effect": "Deny", "Principal": "*", "Action": "s3:*",
      "Resource": ["arn:aws:s3:::finance-exports", "arn:aws:s3:::finance-exports/*"],
      "Condition": { "Bool": { "aws:SecureTransport": "false" } } },
    { "Sid": "OrgOnly", "Effect": "Deny", "Principal": "*", "Action": "s3:*",
      "Resource": ["arn:aws:s3:::finance-exports", "arn:aws:s3:::finance-exports/*"],
      "Condition": { "StringNotEquals": { "aws:PrincipalOrgID": "o-abc123xyz" } } },
    { "Sid": "ReaderRole", "Effect": "Allow",
      "Principal": { "AWS": "arn:aws:iam::111122223333:role/finance-reader" },
      "Action": ["s3:GetObject"], "Resource": "arn:aws:s3:::finance-exports/*" }
  ]
}

Lab: check before you apply

Write a deliberately bad policy that allows Principal "*" s3:GetObject on every object, and run it through Access Analyzer’s check-no-public-access command, which reports whether the policy grants public access. Then run the good policy above through the same check and through validate-policy for warnings.

Apply the good one to a sandbox bucket and test with the reader role, with a role from another account, and with an http:// request.

Snippet
aws accessanalyzer check-no-public-access --policy-document file://bad.json --resource-type AWS::S3::Bucket
aws accessanalyzer validate-policy --policy-document file://good.json --policy-type RESOURCE_POLICY --validate-policy-resource-type AWS::S3::Bucket
aws s3api put-bucket-policy --bucket finance-exports --policy file://good.json

Do not forget ACLs and other paths

If Object Ownership is not set to Bucket owner enforced, ACLs may still grant access. Set it so ACLs are disabled and the policy is the only control.

Presigned URLs, access points, and S3 replication to another account are other ways data can leave. Review them as part of the same exercise.

Diagram

Two bucket policies for the same reports

Who can read finance-exports.

Risky

Principal. "*"

Condition. None

Effect. Anyone on the internet can read every object.

Defensible

Guardrails. Deny non-TLS and deny outside the organization.

Principal. One reader role.

Effect. Only that role, over TLS, from the org.

Check yourself

1. Which policy element makes an Allow statement apply to everyone?

2. Which condition key limits access to your AWS organization?

3. Which Access Analyzer command tells you whether a policy grants public access before applying it?