The S3 access decision

Lesson 2 of 4 in Protecting Data at Rest.

“Why can this object be read?” is the most consequential question in AWS security, because the answer is assembled from four or five independent mechanisms that were added at different times and are evaluated together. Encryption is irrelevant to it — server-side encryption is transparent to an authorized caller, which is why “the bucket was encrypted” has never prevented a single public-data incident.

S3 Block Public Access is the outermost control, and the only one that is a safety interlock rather than a policy. It has four independent settings — block new public ACLs, ignore all public ACLs, block new public bucket policies, and restrict public and cross-account access through any public policy — and it can be applied at the account level (where it covers every bucket, present and future) as well as per bucket. When it is on, it overrides permissive ACLs and bucket policies: a bucket policy granting Principal: "*" simply has no effect. As of April 2023 all four settings are enabled by default on new buckets, so the modern failure mode is someone deliberately turning them off.

ACLs are the oldest layer and should no longer exist in your account. The modern setting is Object Ownership = bucket owner enforced, which disables ACLs entirely: the bucket owner owns every object regardless of who uploaded it, and access is governed solely by policies. This is the default for new buckets. If a question mentions cross-account uploads whose objects the bucket owner cannot read, it is describing the old world — and bucket-owner-enforced is the fix.

The bucket policy and identity policies are where real authorization lives, with the normal AWS evaluation logic: an explicit deny anywhere wins; otherwise an allow is needed — from the identity policy for same-account principals, and from both the identity policy and the bucket policy for cross-account access. Add the optional layers and you have the full picture: a VPC endpoint policy can narrow what is reachable through a private path, an access point gives a named front door with its own policy and network origin, and permission boundaries, session policies, SCPs and RCPs can each subtract permissions without ever appearing in the bucket policy.

One GetObject, every gate it passes

  1. Request arrives — identity, path, encryption headers

    Three facts travel with it and all three matter later: who is calling (an IAM principal, another account, or nobody at all), how it arrived (public endpoint, VPC endpoint, access point), and whether it used TLS.

  2. Organization policies: SCP and RCP

    An SCP can remove the action from the principal’s available permissions; a resource control policy can remove it from the resource’s side for the whole organization — including for principals outside it. Neither grants anything; both can veto, and neither appears in the bucket policy.

  3. Block Public Access

    If the request is anonymous or relies on a public ACL or a public bucket policy, and BPA is on at the account or bucket level, it stops here. BPA overrides permissive configuration — which is what makes it the safety interlock rather than another policy to reason about.

  4. Endpoint or access point policy

    If the request came through a VPC endpoint, the endpoint policy must allow it. If it came through an access point, that access point’s policy applies and its network origin (internet or VPC) is enforced. Both can only narrow what the underlying bucket policy allows.

  5. Explicit deny anywhere?

    Bucket policy, identity policy, permission boundary, session policy — one deny ends the evaluation. This is where the aws:SecureTransport and aws:SourceVpce denies take effect, and it is the first thing to look for when troubleshooting an unexpected refusal.

  6. Is there an allow from the right places?

    Same-account: an allow in the identity policy or the bucket policy suffices. Cross-account: you need an allow in both the bucket policy and the caller’s identity policy. ACLs could historically supply an allow — unless Object Ownership is bucket-owner-enforced, in which case they are not evaluated at all.

  7. Can the caller use the encryption key?

    For an SSE-KMS object, S3 asks KMS to decrypt the object’s data key as the caller. If the key policy does not authorize that principal, the result is AccessDenied from KMS even though every S3 gate passed — the single most confusing error in this stack.

  8. Object bytes returned

    Decryption is transparent to an authorized caller. Encryption never stopped a reader who got this far: that is the whole argument for treating access control and encryption as separate requirements with separate controls.

Two S3 features extend that decision in ways the exam expects you to recognise.

Access points are named endpoints attached to a bucket, each with its own hostname, its own access-point policy, and an optional network origin restricting it to a VPC. Instead of one 400-line bucket policy serving fifteen teams, each team gets an access point whose policy expresses only its own needs, and the bucket policy delegates to access points. Multi-Region Access Points add a single global hostname that routes to the nearest or healthiest bucket in a set, with failover — useful for global applications, and worth knowing because the exam lists them. Access points cannot grant more than the underlying bucket policy permits, so they are a delegation and simplification mechanism, not a bypass.

The troubleshooting order matters as much as the mechanisms. Faced with “this should work and does not”, check in this sequence: explicit denies first — in the bucket policy, the identity policy, a permission boundary, a session policy, an SCP or an RCP, because one of them ends the evaluation regardless of everything else; then whether the request even reached the right place, meaning the endpoint or access point policy and the path condition keys; then Block Public Access if the access was meant to be anonymous; then whether a cross-account request has allows on both sides; and finally the KMS key policy, which is the gate that produces an AccessDenied with nothing wrong in S3 at all. Faced with the opposite — “this should not be readable and is” — invert it: look for a public bucket policy or ACL first, then BPA being off, then an over-broad allow with Principal: "*" and a condition that does not actually constrain anything. IAM Access Analyzer answers this second question continuously by reporting which buckets are reachable from outside the account or organization, which is a far better control than periodic manual review.

“Why can’t this principal read the object?” — triage in order

Interactive decision tree — outcomes:

  • Block Public Access is doing its job

    Anonymous access is blocked by BPA at the account or bucket level, whatever the bucket policy or ACLs say. The right fix is almost never to switch BPA off: serve the content through CloudFront with origin access control, or issue presigned URLs, and leave the interlock in place.

  • An explicit deny is ending the evaluation

    Find it before changing anything else — one deny beats every allow. The usual suspects are the aws:SecureTransport: false deny (a client not using TLS), an aws:SourceVpce requirement (a caller outside the VPC), an encryption-header condition on PutObject, or an organization-level SCP or RCP that the bucket policy never mentions.

  • The request did not arrive the way the policy requires

    An endpoint policy, an access-point policy, or a condition on aws:SourceVpce / aws:SourceVpc is refusing the path rather than the principal. Identical credentials succeed from inside the VPC and fail from a laptop — which is the control working, not a bug.

  • Cross-account access needs both sides

    The resource policy in the bucket’s account and the identity policy in the caller’s account must both allow the action. Add the missing side — and if the objects are SSE-KMS encrypted, the key policy needs the external principal too, which is the step teams forget.

  • S3 allowed it; KMS refused

    S3 asks KMS to decrypt the object’s data key as the caller. Without kms:Decrypt authorized for that principal in the key policy, the result is AccessDenied even though every S3 gate passed. For cross-account reads, the key policy must name the external principal; kms:ViaService with the S3 endpoint is a sensible additional constraint.

  • Check object ownership and the object itself

    If every policy allows it, look at the object: uploaded cross-account before bucket-owner-enforced was set, so still owned by the uploader; or a version with a delete marker; or SSE-C, where no key was supplied on the request. Setting Object Ownership to bucket owner enforced prevents the first class permanently.

Key terms: S3 Block Public Access (BPA), S3 Object Ownership (bucket owner enforced), S3 access point

Interactive checkpoint quiz (2 questions) — open this page in a browser to take it.