A fast, reliable way to serve images from S3, plus an IAM policy & user for managing uploads.
TL;DR
• Create an S3 bucket (optionally enable Static Website Hosting).
• Allow public read via a bucket policy.
• Create a least‑privilege IAM policy + user for uploads/deletes.
• Use the virtual‑hosted S3 URL to reference files in your site or app.
Prerequisites
- AWS account with permissions to manage S3 and IAM
- (Optional) AWS CLI installed and configured locally
1) Create the S3 bucket
- AWS Console → S3 → Create bucket
- Bucket name: e.g.,
acme-bucket-prod - Region:
us-east-1(N. Virginia) - Object Ownership: Bucket owner enforced (recommended) (disables ACLs)
- Block Public Access (bucket settings): Uncheck “Block all public access”, acknowledge the warning, then Create bucket.
Note: If your Account-level “Block Public Access” is On, it will override bucket settings—you’ll need to relax it at the account level for this bucket to be publicly readable.
Optional: Static Website Hosting
- Open the bucket → Properties → Static website hosting → Enable.
- Set Index document (e.g.,
index.html) and Error document (e.g.,404.html).
2) Add a public‑read bucket policy
Open Bucket → Permissions → Bucket policy → Edit, and paste (replace BUCKET_NAME):
|
1 2 3 4 5 6 7 8 9 10 11 12 13 |
{ "Version": "2012-10-17", "Statement": [ { "Sid": "AllowPublicRead", "Effect": "Allow", "Principal": "*", "Action": "s3:GetObject", "Resource": "arn:aws:s3:::BUCKET_NAME/*" } ] } |
- This grants public read of objects only. Writes remain private.
- If account-level Public Access Block is enabled, the console will warn and this policy won’t take effect.
3) Create a least‑privilege IAM policy
Create a policy that limits a user to listing the bucket and putting/getting/deleting objects only in this bucket. Recommended for Object Ownership = bucket owner enforced (no ACLs):
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
{ "Version": "2012-10-17", "Statement": [ { "Effect": "Allow", "Action": ["s3:ListBucket", "s3:GetBucketLocation"], "Resource": "arn:aws:s3:::BUCKET_NAME" }, { "Effect": "Allow", "Action": [ "s3:PutObject", "s3:GetObject", "s3:DeleteObject", "s3:AbortMultipartUpload", "s3:ListMultipartUploadParts", "s3:ListBucketMultipartUploads" ], "Resource": "arn:aws:s3:::BUCKET_NAME/*" } ] } |
If you must manage ACLs (generally not needed with Bucket owner enforced), add:
"s3:PutObjectAcl", "s3:GetObjectAcl"to the object‑level actions.
Create this in IAM → Policies → Create policy (JSON). Name it something like AcmeBucketManager.
4) Create an IAM user and access key
- IAM → Users → Create user (e.g.,
acme-image-uploader) - Attach permissions: select your
AcmeBucketManagerpolicy - Open the new user → Security credentials → Create access key → Application running outside AWS → Create
- Download the
.csvor copy the Access key ID and Secret access key (you’ll only see the secret once)
Security tip: Prefer short‑lived credentials (roles + STS) in production. Use a long‑term access key only when necessary and rotate it regularly.
5) Upload & test (AWS CLI)
|
1 2 3 4 5 6 7 |
# Configure the CLI with the user’s access key aws configure # Upload an image aws s3 cp ./logo.png s3://acme-bucket-prod/images/logo.png --region us-east-1 # Verify it’s publicly readable curl -I "https://acme-bucket-prod.s3.us-east-1.amazonaws.com/images/logo.png" |
6) Public URL formats
Virtual‑hosted (recommended):
|
1 2 |
https://acme-bucket-prod.s3.us-east-1.amazonaws.com/OBJECT_KEY |
Example:
|
1 2 |
https://acme-bucket-prod.s3.us-east-1.amazonaws.com/images/logo.png |
Path‑style:
|
1 2 |
https://s3.us-east-1.amazonaws.com/acme-bucket-prod/OBJECT_KEY |
Static website endpoint (if enabled; HTTP only on S3 endpoint):
|
1 2 |
http://acme-bucket-prod.s3-website-us-east-1.amazonaws.com/OBJECT_KEY |
Tips
- URL‑encode spaces/special characters in
OBJECT_KEY(e.g., space →%20). - Hyphens in bucket names are fine with HTTPS virtual‑hosted URLs.
- If fronted by CloudFront, share the CloudFront URL instead.
7) Optional: CORS for browser apps
If a web app on another origin loads these images and you see CORS errors, add a CORS rule (Bucket → Permissions → CORS configuration):
|
1 2 3 4 5 6 7 8 9 |
<CORSConfiguration> <CORSRule> <AllowedOrigin>*</AllowedOrigin> <AllowedMethod>GET</AllowedMethod> <MaxAgeSeconds>300</MaxAgeSeconds> <AllowedHeader>*</AllowedHeader> </CORSRule> </CORSConfiguration> |
Tighten AllowedOrigin to your site domains when known.
8) Production hardening (recommended)
- Put CloudFront in front of S3 with Origin Access Control (OAC) so the bucket itself isn’t public.
- Keep the bucket policy read‑only for anonymous users; never allow public writes.
- Enable S3 server access logging and/or CloudFront logging; monitor for abuse.
- Add lifecycle rules for object expiration if you publish ephemeral assets.
- Consider AWS WAF on CloudFront to rate‑limit or block malicious traffic.




