skip to content

A partner account uploads objects into your S3 bucket and your own account then gets AccessDenied reading some of them. Why does the bucket owner not automatically own uploaded objects, and what do the S3 Object Ownership settings change?

level: seniorimportance: should knowfreq 42%

answer

  1. the writer owns what it writes
  2. paying for data you cannot read
  3. a canned ACL as the old workaround
  4. three settings, one destination
  5. ACLs off is the modern default

basics

~20 s

With legacy ACLs enabled, an object is owned by the account that uploaded it, not by the bucket owner, so a cross-account writer's objects can be unreadable by the bucket's own account. Setting Object Ownership to BucketOwnerEnforced disables ACLs and makes the bucket owner own every object.

solid answer

~40 s

S3's original model made the *writer* the object owner, and the object ACL — not the bucket policy — decided who else could read it. So a partner uploading into your bucket produced objects that you paid for and could not read, unless they passed the `bucket-owner-full-control` canned ACL on every PUT. The fix is the Object Ownership setting. `ObjectWriter` is the legacy behaviour. `BucketOwnerPreferred` transfers ownership to the bucket owner, but only for uploads that send `bucket-owner-full-control`. `BucketOwnerEnforced` disables ACLs entirely: the bucket owner owns everything, ACLs on the bucket and objects are ignored, and access is decided purely by IAM and the bucket policy. That last one is the default for buckets created since April 2023 and the setting you should want everywhere.

code

bash · 5 lines
bash
aws s3api get-bucket-ownership-controls --bucket shared-landing

aws s3api put-bucket-ownership-controls \
  --bucket shared-landing \
  --ownership-controls 'Rules=[{ObjectOwnership=BucketOwnerEnforced}]'

go deeper

for a junior

Know that S3 has an older ACL system alongside bucket policies, and that an object uploaded by another account is owned by that account unless the bucket says otherwise.

for a middle

Explain the three Object Ownership settings and what each does to ownership and to ACL evaluation, including why an upload with bucket-owner-full-control behaves differently under BucketOwnerPreferred.

for a senior

Diagnose the AccessDenied-in-my-own-bucket case quickly, and run the migration safely: audit ACL use through Inventory and CloudTrail, replace grants with policy statements, then enforce and watch a full batch cycle.

for a principal

Set the standard that ACLs are disabled estate-wide, own the exception process for the buckets that cannot be moved yet, and make the cross-account landing-zone contract explicit so partner writes never create data you cannot govern.

## Where the surprise comes from S3 predates IAM. Its original authorization mechanism was the ACL — a small list of grants attached to each bucket and each object — and in that model the **object owner is the account that created the object**, not the account that owns the bucket. That single rule produces the classic cross-account failure: a partner or another team's account writes into your bucket, and your own administrators get `AccessDenied` on `s3:GetObject` for those objects. Your bucket policy is irrelevant, because a bucket policy can only grant permissions the bucket owner holds, and for a foreign-owned object the bucket owner holds none. It is worse than an inconvenience. You are billed for the storage, lifecycle rules still apply, but you cannot read the data and, depending on the ACL, cannot change its ACL either. ## The old workaround The pre-2021 answer was to require the writer to send a canned ACL granting you full control on every upload: ```bash aws s3api put-object \ --bucket shared-landing \ --key partner-b/events.json \ --body events.json \ --acl bucket-owner-full-control ``` and to enforce it from your bucket policy with a `Deny` on any `s3:PutObject` whose `s3:x-amz-acl` is not `bucket-owner-full-control`. It works, but it depends on every writer, in every SDK and every script, remembering a header — and it grants you access without making you the owner, which is a subtly different and weaker thing. ## Object Ownership: three settings S3 replaced the workaround with a bucket-level setting: - **ObjectWriter** — the legacy behaviour. The uploader owns the object; ACLs are live and decide object access. - **BucketOwnerPreferred** — an upload that carries the `bucket-owner-full-control` canned ACL becomes owned by the bucket owner; uploads without it stay owned by the writer. A transitional setting: useful while you confirm every writer is sending the header, not a destination. - **BucketOwnerEnforced** — ACLs are **disabled**. The bucket owner owns every object regardless of who wrote it, all existing bucket and object ACLs stop having any effect, and access is determined solely by identity policies, the bucket policy, and access point policies. With `BucketOwnerEnforced`, requests that try to *set* an ACL fail with `AccessControlListNotSupported`, with the exception of requests specifying the `bucket-owner-full-control` canned ACL, which are tolerated so that existing pipelines do not break. That exception is the practical migration lever: turn the setting on and old scripts sending the canned ACL keep working, while scripts sending `public-read` start failing loudly — which is exactly the failure you want. ## Why disabling ACLs is the right default ACLs are a second, parallel authorization system with different syntax, no conditions, per-object granularity and no central audit. They are the mechanism behind most accidental public exposure (`public-read` on an object slips past a perfectly good bucket policy), and they make "who can read this bucket?" unanswerable without enumerating every object. Disabling them collapses the question back to two policy documents you can read and simulate. Since April 2023, new buckets are created with ACLs disabled and Block Public Access fully on, so this is the modern default rather than a hardening step. The buckets that still need attention are old ones. ## Migrating an existing bucket 1. Find out whether anything relies on ACLs. S3 Storage Lens and the `ObjectOwnership`/ACL fields in an S3 Inventory report will tell you which objects carry non-default ACLs; CloudTrail data events show who is setting them. 2. Replace those grants with bucket policy statements — a cross-account read grant becomes a `Principal` statement, a public-read object becomes a CloudFront distribution in front of a private bucket. 3. Switch Object Ownership to `BucketOwnerEnforced`. This is reversible, so you can back out if a forgotten consumer breaks. 4. Watch for `AccessDenied` and `AccessControlListNotSupported` in application logs and CloudTrail for a full business cycle — weekly and monthly batch jobs are how this bites you a month later. Note that ownership already recorded on existing objects is not rewritten by anything you configure; enforcing bucket-owner ownership applies to the bucket's access decisions going forward. If you must normalise historical objects that a foreign account owns, the reliable route is to have that account copy them in place — a copy creates a new object, owned under the bucket's current setting. ## The interview point The question is really testing whether you know that S3 has two authorization systems, that the older one is off by default now, and that "my own bucket, my own account, AccessDenied" has a specific, non-obvious explanation.

  • Before Object Ownership existed, how did bucket owners force writers to hand over control?
    A bucket policy statement denying `s3:PutObject` unless the request carried `"s3:x-amz-acl": "bucket-owner-full-control"`, so uploads without the canned ACL were rejected outright. It worked, but it granted the bucket owner permissions rather than ownership, and depended on every client sending a header correctly.
  • What happens to a PUT that specifies public-read after you set BucketOwnerEnforced?
    It fails with `AccessControlListNotSupported`. With ACLs disabled, S3 rejects requests that set an ACL, with a tolerated exception for `bucket-owner-full-control` so legacy pipelines keep working. That is a feature during migration: benign legacy headers pass, while anything trying to make an object public breaks visibly instead of silently succeeding.
  • How would you find out, before flipping the setting, whether anything still depends on ACLs?
    Run an S3 Inventory report including the ACL and ownership fields to find objects with non-default grants, cross-check S3 Storage Lens for buckets where ACLs are in use, and look at CloudTrail data events for `PutObjectAcl` and `PutBucketAcl` calls. Then replace each real dependency with a bucket policy statement before switching.

saying these in an interview costs you the question

  • Assumes the bucket owner automatically owns every object in it
  • Thinks a bucket policy can grant access to a foreign-owned object
  • Treats ACLs and bucket policies as the same mechanism
  • Flips BucketOwnerEnforced without auditing existing ACL use
  • Believes the setting rewrites ownership of existing objects

context