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.

Why uploads are refusedSettings sayACL public-readUpload sendsx-amz-acl headerBucket enforcesowner, ACLs offS3 rejectsACL not supportedNew buckets ignore object ACLs; access is controlled only by policies.
  1. Stop sending ACLs: set default_acl to None (or remove AWS_DEFAULT_ACL).
  2. Remove ACL from object_parameters / AWS_S3_OBJECT_PARAMETERS too.
  3. For public static files, add a bucket policy that allows s3:GetObject on the static prefix, or serve through CloudFront with origin access control.
  4. For private media, keep querystring_auth = True so Django generates presigned URLs.
  5. 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.

how access is grantedPer-object ACLs (old)✕set on every upload✕rejected on new buckets✕hard to auditBucket policy or CloudFront✓one rule per prefix✓works with owner enforced✓visible in one placeAccess is now decided by policy, not by each filedjango-deployment.com
Remove ACL settings from Django and describe access once in the bucket configuration.

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.

who needs to read the files?What kind of file is it?static CSS and JSCloudFront + OACor prefix policyprivate user uploadsPresigned URLsquerystring_authpublic user contentPublic prefixpolicy on media/publicNone of these options needs object ACLsdjango-deployment.com
Separate buckets or prefixes for static, public, and private files keep policies simple.

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 set AWS_DEFAULT_ACL = None explicitly.
  • collectstatic speed. Static uploads go faster with ManifestStaticFilesStorage variants that skip unchanged files; see Speed Up collectstatic With S3.

Next steps

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.

Keep going