AWS S3 for Media Files (with CloudFront)
This guide explains how to configure AWS S3 and CloudFront to serve media files in a production environment. This setup uses CloudFront to provide DDoS protection and faster delivery. It also utilizes CloudFront Signed URLs to ensure that only authorized users can access your files.
Even if someone discovers a direct file path, they cannot access the content without a valid signature. Signed URLs rely on temporary keys, which makes it nearly impossible for unauthorized parties to view your private media.
NOTE
Serving static files from S3 often results in 403 errors on the Django admin page. Because of this, this guide focuses on storing and serving only media files through S3 while keeping static files on the local server.
Install Packages
Install the following packages (if not already done):
uv add boto3 django-storages pillow cryptographyAWS S3 Setup
Create a new S3 bucket with following options:
- AWS Region:
ap-south-2(or something closer to your web server) - Bucket Namespace: Global Namespace
- Bucket Name: your-bucket-name
- Object Ownership: ACLs disabled
- Block all public access
- Your bucket should remain private because CloudFront will be the only service allowed to retrieve files from it.
- Bucket Versioning: Disable
- Encryption type: Server-side encryption with Amazon S3 managed keys (SSE-S3)
- Bucket Key: Enable
- Object Lock: Disable
CloudFront Setup
Create a CloudFront distribution to sit in front of your S3 bucket. This serves as your Content Delivery Network (CDN).
Navigate to the CloudFront Distributions Console and create a new distribution using the following options:
- Plan: Free (recommended) or "Pay as you go"
- Distribution Name: Your Distribution Name
- Distribution Type: Single website or app
- Route 53 managed domain: leave blank
- Origin Type: Amazon S3
- S3 Origin: your-bucket-name.s3.ap-south-2.amazonaws.com
- Origin Path: leave blank
- Allow private S3 bucket access to CloudFront: Enable
- This should automatically add a Bucket Policy to allow CloudFront distribution.
- Origin settings: Use recommended origin settings
- CloudFront automatically creates an Origin Access Control (OAC) with signed requests.
- Cache settings: Customize cache settings
- Viewer protocol policy:
Redirect HTTP to HTTPS - Allowed HTTP methods:
GET, HEAD, OPTIONS(No cache for OPTIONS) - Cache policy: CachingOptimized
- Origin request policy: leave blank
- Response headers policy:
CORS-with-preflight-and-SecurityHeadersPolicy - Web Application Firewall (WAF):
included in Free plan
- Viewer protocol policy:
Restrict Viewer Access
In order to restrict viewer access through CloudFront Signed URLs, you'll need to configure Public Keys and Key Groups in CloudFront.
- First create a Public Key Pair locally using:
- This should create 2 files in the current directory:
private_key.pemandpublic_key.pem.
- This should create 2 files in the current directory:
openssl genrsa -out private_key.pem 2048
openssl rsa -pubout -in private_key.pem -out public_key.pem- Navigate to the CloudFront Public Keys Console and create a public key using the
public_key.pemgenerated above.- Make a note of the Key ID (This becomes the AWS_CLOUDFRONT_KEY_ID below).
- The Private Key from previous step becomes the AWS_CLOUDFRONT_KEY below.
- Navigate to the CloudFront Key Groups Console and create a key group using the public key from above.
- Now go to the CloudFront Distributions Console and edit the "Behaviors" tab of newly created distribution above.
- Restrict viewer access: Yes
- Trusted authorization type: Trusted key groups
- Add key groups: Select the Key Group created above
Custom Domain
You can directly use the CloudFront distribution ID with Django Storages. However, it would be good to use a custom subdomain (such as media.example.com) of your domain for better user experience. To do this:
- Navigate to the CloudFront Distributions Console and click "Add domain" under "General" tab of your CloudFront distribution.
- TLS Certificate: Create an ACM (AWS Certificate Manager) certificate by clicking "Create certificate" button.
- This will require adding a
CNAMErecord in the DNS settings of your domain. Once the record is validated, the custom domain will be added.- You can then use the custom subdomain with Django Storages. You use it by setting AWS_S3_CUSTOM_DOMAIN in
settings.pyfile.
- You can then use the custom subdomain with Django Storages. You use it by setting AWS_S3_CUSTOM_DOMAIN in
- This will require adding a
- TLS Certificate: Create an ACM (AWS Certificate Manager) certificate by clicking "Create certificate" button.
- In order for this setup to work, you still have to add another
CNAMErecord that points your subdomain (media.example.com) to CloudFront distribution name.
IAM Setup
Now that CloudFront and S3 are configured, you'll need to create a new IAM user that can write to your S3 bucket. This will be used on Django server to handle media file uploads. This user doesn't need GetObject access, as the reads are handled by CloudFront.
- Navigate to the IAM Console and add a new policy as follows:
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "ListBucketAccess",
"Effect": "Allow",
"Action": [
"s3:ListBucket"
],
"Resource": "arn:aws:s3:::your-bucket-name"
},
{
"Sid": "BucketWriteAccess",
"Effect": "Allow",
"Action": [
"s3:PutObject",
"s3:DeleteObject"
],
"Resource": "arn:aws:s3:::your-bucket-name/*"
}
]
}- Now go to IAM Users Console and create a new user.
- Choose "Attach policies directly" and select the newly created policy from above.
- Once the user is created, go to "Security Credentials" tab of the user and click "Create access key" to create a new credential pair.
- Choose "Use case" as Other.
- Make note of the Access Key and Secret Key that are generated.
Update Bucket Policy
You also need to grant the new IAM user access in your bucket policy alongside the CloudFront distribution. Add the following statement to your existing policy to enable these permissions:
{
"Sid": "AllowDjangoWrite",
"Effect": "Allow",
"Principal": {
"AWS": "arn:aws:iam::your-aws-account-id:user/your-iam-user"
},
"Action": [
"s3:PutObject",
"s3:DeleteObject"
],
"Resource": "arn:aws:s3:::your-bucket-name/*"
}Django Setup
Now update your Django project to use S3 and CloudFront Signed URLs for serving media files in production.
Configuration
Update the .env file (or environment variables) to include following:
- AWS_STORAGE_BUCKET_NAME=your-bucket-name
- AWS_S3_REGION_NAME="ap-south-2"
- AWS_S3_ENDPOINT_URL="https://s3.ap-south-2.amazonaws.com"
- AWS_ACCESS_KEY_ID=your-iam-user-access-key
- AWS_SECRET_ACCESS_KEY=your-iam-user-secret-key
- AWS_S3_CUSTOM_DOMAIN="media.example.com"
- AWS_CLOUDFRONT_KEY_ID=your-cloudfront-public-key-id
- AWS_CLOUDFRONT_KEY=your-cloudfront-private-key
WARNING
Do not alter the CloudFront private key. Copy it exactly into AWS_CLOUDFRONT_KEY while preserving all new lines, and make sure you wrap the entire value in double quotes.
settings.py
Update the following in your Django project's settings.py file:
INSTALLED_APPS = [
# ...
"storages",
# ...
]
STATIC_URL = "static/"
STATICFILES_DIRS = [
BASE_DIR / "static" # project-level assets
]
STATIC_ROOT = BASE_DIR / "staticfiles" # the output directory for collectstatic
# AWS S3 and CloudFront config for django-storages
AWS_STORAGE_BUCKET_NAME = os.getenv("AWS_STORAGE_BUCKET_NAME")
AWS_S3_REGION_NAME = os.getenv("AWS_S3_REGION_NAME")
AWS_S3_ENDPOINT_URL = os.getenv("AWS_S3_ENDPOINT_URL")
AWS_ACCESS_KEY_ID = os.getenv("AWS_ACCESS_KEY_ID")
AWS_SECRET_ACCESS_KEY = os.getenv("AWS_SECRET_ACCESS_KEY")
AWS_S3_CUSTOM_DOMAIN = os.getenv(
"AWS_S3_CUSTOM_DOMAIN"
) # Alternate custom domain set in CloudFront UI
AWS_CLOUDFRONT_KEY_ID = os.getenv("AWS_CLOUDFRONT_KEY_ID")
AWS_CLOUDFRONT_KEY = os.getenv("AWS_CLOUDFRONT_KEY", "").encode("utf-8")
AWS_S3_FILE_OVERWRITE = (
False # append suffix on name clash instead of silently overwriting
)
AWS_DEFAULT_ACL = None # ACLs disabled on bucket; don't send ACL header (Object privacy enforced by "Block all public access" on the bucket)
AWS_S3_VERIFY = True # Explicitly enforce SSL cert verification on S3 requests
AWS_S3_SIGNATURE_VERSION = (
"s3v4" # required for boto3 upload signing; some regions reject SigV2
)
AWS_QUERYSTRING_EXPIRE = 14400 # CloudFront signed URL expiry in seconds (4 hours)
AWS_S3_ADDRESSING_STYLE = "virtual" # bucket-name.s3.amazonaws.com style
AWS_S3_OBJECT_PARAMETERS = {
"CacheControl": "max-age=86400", # instruct CDN/browser to cache the object for 24 hours
}
if DEBUG:
# LOCAL SETUP: serve media from local filesystem
STORAGES = {
"default": {
"BACKEND": "django.core.files.storage.FileSystemStorage",
},
"staticfiles": {
"BACKEND": "django.contrib.staticfiles.storage.StaticFilesStorage",
},
}
# Ensure you have these set so local files know where to save
MEDIA_URL = "/media/"
MEDIA_ROOT = BASE_DIR / "media"
else:
# PROD SETUP: store media in AWS S3, serve via CloudFront signed URLs
STORAGES = {
"default": {
"BACKEND": "storages.backends.s3boto3.S3Boto3Storage",
},
"staticfiles": {
"BACKEND": "django.contrib.staticfiles.storage.StaticFilesStorage",
},
}
# Media is served via the CloudFront custom domain
MEDIA_URL = f"https://{AWS_S3_CUSTOM_DOMAIN}/"Apply Changes
NOTE
Make sure your Apache config needs to point its /static alias to the local static files directory.
- Run
collectstaticand restart Apache server:
uv run manage.py collectstatic && sudo systemctl restart apache2TIP
You can now use the upload_to parameter in ImageField or FileField of the Django models, to define the S3 storage path for the media files.
