13.6 S3 Object Ownership: Enforcing Bucket Owner Full Control
Right, let’s talk about S3 Object Ownership. This is one of those features that started as a quiet little checkbox and has become arguably one of the most important security controls in all of AWS S3. Ignore this at your peril, because getting it wrong is the fastest way to either a security incident or a massive headache when you can’t access the data you just paid to store.
Here’s the core problem it solves: by default, when one AWS account uploads an object to a bucket owned by another account, the uploading account retains ownership of that object. Let that sink in. You own the bucket, but some other account owns the contents inside it. This is as absurd as it sounds. It means you, the bucket owner, might not even have permission to read or delete the object you’re storing. You’re basically running a storage locker for someone else who has the only key. The original design was probably meant for complex cross-account workflows, but for 99% of use cases, it’s a nightmare.
The Two Settings and Why You Care
There are two main settings for this, and your life will be infinitely easier if you almost always choose one of them.
BucketOwnerPreferred: This is the polite suggestion. If the uploading account doesn’t explicitly set an ACL (Access Control List) on the object, then you, the bucket owner, automatically become the owner. It’s a good default for buckets where you mostly control the uploads but want to be friendly to occasional external contributors. But it’s just a suggestion. If that other account does set an ACL, they can still lock you out. This is why I call it the “hopeful” setting.
BucketOwnerEnforced: This is the bouncer. It doesn’t suggest; it dictates. It completely ignores any ACL set by the object uploader—it just smashes them to pieces—and ensures you, the bucket owner, are the sole owner of every single object in the bucket. ACLs are disabled. This is the setting you want for 99.9% of all buckets you create. It simplifies permissions to a glorious degree. You control access via your bucket policy, full stop. No more wondering who owns what.
Setting It Up: Bucket Creation vs. Existing Buckets
You should set this at bucket creation. It’s the easiest and cleanest way. Here’s how you’d do it with AWS CLI v2:
# Create a new bucket with the strongest ownership setting from the start
aws s3api create-bucket \
--bucket my-super-secure-bucket-123 \
--object-ownership BucketOwnerEnforced
But we don’t live in a perfect world. You probably have existing buckets. Changing this setting on a bucket full of objects owned by other accounts is a delicate operation. The BucketOwnerEnforced setting can only be applied if the bucket is already free of object ACLs. You often have to first change the ownership of all existing objects to yourself before you can flip the switch.
Here’s how to fix an existing bucket. First, check the current ownership of objects. You’ll often find a mix.
# List objects and their owners to see the mess you're dealing with
aws s3api list-objects --bucket my-old-bucket --query 'Contents[].{Key:Key, Owner:Owner.DisplayName}'
Then, you need to run a mass ownership change. This is a one-way trip and requires s3:PutObjectAcl permission on the bucket for each object.
# Use the CLI to change the ownership on all objects in one go
aws s3 cp s3://my-old-bucket/ s3://my-old-bucket/ --recursive --metadata-directive REPLACE --acl bucket-owner-full-control
Yes, you’re copying the objects onto themselves. It looks weird, but it’s the standard way to update object metadata (like ownership) without duplicating storage costs. Once this command finishes and you’ve verified all objects have the bucket-owner-full-control ACL, then you can safely enforce the new rule.
# Now you can slam the door shut
aws s3api put-bucket-ownership-controls \
--bucket my-old-bucket \
--ownership-controls Rules=[{ObjectOwnership=BucketOwnerEnforced}]
The Gotchas and The Glory
Pitfall: The biggest mistake is assuming this is set by default. It’s not. Until mid-2021, the default was the old, chaotic way. AWS now (thankfully) sets BucketOwnerEnforced as the default for new buckets in the AWS Console, but the programmatic APIs (like CLI, CDK, Terraform) often still default to the old behavior for backwards compatibility. Always, always, always explicitly declare it in your Infrastructure-as-Code templates.
Best Practice: For any bucket that isn’t specifically designed for cross-account uploads where the uploader must retain access, use BucketOwnerEnforced. Full stop. It centralizes control, eliminates a whole class of permission nightmares, and aligns with the security principle of least privilege—you’re not granting random accounts ongoing access to objects in your bucket by default. It makes your S3 security model infinitely simpler and more robust. It’s not just a setting; it’s the foundation of a sane S3 strategy.