The Challenge
S3 buckets default to fully private. Making images publicly readable requires threading through two layers of access control: the bucket's Public Access Block settings and the bucket policy itself.
PublicAccessBlock Configuration
Even with a public bucket policy, AWS blocks public access by default if the bucket-level block is enabled. The BlockPublicPolicy and RestrictPublicBuckets flags must be False, while ACL-related blocks can stay True (we're using a policy, not ACLs).
def _ensure_public_cms_images():
s3 = _boto_s3()
s3.put_public_access_block(
Bucket=S3_BUCKET,
PublicAccessBlockConfiguration={
"BlockPublicAcls": True,
"IgnorePublicAcls": True,
"BlockPublicPolicy": False, # must be False
"RestrictPublicBuckets": False, # must be False
},
)
policy = json.dumps({
"Version": "2012-10-17",
"Statement": [{
"Sid": "PublicReadCmsImages",
"Effect": "Allow",
"Principal": "*",
"Action": "s3:GetObject",
"Resource": f"arn:aws:s3:::{S3_BUCKET}/cms-images/*",
}],
})
s3.put_bucket_policy(Bucket=S3_BUCKET, Policy=policy)
Why Prefix-Scoped
The bucket also stores private uploads from the media manager app. The policy resource ARN ends in /cms-images/* — only that prefix is public. User uploads under images/, documents/ etc. remain private.
_ensure_public_cms_images() before uploading images, not after. The upload succeeds either way, but the image won't be publicly accessible until the policy is in place.