Fix django-storages "AccessControlListNotSupported: The Bucket Does Not Allow ACLs"
Fix the S3 error AccessControlListNotSupported, the bucket does not allow ACLs, in django-storages: stop sending ACL headers with default_acl None, use bucket policies for public static files, and keep private media behind presigned URLs.
Problem statement
A new S3 bucket, or collectstatic against one, fails with:
botocore.exceptions.ClientError: An error occurred (AccessControlListNotSupported)
when calling the PutObject operation: The bucket does not allow ACLs
Since April 2023, new S3 buckets have Object Ownership set to Bucket owner enforced, which disables ACLs entirely. Any upload that includes an x-amz-acl header, such as public-read, is rejected. Older django-storages settings, tutorials, and copied configurations set AWS_DEFAULT_ACL = "public-read" so files could be served publicly, which now fails on every new bucket.
- Stop sending ACLs: set
default_acltoNone(or removeAWS_DEFAULT_ACL). - Remove
ACLfromobject_parameters/AWS_S3_OBJECT_PARAMETERStoo. - For public static files, add a bucket policy that allows
s3:GetObjecton the static prefix, or serve through CloudFront with origin access control. - For private media, keep
querystring_auth = Trueso Django generates presigned URLs. - Do not re-enable ACLs on the bucket just to make old settings work.
Step-by-step solution
1. Remove ACL settings
With the STORAGES setting (Django 4.2+, django-storages 1.14+):
STORAGES = {
"default": {
"BACKEND": "storages.backends.s3.S3Storage",
"OPTIONS": {
"bucket_name": "myapp-media",
"region_name": "eu-central-1",
"default_acl": None,
"querystring_auth": True,
"file_overwrite": False,
},
},
"staticfiles": {
"BACKEND": "storages.backends.s3.S3Storage",
"OPTIONS": {
"bucket_name": "myapp-static",
"region_name": "eu-central-1",
"default_acl": None,
"querystring_auth": False,
"location": "static",
},
},
}
Search the project for leftovers: grep -rn "ACL" settings/ often finds AWS_DEFAULT_ACL or "ACL": "public-read" inside AWS_S3_OBJECT_PARAMETERS.
2. Make static files public with a bucket policy
With ACLs gone, public access is granted by policy. For a static bucket served directly:
{
"Version": "2012-10-17",
"Statement": [{
"Sid": "PublicReadStatic",
"Effect": "Allow",
"Principal": "*",
"Action": "s3:GetObject",
"Resource": "arn:aws:s3:::myapp-static/static/*"
}]
}
The bucket's Block Public Access settings must allow public policies for this to take effect. The safer pattern is to keep the bucket private and put CloudFront in front with origin access control, so only CloudFront can read.
3. Keep private media private
User uploads usually should not be public. With querystring_auth = True, file.url returns a presigned URL valid for querystring_expire seconds (3,600 by default). No policy or ACL is needed; Django's IAM credentials sign each link. Check that your templates use {{ obj.file.url }} rather than building URLs by hand.
4. Check the IAM policy
The app's IAM user or role needs object permissions, but no longer s3:PutObjectAcl:
{
"Effect": "Allow",
"Action": ["s3:PutObject", "s3:GetObject", "s3:DeleteObject", "s3:ListBucket"],
"Resource": ["arn:aws:s3:::myapp-media", "arn:aws:s3:::myapp-media/*"]
}
5. Migrate existing public files
If older files relied on public-read ACLs and you switch an old bucket to Bucket owner enforced, those ACLs stop applying. Add the bucket policy or CloudFront setup before changing ownership, so public files stay reachable.
Explanation
S3 has two permission systems: IAM and bucket policies, and the older per-object access control lists. AWS now recommends disabling ACLs, and new buckets do so by default through the Bucket owner enforced ownership setting. In that mode, S3 rejects any request that tries to set a non-default ACL, which is exactly what django-storages does when default_acl is public-read. Setting default_acl to None makes django-storages omit the header, and the bucket's policies decide access.
Edge cases / notes
- Other providers. Cloudflare R2 ignores ACLs entirely; DigitalOcean Spaces still supports them. Settings that work on Spaces fail on new AWS buckets.
- Old django-storages. Very old versions defaulted to
public-read. Upgrade, or setAWS_DEFAULT_ACL = Noneexplicitly. - collectstatic speed. Static uploads go faster with
ManifestStaticFilesStoragevariants that skip unchanged files; see Speed Up collectstatic With S3.
Next steps
- If uploads fail with AccessDenied instead, see Fix Django S3 Upload Access Denied.
- Serve static files through a CDN with Serve Django Static Files With CloudFront.
- Review the full setup in Store Django Media Files on S3.
FAQ
Should I re-enable ACLs on the bucket?
AWS recommends against it. Remove ACL settings from Django and use bucket policies or CloudFront instead.
How do I make some files public without ACLs?
Put them under a prefix such as public/ and allow s3:GetObject on that prefix in the bucket policy, or serve them through CloudFront.
Why did this start with a new bucket?
New buckets created since April 2023 disable ACLs by default. Older buckets keep their previous setting.
Does default_acl None make files private?
It makes files follow the bucket's policies. On a bucket without a public policy, objects are private.