Course outline

10.2

Who Can Read My Bucket? IAM, Bucket Policies and Block Public Access

User vs resource policies, cross-account access, ACLs and Object Ownership, and the settings that prevent leaks.

26 min Reading + animations

Tip: tap or hover any dotted-underlined term to see what it means.

By the end of this lesson you will be able to

  • Name the tools that control access to S3 and say which ones grant and which ones only limit: IAM policies, bucket policies, ACLs, Block Public Access and Object Ownership.
  • Read and write a bucket policy: Effect, Principal, Action, Resource, Condition, and the difference between the bucket ARN and the object ARN.
  • Decide any S3 request by hand: same account (one Allow is enough), another account (both sides must allow), anonymous (only the bucket can allow), with explicit Deny and Block Public Access on top.
  • Pick the right pattern for the classic cases: a public website, an application on EC2, a partner account, a whole organization, HTTPS only, access only from your VPC.
  • Explain each of the four Block Public Access settings, the levels they apply at, and why a public policy can stop working without anyone touching it.
  • Explain Object Ownership and why ACLs have been off by default since April 2023, and find exposed buckets with IAM Access Analyzer for S3.

#Every bucket starts private

#Two questions on every request

Every request that reaches S3 is answered in two steps, exactly like every other AWS request (lesson 2.2):

  1. Who is asking? S3 looks at the signature. A signed request comes from a principal: an IAM user, a role session (an EC2 instance, a Lambda function, a person who assumed a role), the root user, or an AWS service. A request with no signature is anonymous: a browser opening a link, curl with no credentials.
  2. Is this principal allowed to do this action on this resource? S3 collects every policy that applies and decides Allow or Deny.

A brand-new bucket answers the second question with no for everyone except its own account’s principals that have IAM permissions. Nobody outside the account, and no anonymous visitor, can read anything until someone deliberately grants it. Since April 2023 every new bucket also comes with two extra locks already closed: Block Public Access is on, and ACLs are disabled. Both are explained later in this lesson.

#The tools at a glance

ToolAttached toGrants access?Typical use
IAM policy (user-based, identity-based)A user, group or roleYesYour own people and applications
Bucket policy (resource-based)The bucketYesPublic read, other accounts, organization-wide sharing, guard rails with Deny
ACL (bucket ACL, object ACL)The bucket or one objectYes, but disabled by defaultLegacy; rare per-object cases
Block Public AccessOrganization, account, bucket, access pointNo, only stops public grantsLeak prevention
Object OwnershipThe bucketNo, decides who owns uploaded objects and whether ACLs countKeep Bucket owner enforced
Access point policyAn access pointYesPer-team entry points to a shared bucket (lesson 10.9)
VPC endpoint policyA gateway or interface endpointLimits only“From this VPC, only these buckets” (lesson 10.10)
SCP and RCPOrganization, OU, accountLimits onlyCompany-wide limits (lesson 2.2, chapter 20)
KMS key policyA KMS keyYes, for the keyObjects encrypted with SSE-KMS also need the key (lesson 10.8)

The slides sum it up as user-based (IAM policies: which API calls this user may make) and resource-based (bucket policies, object ACLs, bucket ACLs). The rest of this lesson takes them one by one, then puts them together.

#User-based access: IAM policies

An IAM policy is attached to a user, a group or a role and says which actions that identity may perform on which resources. You met the JSON in lesson 2.1. For S3 the important detail is which ARN each action needs:

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Sid": "ListTheInvoicesPrefix",
      "Effect": "Allow",
      "Action": "s3:ListBucket",
      "Resource": "arn:aws:s3:::shop-media-example",
      "Condition": { "StringLike": { "s3:prefix": "invoices/*" } }
    },
    {
      "Sid": "ReadAndWriteInvoices",
      "Effect": "Allow",
      "Action": ["s3:GetObject", "s3:PutObject"],
      "Resource": "arn:aws:s3:::shop-media-example/invoices/*"
    }
  ]
}
  • Bucket-level actions such as s3:ListBucket act on the bucket ARN, arn:aws:s3:::shop-media-example, with no slash.
  • Object-level actions such as s3:GetObject, s3:PutObject and s3:DeleteObject act on object ARNs, arn:aws:s3:::shop-media-example/invoices/*.
  • Put both forms in a policy when you need both. A policy with only …/* lets you read files whose names you already know, but listing fails with AccessDenied. A policy with only the bucket ARN lets you list, but reading fails.
  • S3 ARNs have no Region and no account ID, because bucket names are global (lesson 10.1).

#Applications get roles, never keys

The slides show two pictures for user-based access: an IAM user with an IAM policy, and an EC2 instance with an instance role. The second one is the pattern to remember. An application on EC2 never gets an access key copied onto the disk; it gets an IAM role through an instance profile, and the SDK picks up short-lived credentials automatically (lesson 2.4). Lambda functions, ECS tasks and other services work the same way with their execution or task roles.

#Resource-based access: bucket policies

#What a bucket policy looks like

A bucket policy is a JSON policy attached to the bucket itself. Because it sits on the resource, it must say who it is about: it has a Principal element, which IAM policies do not have.

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Sid": "PublicReadForProductPhotos",
      "Effect": "Allow",
      "Principal": "*",
      "Action": "s3:GetObject",
      "Resource": "arn:aws:s3:::shop-media-example/photos/*"
    }
  ]
}
ElementMeaningExamples
EffectAllow or Deny"Allow"
PrincipalWhom the statement is about"*" (everyone, including anonymous), {"AWS": "arn:aws:iam::444455556666:root"} (an account), {"AWS": "arn:aws:iam::111122223333:role/shop-web-role"}, {"Service": "logging.s3.amazonaws.com"}
ActionThe S3 API calls"s3:GetObject", "s3:*"
ResourceThe bucket and/or its objectsarn:aws:s3:::shop-media-example, arn:aws:s3:::shop-media-example/photos/*
Condition (optional)Extra tests on the requestaws:SecureTransport, aws:SourceIp, aws:SourceVpce, aws:PrincipalOrgID, s3:x-amz-server-side-encryption

A few rules worth knowing:

  • A bucket has one bucket policy, up to 20 KB. When it grows too big because many teams share the bucket, the answer is S3 Access Points (lesson 10.9).
  • The Resource in a bucket policy must be this bucket or objects inside it.
  • Principal: "*" really means everyone, signed or not. Use it only for truly public data, or together with a Condition that narrows it, such as an organization ID.
  • Only the bucket owner’s account (with s3:PutBucketPolicy permission) can change the policy.

#What bucket policies are used for

The slides and the notes list the jobs a bucket policy does. Each one has its own section below:

  • grant public access, for example read-only for a static website;
  • grant access to another account (cross-account);
  • force HTTPS and force encryption on upload;
  • restrict by network: only from certain IP ranges, or only through your VPC endpoint;
  • share with a whole organization.

#ACLs, the old way

Before bucket policies existed (2006), S3 had access control lists. An ACL is a list of grants on one bucket or one object: “account X may READ”, “the AllUsers group may READ”. ACLs are coarse (READ, WRITE, READ_ACP, WRITE_ACP, FULL_CONTROL), cannot have conditions, cannot Deny, hold at most 100 grants, and live on every object separately, which makes them very hard to audit. The slides call object ACLs “finer grain” because you can set one per object; that is precisely what makes them hard to keep track of.

Two predefined groups caused many famous leaks: AllUsers (anyone on the internet) and AuthenticatedUsers (anyone with any AWS account in the world, not just your users). S3 treats a grant to either group as public.

Today AWS recommends keeping ACLs disabled and doing everything with policies. New buckets have ACLs disabled by default; the section on Object Ownership explains what that means.

#How S3 decides

#The rules from lesson 2.2, applied to S3

S3 uses the same evaluation logic as the rest of AWS (lesson 2.2), with a few extra inputs. The slides compress it into one sentence: a principal can access an object if the IAM permissions allow it OR the resource policy allows it, AND there is no explicit Deny. That sentence is true inside one account. The full picture:

  1. Block Public Access first switches off any public grant it is set to block (details later). It never grants.
  2. Explicit Deny anywhere wins. A Deny in the caller’s IAM policy, the bucket policy, an SCP or RCP, or an endpoint policy ends the evaluation. No Allow can undo it, not even an administrator’s.
  3. Then S3 needs Allows, and how many depends on who is asking:
Who is askingWhat must allow it
A principal in the bucket owner’s accountThe IAM policy or the bucket policy (union)
A principal in another accountThe caller’s IAM policy and the bucket policy (both)
AnonymousThe bucket policy (or, with ACLs enabled, an ACL), since there is no IAM side at all
Nobody allows itImplicit deny: 403 AccessDenied

On top of that, organization SCPs and RCPs, permissions boundaries and session policies can only limit (lesson 2.2), and for objects encrypted with SSE-KMS the caller also needs permission on the KMS key (lesson 10.8).

Who is asking? Four principals, one bucket

The shop’s bucket lives in account 111122223333. An IAM user and an EC2 instance in the same account, an anonymous visitor on the internet, and a user in a partner account all send the same kind of request: read one object. Which policy decides each one?

Scenario

User-based access: the user’s own IAM policy allows it.

Shop account111122223333
Courier account444455556666
shop-media-example
IAM user rina
Web app
Visitor
Courier user
GetObject

Principal

rina

Request

GET invoices/…/inv-1043.pdf

Decided by

…

Result

…

Step 1 of 2
1

Rina, an IAM user in the shop’s account, asks for invoices/2026/10/inv-1043.pdf. She signs the request with her credentials, so S3 knows exactly who she is.

#Why cross-account needs both sides

The rule “both sides must allow” protects both companies:

  • The bucket owner must agree, otherwise anyone could write an IAM policy for someone else’s bucket and read it.
  • The caller’s account must agree, otherwise a stranger’s bucket policy could hand your employees permissions your security team never approved.

When a bucket policy names a whole account (arn:aws:iam::444455556666:root), it means “I trust that account to decide which of its users and roles get this”. It does not give every user in that account access automatically. Their administrator still has to grant it with IAM.

Cross-account access: both halves

A user in the courier’s account reads labels/ in the shop’s bucket. Within one account a single Allow is enough; across accounts S3 needs an Allow from the caller’s account and from the bucket owner.

Scenario

Courier user444455556666IAM: allow
Courier IAM444455556666
Amazon S3shop-media-example
Role in shop111122223333
IAM policy: allow labels/*
Step 1 of 4
1

Half one, in the caller’s account: the courier’s admin attaches an IAM policy that allows s3:GetObject on arn:aws:s3:::shop-media-example/labels/*. Without it, the courier account has not allowed its own user to go anywhere.

#The 403 that hides a 404

If a principal may not list the bucket (no s3:ListBucket) and asks for a key that does not exist, S3 answers 403 AccessDenied, not 404 Not Found. This is deliberate: otherwise an outsider could probe which file names exist. When you troubleshoot a 403 on a GET, first check that the key really exists. With s3:ListBucket allowed, a missing key gives 404 NoSuchKey.

Self-checkQuestion

A user in account 444455556666 tries to read an object in a bucket owned by account 111122223333. The bucket policy allows s3:GetObject for account 444455556666. The user has no IAM policies at all. What happens?

#The classic access patterns

#Public read for a website

The slides’ first example: an anonymous visitor on the web reads files from a bucket, allowed by a bucket policy. The shop does it for product photos, with the policy shown earlier (Principal: "*", s3:GetObject, photos/*). Three things must be true:

  1. Block Public Access must not block it, at the bucket and account level (and organization, if one is set).
  2. The bucket policy grants s3:GetObject to "*" on the right prefix. Grant read, never write: a public s3:PutObject turns your bucket into free hosting for anyone’s files, on your bill.
  3. The Resource is exact. photos/* is public; invoices/ stays private. Writing shop-media-example/* by mistake is how leaks happen.

Website hosting itself (endpoints, index and error documents, the 403 trap) is lesson 10.3. Today the better design for most websites is a private bucket with CloudFront in front, using Origin Access Control, so the bucket itself never becomes public; that comes in chapter 11.

#An application on EC2

Covered above: an IAM role on the instance with an IAM policy for exactly the prefixes it needs. Same account, so no bucket policy is required. Add a bucket-policy Deny only if you want a guard rail (for example “only through our VPC endpoint”).

#Another account: two patterns

The slides show cross-account access with a bucket policy. In practice there are two ways, and the exam expects you to know both:

Bucket policy + IAM policyRole in the bucket’s account
HowThe bucket policy names the other account (or role); its admin grants its users with IAMThe bucket owner creates a role that the other account may assume (lesson 2.4)
The caller acts asItself, from its own accountA role session of the bucket owner’s account
Policies neededBoth sidesTrust policy + role’s permissions; no bucket policy
Good forServices in the other account that cannot switch roles; many buckets shared to one partnerKeeping all permissions, audit and session limits in the owner’s account

In both patterns the owner should grant the smallest set: one prefix, read-only, maybe with a condition.

#A whole organization

Listing 40 account IDs in a bucket policy, and updating it every time an account is added, does not scale. Use the condition key aws:PrincipalOrgID:

{
  "Sid": "AllAccountsInOurOrganization",
  "Effect": "Allow",
  "Principal": "*",
  "Action": "s3:GetObject",
  "Resource": "arn:aws:s3:::shop-shared-config-example/*",
  "Condition": { "StringEquals": { "aws:PrincipalOrgID": "o-a1b2c3d4e5" } }
}

Principal: "*" with a fixed organization ID is not public in S3’s eyes, so Block Public Access does not stop it. Every account in the organization, including accounts added next year, can read, provided its own IAM policies allow it too.

#Guard rails: Deny statements in a bucket policy

A bucket policy can take access away as well as give it. Because an explicit Deny beats every Allow, a few Deny statements turn the bucket into its own security guard, whatever IAM policies people write later.

#HTTPS only (encryption in transit)

The notes’ main exam trick:

{
  "Sid": "DenyInsecureTransport",
  "Effect": "Deny",
  "Principal": "*",
  "Action": "s3:*",
  "Resource": [
    "arn:aws:s3:::shop-media-example",
    "arn:aws:s3:::shop-media-example/*"
  ],
  "Condition": { "Bool": { "aws:SecureTransport": "false" } }
}

aws:SecureTransport is true when the request arrived over TLS (HTTPS) and false for plain HTTP. The Deny catches every plain-HTTP request, from anyone. Lesson 10.8 covers TLS and S3 encryption in depth.

#Only from our network

  • aws:SourceVpce: only requests that came through a specific VPC endpoint. The usual way to say “invoices can be read only from inside our VPC” (gateway endpoints are in lesson 10.10). aws:SourceVpc does the same for any endpoint in one VPC.
  • aws:SourceIp: only requests from certain public IP ranges, such as the office’s 198.51.100.0/24. It does not work for traffic that arrives through a VPC endpoint, because such a request carries no public address for it to match; use the endpoint keys there.

Write these as Deny … unless (StringNotEquals, NotIpAddress) rather than “Allow only if”: an Allow with a condition does nothing against someone whose IAM policy already allows s3:*, while a Deny stops everyone.

Deny statements as guard rails

A bucket policy can also take access away. The shop adds two Deny statements to its invoices bucket policy: refuse plain HTTP, and refuse requests that do not come through the VPC endpoint. A Deny beats every Allow, even an administrator’s.

Scenario

Deny when aws:SecureTransport is false.

Web app on EC210.0.1.25
Admin laptop203.0.113.40
Gateway endpointvpce-1a2b3c4d
Amazon S3invoicesforce HTTPS
Deny if SecureTransport = false
Step 1 of 5
1

Statement: Effect Deny, Principal "*", Action s3:*, Resource the bucket and its objects, Condition Bool aws:SecureTransport "false". It matches every request that did not use TLS.

#Do not lock yourself out

A Deny with Action: "s3:*" also denies s3:PutBucketPolicy and s3:DeleteBucketPolicy. If the condition excludes the people who maintain the bucket, nobody can fix the policy any more. Two habits prevent it:

  • Add an exception for an admin role: "Condition": {"ArnNotEquals": {"aws:PrincipalArn": "arn:aws:iam::111122223333:role/storage-admin"}} next to the other condition.
  • If it happens anyway, the account’s root user can always delete the bucket policy. In an AWS Organizations member account without root credentials, the management account can do it as a centrally managed privileged root task.

#Block Public Access

#Why it exists

For years, S3 data leaks made the news: voter rolls, hospital records, defence files, all in buckets someone had made public with one wrong policy or one AllUsers ACL. In November 2018 AWS added S3 Block Public Access: settings that override policies and ACLs so that public access cannot happen, however the bucket is configured. As the slide says: if you know a bucket should never be public, leave them on.

#The four settings

SettingActs whenWhat it does
BlockPublicAclsSomeone writes an ACLRejects PutBucketAcl, PutObjectAcl and uploads that carry a public ACL. Existing ACLs are untouched.
IgnorePublicAclsA request is evaluatedS3 ignores every public ACL on the bucket and its objects, as if it were not there.
BlockPublicPolicySomeone writes a bucket (or access point) policyRejects s3:PutBucketPolicy if the new policy is public. Existing policies are untouched.
RestrictPublicBucketsA request is evaluatedIf the bucket’s policy is public, only AWS service principals and principals of the owner’s own account can use it. Anonymous users and all other accounts are refused.

Two pairs: one setting in each pair stops new public grants, the other disarms existing ones. The settings are independent, and the console offers one checkbox, “Block all public access”, that turns on all four.

#What counts as public

S3 starts by assuming a bucket policy is public and then looks for a reason it is not. A statement is not public if it grants access only to fixed values of:

  • specific principals: an account, a user, a role, a service principal;
  • aws:PrincipalOrgID (your organization);
  • aws:SourceIp with a range no wider than /8 for IPv4 (private RFC 1918 ranges are fine);
  • aws:SourceVpc, aws:SourceVpce, aws:SourceArn, aws:SourceAccount, aws:SourceOwner, aws:userid, s3:DataAccessPointArn, s3:DataAccessPointAccount.

So Principal: "*" on its own is public; Principal: "*" with aws:SourceVpce = vpce-1a2b3c4d is not. An ACL is public if it grants anything to AllUsers or AuthenticatedUsers.

#Levels and precedence

You can set Block Public Access at four levels:

  • Access point: set when the access point is created; it cannot be changed later.
  • Bucket: applies to the bucket and its access points.
  • Account: applies to every bucket and access point the account owns, now and in the future, in all Regions (it can take a little time to reach every Region).
  • Organization (new, November 2025): an AWS Organizations policy that turns on all four settings for chosen accounts or OUs, all or nothing, and new member accounts inherit it.

S3 always applies the most restrictive combination. If the account says “block” and the bucket says “allow public”, the bucket is blocked. To publish a public website you must allow it at every level that applies. AWS recommends turning all four on at the account level, and keeping the few public buckets you really need in a separate account.

Defaults: since April 2023 all four settings are on for every new bucket (console, CLI, SDK and CloudFormation alike). Existing buckets were not changed. The account-level settings are not turned on for you; that is your job.

Block Public Access in action

Four switches that never grant anything: they only stop or ignore public grants. Two act when someone tries to write a public policy or ACL, two act when a request is evaluated.

Scenario

BlockPublicPolicy rejects the write: the policy is never saved.

Admin111122223333
Block Public Access4 settingsall 4 on
Amazon S3shop-media-example
Visitoranonymous
PutBucketPolicy, Principal "*"
Step 1 of 3
1

A new bucket, all four settings on (the default since April 2023). A developer pastes a public-read policy to share one file quickly.

#Object Ownership and ACLs

#The problem it solves

With ACLs, the account that uploads an object owns it, even in your bucket. In the old days, when a partner uploaded a file into your bucket, the file belonged to the partner, and its default ACL gave full control only to the partner. You paid for storing a file you could not read. The workaround was to force every uploader to send the bucket-owner-full-control ACL.

S3 Object Ownership (2020, with the “ACLs disabled” option added in November 2021) fixes this with one bucket setting:

SettingACLsWho owns new objects
Bucket owner enforced (default since April 2023)Disabled: they no longer affect access at allThe bucket owner owns every object, new and existing
Bucket owner preferredEnabledThe bucket owner, if the upload carries bucket-owner-full-control; otherwise the writer
Object writerEnabledThe uploading account (the original behaviour)

With Bucket owner enforced:

  • policies alone decide access: IAM, bucket policies, access point policies, endpoint policies, SCPs, RCPs;
  • uploads with no ACL, or with bucket-owner-full-control, are accepted; any other ACL fails with 400 AccessControlListNotSupported;
  • reading ACLs still works (you see full control for the owner only), and you can re-enable ACLs later, which restores the old ACLs.

Before switching an old bucket, move any access that ACLs still give (for example a public-read ACL on some files, or log delivery to the bucket) into the bucket policy, otherwise that access stops. AWS recommends keeping ACLs disabled except in the rare case where you must control access for each object individually. A few older features still require ACLs on the target bucket, for example CloudFront’s legacy standard logging; you will meet that in chapter 11.

Who owns an uploaded object?

The courier’s system uploads delivery proofs into the shop’s bucket. With the old Object writer setting the courier would own those files; with Bucket owner enforced, the default since April 2023, the shop owns everything and ACLs are switched off.

Scenario

The uploader owns the object, and the bucket owner cannot read it.

Courier system444455556666
Amazon S3shop-media-exampleACLs on: Object writer
Shop analyst111122223333
PutObject proofs/DLV-88.jpg
Step 1 of 4
1

An old bucket uses Object Ownership “Object writer”. The bucket policy lets the courier account upload to proofs/. The courier uploads a photo without any ACL.

#Finding exposed buckets: IAM Access Analyzer for S3

Policies, ACLs, access points and settings interact, and people make mistakes. IAM Access Analyzer for S3 shows you, in the S3 console, every general purpose bucket that can be reached from outside your account:

  • Public access findings: anyone on the internet can reach the bucket.
  • Cross-account access findings: other AWS accounts can, including accounts outside your organization.

For each bucket it shows how it is shared (bucket policy, bucket ACL, access point policy, Multi-Region Access Point policy) and the access level (list, read, write, permissions, tagging). From there you can:

  • block all public access to a bucket with one click;
  • archive a finding that is intended (the public website, the courier share), which records that someone reviewed it; archived buckets stay listed so you always know they are shared;
  • download a CSV report for auditors.

It costs nothing extra, but it needs an external access analyzer created in IAM Access Analyzer in each Region where you have buckets (with the account as the zone of trust). New or changed bucket policies and ACLs show up in findings within about 30 minutes; account-level setting changes can take up to 6 hours.

#Try the decision yourself

Pick a scenario, then change one switch at a time: the caller, the IAM policy, the bucket policy, HTTPS, ACLs and the four Block Public Access settings. Watch which check decides.

Will S3 return the file? Decide it yourself

One GET for one object in shop-media-example (account 111122223333). Pick a scenario, or set every switch yourself, and watch which check decides and why.

Scenarios
Who is asking
Caller’s IAM policy
Bucket policy
Request sent over
Deny when SecureTransport is false
Object Ownership
Object ACL grants AllUsers read
Block Public Access
  1. 1. Block Public Access

    no public grant to act on

  2. 2. Any Deny?

    no Deny anywhere

  3. 3. Bucket side

    no Allow for this caller

  4. 4. Caller side

    IAM policy allows

  5. 5. Combine

    same account: either is enough

Decision: Allowed: 200 OK

i

The web app’s role has an IAM policy that allows s3:GetObject. Same account, so this Allow alone is enough; the bucket policy does not need to mention the role. The caller is in the bucket owner’s account, so the bucket side and the caller side are a union: one Allow, and no Deny, is enough.

#What goes wrong: a 403 checklist

SymptomLikely causeFix
Website or public link returns 403Block Public Access on at bucket or account level, or no public-read policy, or the policy’s Resource misses the prefixAllow at every level, fix the Resource; or use CloudFront
Can read files, aws s3 ls gives AccessDeniedPolicy has only bucket/*, no s3:ListBucket on the bucket ARNAdd s3:ListBucket on arn:aws:s3:::bucket
Can list, cannot readPolicy has only the bucket ARNAdd the object ARN bucket/*
GET of a missing key gives 403, not 404No s3:ListBucket, so S3 hides whether the key existsCheck the key; grant s3:ListBucket if appropriate
Partner gets 403Missing one half: caller’s IAM policy or the bucket policyBoth sides must allow
Partner lost access after a security changeRestrictPublicBuckets turned on and the bucket policy has a public statementRemove the public statement; name the partner
Everyone, even admins, gets 403An explicit Deny matches: HTTP instead of HTTPS, wrong VPC endpoint, IP conditionFix the client, or add an exception with aws:PrincipalArn
Owner cannot read objects a partner uploadedObject Ownership is Object writer, partner owns the objectsBucket owner enforced
Uploads fail with AccessControlListNotSupportedACLs disabled, client sends an ACLRemove the ACL or send bucket-owner-full-control
403 only on some objects, all SSE-KMSCaller has no kms:Decrypt on the keyGrant the key (lesson 10.8)

#Access control in system design

  • Private by default, public by exception. Keep Block Public Access on at the account level everywhere; put the few truly public buckets in a separate account, or better, serve them through CloudFront from a private bucket.
  • Least privilege by prefix. Design key prefixes around who needs what (photos/, invoices/, labels/) so policies can grant exactly one prefix.
  • Roles for workloads. EC2, Lambda and containers get IAM roles; no long-term keys anywhere.
  • Bucket policy as the guard rail. Keep the Allows mostly in IAM, and use the bucket policy for what must hold no matter what IAM says: HTTPS only, only through the VPC endpoint, only the organization.
  • Cross-account by name. Name the partner’s account or role; never use Principal: "*" to share with one partner.
  • ACLs off. Bucket owner enforced everywhere; move any remaining ACL grants into policies.
  • Watch continuously. IAM Access Analyzer findings, plus the AWS Security Hub control that checks Block Public Access, catch the mistakes people will make anyway.
  • Temporary links instead of public objects. For “let one customer download one invoice”, use a pre-signed URL (lesson 10.9 and lesson 10.12), not a public object.

#Try it yourself

The commands use the shop’s made-up bucket shop-media-example in account 111122223333.

# 1. See the four Block Public Access settings: bucket level, then account level
aws s3api get-public-access-block --bucket shop-media-example
aws s3control get-public-access-block --account-id 111122223333

# 2. Turn all four on for the whole account (recommended)
aws s3control put-public-access-block --account-id 111122223333 \
  --public-access-block-configuration \
  BlockPublicAcls=true,IgnorePublicAcls=true,BlockPublicPolicy=true,RestrictPublicBuckets=true

# 3. Object Ownership: check it, and disable ACLs on an old bucket
aws s3api get-bucket-ownership-controls --bucket shop-media-example
aws s3api put-bucket-ownership-controls --bucket shop-media-example \
  --ownership-controls 'Rules=[{ObjectOwnership=BucketOwnerEnforced}]'

# 4. Save a bucket policy from a file, read it back, and ask S3 whether it is public
aws s3api put-bucket-policy --bucket shop-media-example --policy file://policy.json
aws s3api get-bucket-policy --bucket shop-media-example --query Policy --output text
aws s3api get-bucket-policy-status --bucket shop-media-example
# -> "PolicyStatus": { "IsPublic": false }

# 5. Test the HTTPS-only rule: the first fails with 403, the second works
curl -s -o /dev/null -w "%{http_code}\n" http://shop-media-example.s3.ap-southeast-1.amazonaws.com/photos/saree-red.jpg
curl -s -o /dev/null -w "%{http_code}\n" https://shop-media-example.s3.ap-southeast-1.amazonaws.com/photos/saree-red.jpg

# 6. Simulate an IAM principal against S3 before you change anything
aws iam simulate-principal-policy \
  --policy-source-arn arn:aws:iam::111122223333:role/shop-web-role \
  --action-names s3:GetObject \
  --resource-arns arn:aws:s3:::shop-media-example/invoices/2026/10/inv-1043.pdf

# 7. IAM Access Analyzer: create the analyzer once per Region, then list S3 findings
aws accessanalyzer create-analyzer --analyzer-name account-external --type ACCOUNT
aws accessanalyzer list-findings-v2 \
  --analyzer-arn arn:aws:access-analyzer:ap-southeast-1:111122223333:analyzer/account-external \
  --filter '{"resourceType": {"eq": ["AWS::S3::Bucket"]}}'

The policy.json for step 4 can be any of the policies in this lesson. Try saving the public-read policy while BlockPublicPolicy is on: the call fails with AccessDenied, and that is the setting doing its job.

#Summary

Key takeaways

  • Every bucket starts private. Since April 2023 new buckets also have Block Public Access on and ACLs disabled (Bucket owner enforced).
  • User-based: IAM policies on users, groups and roles; applications on EC2 get an IAM role, never keys. Resource-based: the bucket policy (JSON with a Principal, one per bucket, 20 KB), and legacy ACLs.
  • Bucket-level actions (s3:ListBucket) use arn:aws:s3:::bucket; object actions (s3:GetObject) use arn:aws:s3:::bucket/*.
  • Explicit Deny wins everywhere. Same account: IAM or bucket policy. Cross-account: both the caller’s IAM policy and the bucket policy. Anonymous: only the bucket can allow.
  • Bucket policy jobs: public read (Principal: "*", read only, exact prefix), cross-account, organization (aws:PrincipalOrgID), HTTPS only (Deny when aws:SecureTransport is false: in transit), forcing an encryption type on upload (at rest, lesson 10.8), network limits (aws:SourceVpce, aws:SourceIp).
  • Block Public Access never grants: BlockPublicAcls and BlockPublicPolicy reject new public grants; IgnorePublicAcls and RestrictPublicBuckets disarm existing ones (the latter also blocks other accounts when the policy is public). Levels: access point, bucket, account, organization (since November 2025); the most restrictive wins.
  • Object Ownership: Bucket owner enforced (default, ACLs off, owner owns everything), Bucket owner preferred, Object writer. Uploads with other ACLs fail with AccessControlListNotSupported.
  • IAM Access Analyzer for S3 lists public and cross-account buckets, blocks public access in one click, archives intended findings; needs an analyzer per Region.