Fix “The bucket does not allow ACLs” in Amazon S3
Explore with AI
The bucket does not allow ACLs Your upload or ACL call sent an access control list (often public-read) to an S3 bucket whose Object Ownership is set to Bucket owner enforced, where ACLs are turned off, so S3 rejects it with a 400 AccessControlListNotSupported. The usual fix is to stop sending the ACL and grant access with a bucket policy. If you truly need per-object ACLs, switch the bucket to Bucket owner preferred.
Ongoing incident? Our founding team can help you get your AWS app back up.On this page
- What “The bucket does not allow ACLs” means
- Confirm the bucket has ACLs disabled
- Cause 1: your upload code sets a canned ACL
- Cause 2: a new bucket created by scripts or IaC after April 2023
- Cause 3: an explicit ACL call or a cross-account grant
- When you really need ACLs: re-enable them safely
- Keep the error from coming back
- Common questions
You usually hit this error on an upload that worked yesterday, or that works against an older bucket. The AWS CLI prints it as An error occurred (AccessControlListNotSupported) when calling the PutObject operation: The bucket does not allow ACLs. The SDKs surface the same error code with a 400 status.
S3 is telling you that the request carried an access control list, and the bucket has ACLs switched off. The bucket is fine and your credentials are fine. The request and the bucket’s ownership setting disagree, and you need to change one of them.
What “The bucket does not allow ACLs” means
Every S3 bucket has a bucket-level setting called S3 Object Ownership. It has three values:
- Bucket owner enforced (the default): ACLs are disabled. The bucket owner owns every object, and access comes only from policies (IAM policies, bucket policies, VPC endpoint policies, SCPs and RCPs).
- Bucket owner preferred: ACLs are enabled. New objects written with the
bucket-owner-full-controlcanned ACL belong to the bucket owner. - Object writer: ACLs are enabled. The uploading account owns the object and can grant access through ACLs.
With Bucket owner enforced, S3 accepts only PUT requests that carry no ACL, or that carry a bucket owner full control ACL such as the bucket-owner-full-control canned ACL. Any other ACL, whether public-read or a custom grant to another account, fails with a 400 and the error code AccessControlListNotSupported. Requests to set or update ACLs on objects or the bucket fail the same way. Requests to read ACLs still work, and always show full control for the bucket owner. All of this is spelled out on the AWS Object Ownership troubleshooting page.
Why it catches so many people: AWS changed the defaults in April 2023. From then on, every new bucket gets ACLs disabled and Block Public Access turned on, however it is created: CLI, API, SDK or CloudFormation. Console-created buckets had those defaults since the features shipped in 2018 and 2021. Existing buckets did not change. So code that sets ACL: public-read keeps working on the old bucket and fails on the new one.
graph TB A[PutObject arrives with an ACL] --> B{Object Ownership setting?} B -->|Bucket owner enforced| C{ACL is bucket-owner-full-control?} C -->|yes| D[Upload succeeds] C -->|no| E[400 AccessControlListNotSupported] B -->|ACLs enabled| F{Public ACL and BlockPublicAcls on?} F -->|yes| G[Rejected by Block Public Access] F -->|no| D E --> H[Drop the ACL, use a bucket policy] H --> D style E fill:#fee2e2,stroke:#fca5a5,color:#7f1d1d style G fill:#fee2e2,stroke:#fca5a5,color:#7f1d1d style H fill:#d1fae5,stroke:#6ee7b7,color:#065f46
Confirm the bucket has ACLs disabled
Before changing anything, check the bucket the failing code actually writes to. Mixed-up bucket names and Regions are a common reason a fix “doesn’t work”.
aws s3api get-bucket-ownership-controls --bucket my-bucket
If ACLs are disabled you will see:
{
"OwnershipControls": {
"Rules": [
{ "ObjectOwnership": "BucketOwnerEnforced" }
]
}
}
Reading this setting needs s3:GetBucketOwnershipControls. Changing it needs s3:PutBucketOwnershipControls.
Then check the bucket’s Block Public Access settings, because they decide what happens if you re-enable ACLs later:
aws s3api get-public-access-block --bucket my-bucket
In the console, both live on the bucket’s Permissions tab, under Object Ownership and Block public access.
Cause 1: your upload code sets a canned ACL
This is the most common case by far. An SDK call, a CLI command or an upload library sets an ACL on every object, usually public-read so files can be served straight from S3. The Stack Overflow question behind this error is a .NET TransferUtility upload with CannedACL = S3CannedACL.PublicRead.
How to tell it’s yours: search your code and config for the ACL parameter. It looks different per client:
# AWS CLI
aws s3api put-object --bucket my-bucket --key a.png --body a.png --acl public-read
aws s3 cp a.png s3://my-bucket/ --acl public-read
# boto3
s3.upload_file("a.png", "my-bucket", "a.png", ExtraArgs={"ACL": "public-read"})
// AWS SDK for JavaScript v3
await s3.send(new PutObjectCommand({ Bucket: "my-bucket", Key: "a.png", Body, ACL: "public-read" }));
// AWS SDK for .NET
var req = new TransferUtilityUploadRequest { BucketName = bucket, Key = key, CannedACL = S3CannedACL.PublicRead };
Upload libraries and framework storage drivers often set the ACL from a config default, so also grep settings files for acl, ACL and public-read.
Fix: remove the ACL and grant access with a bucket policy
This is the route AWS recommends. It keeps the bucket on Bucket owner enforced, which AWS advises for most modern use cases.
-
Delete the ACL parameter from the upload call. A
PUTwith no ACL succeeds on a bucket with ACLs disabled:aws s3api put-object --bucket my-bucket --key a.png --body a.png -
If the objects really must be publicly readable, grant read access with a bucket policy. This is a public policy, so the bucket’s
BlockPublicPolicyandRestrictPublicBucketssettings must be off for it to apply:{ "Version": "2012-10-17", "Statement": [ { "Sid": "PublicReadForUploads", "Effect": "Allow", "Principal": "*", "Action": "s3:GetObject", "Resource": "arn:aws:s3:::my-bucket/public/*" } ] }Scope the
Resourceto a prefix wherever you can, so only the objects you mean to share are public.aws s3api put-public-access-block --bucket my-bucket \ --public-access-block-configuration BlockPublicAcls=true,IgnorePublicAcls=true,BlockPublicPolicy=false,RestrictPublicBuckets=false aws s3api put-bucket-policy --bucket my-bucket --policy file://policy.json -
Re-run the upload and fetch an object. The upload returns an ETag, and the object is readable through the policy.
Check the account level too. Block Public Access applies the most restrictive mix of the account, bucket and access point settings, and an organisation-level policy can enforce it on top. If the account blocks public policies, the bucket-level change does nothing. See Blocking public access to your Amazon S3 storage.
If the objects don’t need to be public at all, skip step 2. Leave every Block Public Access setting on and grant access to the specific IAM roles that read them.
Cause 2: a new bucket created by scripts or IaC after April 2023
The code didn’t change. The bucket did. A Terraform, CloudFormation or CLI script created a fresh bucket in a new environment or account, and it picked up the post-April 2023 defaults. The old bucket in production was created earlier, has ACLs enabled, and still works. AWS’s notice says this directly: apps that need public buckets or ACLs must now configure that on purpose, and automation scripts and CloudFormation templates may need updating.
How to tell it’s yours: run get-bucket-ownership-controls on both buckets. The working bucket shows an ACLs-enabled setting. The failing bucket shows BucketOwnerEnforced.
Fix: make the ownership setting explicit in your infrastructure code
-
Decide which setting you want. If you can, take the Cause 1 fix and remove the ACL from the code, so every environment can stay on Bucket owner enforced.
-
Either way, declare the setting in the template so new buckets stop drifting from old ones. In CloudFormation this is the bucket’s OwnershipControls property:
UploadsBucket: Type: AWS::S3::Bucket Properties: OwnershipControls: Rules: - ObjectOwnership: BucketOwnerEnforced -
With the CLI, set it when you create the bucket.
CreateBuckettakes thex-amz-object-ownershipheader:aws s3api create-bucket --bucket my-bucket --object-ownership BucketOwnerEnforced
Cause 3: an explicit ACL call or a cross-account grant
Some code makes the object public as a separate step after upload with PutObjectAcl, or sets bucket ACLs with PutBucketAcl. Other setups grant read access to another AWS account with --grant-read or a custom ACL. With ACLs disabled, every request that sets or updates an ACL fails, so all of these return the same error.
How to tell it’s yours: the failing operation in the error is PutObjectAcl or PutBucketAcl, or the PutObject request carries x-amz-grant-* headers.
Fix: move the grant into a policy, or send bucket-owner-full-control
-
Delete the separate
PutObjectAclstep. Grant the same access with a bucket policy, as in Cause 1. -
For another account that needs to read, add a bucket policy statement with that account’s role as
Principal. AWS notes that once ACLs are disabled, cross-account access after upload must come from bucket policies. -
If another account writes into your bucket and its code always sends
bucket-owner-full-control, you don’t need to change anything. That ACL is accepted on a bucket with ACLs disabled:aws s3 cp file.txt s3://my-bucket --acl bucket-owner-full-control
When you really need ACLs: re-enable them safely
Some tools only speak ACLs, or you need per-object access control. AWS says keep ACLs disabled except when you must control access for each object individually. If that’s you, switch the bucket to an ACLs-enabled setting.
-
Turn ACLs back on with Bucket owner preferred:
aws s3api put-bucket-ownership-controls --bucket my-bucket \ --ownership-controls 'Rules=[{ObjectOwnership=BucketOwnerPreferred}]'In the console: bucket, Permissions, Object Ownership, Edit, choose ACLs enabled.
-
If the ACL you send is public (
public-readgrants to theAllUsersgroup, which S3 counts as public), relax the two ACL settings in Block Public Access. WithBlockPublicAclson, aPutObjectthat includes a public ACL still fails, now because of Block Public Access. WithIgnorePublicAclson, the upload succeeds, but S3 ignores the public grant and the object stays private.aws s3api put-public-access-block --bucket my-bucket \ --public-access-block-configuration BlockPublicAcls=false,IgnorePublicAcls=false,BlockPublicPolicy=true,RestrictPublicBuckets=true -
Re-run the original upload. It should return an ETag with no error.
This is the step people miss when they report they “enabled ACLs and it still fails”. The Object Ownership change removes AccessControlListNotSupported, and Block Public Access then rejects the public ACL for a separate reason. Remember too that account-level and organisation-level Block Public Access win over the bucket’s own setting.
Two things to know before you flip it. Re-enabling ACLs restores any object ACLs that existed before they were disabled. And objects written while Bucket owner enforced was on stay owned by the bucket owner.
Keep the error from coming back
- Pin Object Ownership in every template. Declare
OwnershipControlson every bucket, so a new environment gets the same setting as production and the April 2023 default can’t surprise you. - Require ACLs-off buckets at creation. AWS publishes an IAM policy that denies
s3:CreateBucketunlesss3:x-amz-object-ownershipisBucketOwnerEnforced. You can use it as an SCP across an organisation. See Disabling ACLs for all new buckets. - Remove ACL parameters from code. A bucket policy covers public reads. Then no upload path depends on the bucket’s ACL setting.
- Test uploads against a freshly created bucket in CI. That reproduces what a new environment sees, and it catches an
ACLparameter before production does. - Audit settings across accounts. S3 Storage Lens can report which Object Ownership setting each bucket uses, so you can find the ACLs-enabled buckets your code still quietly depends on.
- Log the S3 error code on failed uploads. Record the error code and the bucket name in your upload handler’s error log. A spike in
AccessControlListNotSupportedthen points straight at the bucket, without anyone having to reproduce it.
Running on AWS? See how Polylane monitors AWS in production.
Common questions.
What is the AccessControlListNotSupported error code?
It's the code S3 returns with a 400 status when a request tries to set or update an ACL on a bucket whose Object Ownership is Bucket owner enforced. The message text is The bucket does not allow ACLs. Uploads with no ACL, or with bucket-owner-full-control, are still accepted.
Why does the same code work on one bucket and fail on another?
Since April 2023, every new S3 bucket gets ACLs disabled and Block Public Access on by default, however it's created. Buckets created before that kept their settings. Run aws s3api get-bucket-ownership-controls on both buckets and you'll see the difference.
I enabled ACLs in the console and uploads still fail. Why?
First, check you changed the bucket the code actually writes to. If you send public-read, the BlockPublicAcls setting still rejects any PutObject that includes a public ACL. Turn off BlockPublicAcls and IgnorePublicAcls on the bucket, and check that account-level Block Public Access isn't overriding it.
How do I make uploaded files public without ACLs?
Remove the ACL from the upload, then add a bucket policy that allows s3:GetObject to Principal * on a prefix such as my-bucket/public/*. The bucket's BlockPublicPolicy and RestrictPublicBuckets settings must be off for a public policy to be accepted and to take effect.
Can I still use bucket-owner-full-control with ACLs disabled?
Yes. S3 accepts PUT requests with the bucket-owner-full-control canned ACL, or the equivalent XML grant, on a bucket set to Bucket owner enforced. Existing cross-account writers that send it see no change.
Is it safe to switch an existing bucket back to ACLs enabled?
You can switch at any time with put-bucket-ownership-controls. Any object ACLs that existed before ACLs were disabled come back into force, so review them first. Objects written while ACLs were disabled stay owned by the bucket owner.
Which permission do I need to change Object Ownership?
Setting, updating or deleting it needs s3:PutBucketOwnershipControls. Reading it needs s3:GetBucketOwnershipControls. Changing Block Public Access on a bucket needs s3:PutBucketPublicAccessBlock.
Sources (5)
- Controlling ownership of objects and disabling ACLs for your bucket
- Object Ownership troubleshooting: AccessControlListNotSupported
- Disabling ACLs for all new buckets and enforcing Object Ownership
- Blocking public access to your Amazon S3 storage
- Amazon S3 will disable ACLs and enable Block Public Access for new buckets from April 2023
Boris Tane is the founder of Polylane. He previously founded Baselime, observability for the future of the cloud, which Cloudflare acquired. At Cloudflare he built and led the Workers observability team.
Related
- How to correlate incidents with deploys using evidence Learn how to correlate incidents with deploys using release markers, metrics, logs and verified GitHub, Vercel, Cloudflare and Kubernetes change events.
- Fix AuthorizerConfigurationException in API Gateway AuthorizerConfigurationException means API Gateway couldn't call your Lambda authorizer or couldn't read its response. Find the cause and fix it.
- Fix 403 Forbidden (Access Denied) from Amazon S3 Why Amazon S3 returns 403 Forbidden: Block Public Access, missing s3:ListBucket, object ownership, clock skew, expired presigned URLs and CloudFront. Fix each.
- Fix 502 Bad Gateway on AWS API Gateway and Load Balancers Why AWS returns 502 Bad Gateway from API Gateway or an Application Load Balancer, how to find which hop wrote it, and the fix for each cause.
- AsyncEventsDropped: diagnose Lambda async event loss AsyncEventsDropped counts Lambda async events discarded after age or retry limits. Check throttling, destinations and DLQs, then verify processing recovery.