OnCallReady

Lesson 35.24 · AWS I: CLI, IAM, S3 & KMS · 17 min read

S3 security: Block Public Access, bucket policies and object ownership

In plain words

A bucket has several locks on its door. The account owner can put a master lock on every bucket that says "never let the whole street in, whatever the signs say" (Block Public Access). Each bucket can also have a sign on the door listing who may come in (a bucket policy). An old system of name tags on each box (ACLs) is switched off by default today.

Going public means someone removed the master lock and put up a sign saying "everyone". The safe habit is to leave the master lock on and hand out time-limited tickets instead.

S3 security: who can read a bucket

"Our S3 bucket was public" is the headline of more data breaches than any other cloud mistake. AWS has spent years making it harder: since April 2023 every new bucket blocks public access and has ACLs disabled. But the defaults can be switched off with one call, old buckets predate them, and a bucket policy is a JSON document someone edits at 18:00 on a Friday. This lesson is the layers that decide who can read a bucket, how S3 decides that a policy is "public", and how to share data without making anything public.

Need to know: access to an object is decided by IAM policies, the bucket policy, Block Public Access (BPA: four switches, on the bucket and on the account, the stricter wins), Object Ownership (with BucketOwnerEnforced, the default, ACLs are disabled) and encryption. A policy is public when it allows "Principal": "*" without a condition that pins the caller to known values. BPA's BlockPublicPolicy refuses such a policy; RestrictPublicBuckets ignores the public part of one that already exists. To share an object, use a presigned URL or a CDN, not a public bucket.

The layers

LayerSet onDecides
IAM identity policiesusers, roleswhat your own principals may do in S3
Bucket policythe bucketwho else (other accounts, everyone, services) may do what, and extra Deny rules for everyone
Block Public Accessthe account (s3control) and each bucketa veto: public policies and public ACLs are refused or ignored
Object Ownershipthe bucketwho owns uploaded objects; BucketOwnerEnforced = the bucket owner, ACLs off
ACLs (legacy)bucket and objectsgrants by canonical user or group; off on modern buckets
Encryptionthe bucket default, each objectwhich key; with SSE-KMS the KMS key policy is another layer
$ aws s3api get-public-access-block --bucket try-press-111122223333
{
    "PublicAccessBlockConfiguration": {
        "BlockPublicAcls": true,
        "IgnorePublicAcls": true,
        "BlockPublicPolicy": true,
        "RestrictPublicBuckets": true
    }
}
$ aws s3control get-public-access-block --account-id 111122223333
aws: [ERROR]: An error occurred (NoSuchPublicAccessBlockConfiguration) when calling the GetPublicAccessBlock operation: The public access block configuration was not found

Additional error details:
AccountId: 111122223333
$ aws s3api get-bucket-ownership-controls --bucket try-press-111122223333
{
    "OwnershipControls": {
        "Rules": [
            {
                "ObjectOwnership": "BucketOwnerEnforced"
            }
        ]
    }
}
$ aws s3api get-bucket-policy --bucket try-press-111122223333
aws: [ERROR]: An error occurred (NoSuchBucketPolicy) when calling the GetBucketPolicy operation: The bucket policy does not exist

Additional error details:
BucketName: try-press-111122223333

The bucket has the 2023 defaults: all four BPA settings on, ACLs disabled, no policy. The account has no BPA configuration at all - each bucket is on its own. Turning BPA on for the account is the single most effective S3 control: it covers every bucket, including the ones created next year by someone who turned the bucket-level switch off.

The four switches

SettingEffect
BlockPublicAclsrefuse new public ACLs (PutBucketAcl, PutObjectAcl, PutObject with a public ACL)
IgnorePublicAclsignore public ACLs that already exist
BlockPublicPolicyrefuse a PutBucketPolicy that would make the bucket public
RestrictPublicBucketsif the bucket policy is public anyway, only AWS services and principals of the bucket owner's account can use it: anonymous and other accounts are denied

What "public" means to S3

S3 evaluates a bucket policy and calls it public if some Allow statement grants to everyone ("Principal": "*" or {"AWS": "*"}) and is not limited by a condition with a fixed value on keys such as aws:SourceIp (a specific range, not 0.0.0.0/0), aws:SourceVpce, aws:SourceVpc, aws:PrincipalOrgID, aws:PrincipalAccount, aws:SourceArn or aws:SourceAccount. The press team wants their logo world-readable:

$ cd ~/oncall-lab/labs/aws
$ cat try/press-public.json
{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Sid": "PublicRead",
            "Effect": "Allow",
            "Principal": "*",
            "Action": "s3:GetObject",
            "Resource": "arn:aws:s3:::try-press-111122223333/*"
        }
    ]
}
$ aws s3api put-bucket-policy --bucket try-press-111122223333 --policy file://try/press-public.json
aws: [ERROR]: An error occurred (AccessDenied) when calling the PutBucketPolicy operation: User: arn:aws:iam::111122223333:user/learner is not authorized to perform: s3:PutBucketPolicy on resource: "arn:aws:s3:::try-press-111122223333" because public policies are prevented by the BlockPublicPolicy setting in S3 Block Public Access.

BlockPublicPolicy refused it, and the error says why. To publish it anyway, someone must first switch BPA off for the bucket - which is exactly what happens in the incidents:

$ aws s3api delete-public-access-block --bucket try-press-111122223333
$ aws s3api put-bucket-policy --bucket try-press-111122223333 --policy file://try/press-public.json
$ aws s3api get-bucket-policy-status --bucket try-press-111122223333
{
    "PolicyStatus": {
        "IsPublic": true
    }
}
$ curl -s https://try-press-111122223333.s3.eu-central-1.amazonaws.com/press-release.txt
oncall-lab launches its on-call trainer.

get-bucket-policy-status is S3's own verdict, and the one auditors check. Now put the brakes back on without touching the policy, and watch RestrictPublicBuckets work:

$ aws s3api put-public-access-block --bucket try-press-111122223333 --public-access-block-configuration BlockPublicAcls=true,IgnorePublicAcls=true,BlockPublicPolicy=true,RestrictPublicBuckets=true
$ curl -s https://try-press-111122223333.s3.eu-central-1.amazonaws.com/press-release.txt
<?xml version="1.0" encoding="UTF-8"?>
<Error><Code>AccessDenied</Code><Message>Access Denied</Message><RequestId>BBE7F370AF079655</RequestId><HostId>Lx9w+/YzPovPcHufYUokrcb76FEOh5CdlKCVZKi/COlTrlDjX1oEaoxeZcFP4cHnCLop3rJjpr4j=</HostId></Error>
$ aws s3 cp s3://try-press-111122223333/press-release.txt -
oncall-lab launches its on-call trainer.

The public policy is still there, but anonymous requests are denied again, while your own account keeps access. That is the incident move: re-enable BPA first (seconds, reversible), then fix the policy calmly.

Bucket policy patterns worth knowing

A bucket policy with a Deny applies to everyone, admins included - which makes it the place for rules that must hold whatever IAM says:

$ cat try/deny-insecure.json
{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Sid": "DenyInsecureTransport",
            "Effect": "Deny",
            "Principal": "*",
            "Action": "s3:*",
            "Resource": [
                "arn:aws:s3:::try-press-111122223333",
                "arn:aws:s3:::try-press-111122223333/*"
            ],
            "Condition": {
                "Bool": {
                    "aws:SecureTransport": "false"
                }
            }
        }
    ]
}
$ aws s3api put-bucket-policy --bucket try-press-111122223333 --policy file://try/deny-insecure.json
$ curl -s http://try-press-111122223333.s3.eu-central-1.amazonaws.com/press-release.txt
<?xml version="1.0" encoding="UTF-8"?>
<Error><Code>AccessDenied</Code><Message>Access Denied</Message><RequestId>9A774FA9413C8090</RequestId><HostId>z4dVWntBw/IwcmW+i2YKi3ajxlAyyFb+4wvFwEf8JVUrp080WSBc2A9POffweQnUDsRguFFDrnCx=</HostId></Error>
$ aws s3 cp s3://try-press-111122223333/press-release.txt -
oncall-lab launches its on-call trainer.

ACLs: the old way, now off

ACLs (bucket and object grants to canonical user IDs or the groups AllUsers / AuthenticatedUsers) predate bucket policies. With Object Ownership set to BucketOwnerEnforced they are disabled: every object belongs to the bucket owner, and any request that tries to set an ACL other than private fails:

$ aws s3api put-object --bucket try-press-111122223333 --key public.txt --body try/press-public.json --acl public-read
aws: [ERROR]: An error occurred (AccessControlListNotSupported) when calling the PutObject operation: The bucket does not allow ACLs
$ aws s3api get-bucket-acl --bucket try-press-111122223333 --query 'Grants[].[Grantee.Type,Permission]' --output text
CanonicalUser	FULL_CONTROL

Old tools and tutorials still pass --acl public-read; on a modern bucket that is an error, not a silent public object - one more reason to leave ownership enforced.

Sharing without going public

In an interview: "How do you make sure no S3 bucket in the account is public?" - "Turn on Block Public Access at the account level (all four settings), keep Object Ownership on BucketOwnerEnforced, and protect the setting with an SCP that denies s3:PutAccountPublicAccessBlock. Then monitor: get-bucket-policy-status / IAM Access Analyzer findings for buckets shared outside the account. Share data with presigned URLs, CloudFront or explicit cross-account grants instead."

You can now: list the layers that decide S3 access, read and set Block Public Access at the bucket and account level, explain S3's definition of a public policy, contain a public bucket in seconds with BPA, write the TLS-only and cross-account bucket policy patterns, and explain why ACLs fail on modern buckets.

Why it helps

Public buckets are behind many of the data leaks in the news. Since April 2023 new buckets have Block Public Access on and ACLs disabled, but older buckets, copied templates and well-meant fixes still open them. On call you need to tell quickly whether a bucket is public, why, and how to close it without breaking the site that legitimately uses it.

The same layers decide every S3 permission question: IAM, the bucket policy, Block Public Access, Object Ownership and encryption. Knowing which one said no is how you fix access problems without opening a hole.

Commands in this lesson

aws cd cat curl

FAQ

What does S3 consider public?

A bucket policy is public if it grants access to a wildcard principal (* or AWS: *) without a condition that pins it to fixed values such as specific IP ranges, VPC endpoints, accounts or an organization. An ACL is public if it grants AllUsers or AuthenticatedUsers. get-bucket-policy-status returns IsPublic for the policy.

What do the four Block Public Access settings do?

BlockPublicAcls rejects new public ACLs, IgnorePublicAcls ignores existing ones, BlockPublicPolicy rejects a public bucket policy, and RestrictPublicBuckets limits an existing public policy to AWS service principals and the bucket owner's account. They exist at the account level and per bucket; the stricter setting wins.

How do I serve a static website without a public bucket?

Put CloudFront in front of the bucket and give the CloudFront distribution access with Origin Access Control: the bucket policy allows only the cloudfront.amazonaws.com service principal with a condition on the distribution's ARN. The bucket stays private behind Block Public Access, and you get HTTPS, caching and your own domain.

Why did put-bucket-acl fail with AccessControlListNotSupported?

The bucket uses Object Ownership BucketOwnerEnforced, the default since April 2023: ACLs are disabled, the bucket owner owns every object, and access is controlled only by policies. Calls that set ACLs fail. That is intended; use a bucket policy instead, or change Object Ownership only if a legacy system truly needs ACLs.

Why deny requests without TLS in a bucket policy?

S3 accepts plain HTTP on its endpoints. A statement that denies every s3 action when aws:SecureTransport is false forces encryption in transit for every caller, including misconfigured tools. It is a Deny, so it wins over any Allow, and it is one of the standard controls compliance scanners check.

In an interview Mid

How do you make sure no S3 bucket in an account is public?

Prevent it in layers: turn on Block Public Access at the account level with all four settings, keep Object Ownership on BucketOwnerEnforced so ACLs are disabled, and protect the account setting with an SCP that denies s3:PutAccountPublicAccessBlock. Detect with aws s3api get-bucket-policy-status and IAM Access Analyzer findings for buckets shared outside the account. When one is public, remove the wildcard statement from the bucket policy (or turn Block Public Access back on), check CloudTrail for who changed it, and share data with presigned URLs or CloudFront instead.

Also asked: What is the difference between a bucket policy and an ACL? · How do you serve a static website from S3 securely? · What does RestrictPublicBuckets do?

Practise this lesson in the terminal Free, in your browser - a real Ubuntu terminal to try it in, with missions that check your work.