🎯 Introduction
When building a Next.js app with Strapi as a CMS, media management is a critical challenge. Most guides show you the "easy" way: setting your S3 bucket to public-read. This is a major security risk, allowing anyone to access and abuse your bucket.
In this article, we will do it the professional and most secure way:
- Your S3 Bucket will be 100% Private.
- CloudFront (CDN) will be the only public gateway to access files.
- Strapi will upload to S3 and return the correct CloudFront URL.
- Next.js will display optimized images from CloudFront.
- Everything will be served from your own custom domain (e.g.,
media.yourdomain.com).
Estimated Cost: Virtually FREE for millions of requests, thanks to CloudFront's generous permanent Free Tier (1TB/month).
Part 1: 🏗️ Building the AWS Foundation (S3, IAM, CloudFront)
1.1. Create S3 Bucket (The Private Vault)
- Go to the S3 service in your AWS Console.
- Create a new bucket (e.g.,
my-app-media). - Important: Under "Block Public Access settings," check "Block all public access". This is the key to our entire security architecture.
1.2. Create IAM User (The Key for Strapi)
Strapi needs "keys" to upload files. We'll create a user with the least privilege necessary.
- Go to the IAM service > Users > Create user.
- Name it (e.g.,
strapi-s3-uploader). - Choose "Attach policies directly" > "Create policy".
- Select the JSON tab and paste the following policy (replace
YOUR-BUCKET-NAME): - After creating the user, go to the "Security credentials" tab > "Create access key". Save the Access key and Secret access key.
plaintext1{ 2 "Version": "2012-10-17", 3 "Statement": [ 4 { 5 "Sid": "AllowStrapiToManageFiles", 6 "Effect": "Allow", 7 "Action": [ 8 "s3:PutObject", 9 "s3:GetObject", 10 "s3:DeleteObject" 11 ], 12 "Resource": "arn:aws:s3:::YOUR-BUCKET-NAME/*" 13 }, 14 { 15 "Sid": "AllowStrapiToListBucket", 16 "Effect": "Allow", 17 "Action": "s3:ListBucket", 18 "Resource": "arn:aws:s3:::YOUR-BUCKET-NAME" 19 } 20 ] 21}
Note: We have removed s3:PutObjectAcl because we will not be using ACLs.
1.3. Create CloudFront (The Public Gateway)
- Go to the CloudFront service > Create distribution.
- Origin domain: Choose your S3 bucket.
- S3 bucket access:
- Select "Yes use OAC (Origin Access Control)".
- Click "Create new OAC". CloudFront will create it.
- Click the Copy policy button and then Update S3 bucket policy (CloudFront will automatically update your S3 bucket's policy to allow
GetObject).
- Viewer Protocol Policy: Select "Redirect HTTP to HTTPS".
- Create the distribution and save the Distribution domain name (e.g.,
d12345abcdef.cloudfront.net).
Part 2: 🌐 Configuring a Custom Domain (e.g., media.yourdomain.dev)
2.1. Request an SSL Certificate (ACM)
- Go to the AWS Certificate Manager (ACM) service.
- Very Important: Switch your region to
us-east-1 (N. Virginia). CloudFront requires certificates to be in this region. - Request a public certificate for your domain (e.g.,
media.yourdomain.dev). - Select DNS validation. AWS will give you a
CNAMErecord to add to your DNS. - Add this CNAME record at your DNS provider. Wait for the status to become "Issued".
2.2. Point Your Domain to CloudFront
- At your DNS provider, create a new
CNAMErecord:- Host/Name:
media - Value/Target: Point it to your CloudFront domain (
d12345abcdef.cloudfront.net).
- Host/Name:
2.3. Update the CloudFront Distribution
- Go back to CloudFront, select your distribution > "Edit".
- Alternate domain name (CNAME): Add your domain (
media.yourdomain.dev). - Custom SSL certificate: Select the certificate you just created in ACM.
- Save and wait for it to deploy.
Part 3: ✍️ Configuring Strapi (To Auto-Return CDN URLs)
This is the "golden" config for Strapi that solves all our problems (ACLs, s3Options, URLs).
3.1. Install the Provider
plaintext1npm install @strapi/provider-upload-aws-s3
3.2. Configure plugins.js
Create the file ./config/plugins.js with this content:
javascript1// ./config/plugins.js 2module.exports = ({ env }) => ({ 3 upload: { 4 config: { 5 provider: 'aws-s3', 6 providerOptions: { 7 // S3 SDK config (important, must be in s3Options) 8 s3Options: { 9 accessKeyId: env('AWS_ACCESS_KEY_ID'), 10 secretAccessKey: env('AWS_SECRET_ACCESS_KEY'), 11 region: env('AWS_REGION'), 12 }, 13 // Bucket info 14 params: { 15 Bucket: env('AWS_BUCKET_NAME'), 16 }, 17 // AUTOMATICALLY return the CloudFront URL 18 baseUrl: env('CDN_URL'), 19 }, 20 actionOptions: { 21 // Disable ACLs to work with new secure S3 buckets 22 upload: { 23 ACL: null 24 }, 25 uploadStream: { 26 ACL: null 27 }, 28 delete: {}, 29 }, 30 }, 31 }, 32});
3.3. Configure .env
Add these variables to your Strapi .env file:
plaintext1AWS_ACCESS_KEY_ID=YOUR_KEY 2AWS_SECRET_ACCESS_KEY=YOUR_SECRET 3AWS_REGION=ap-southeast-1 4AWS_BUCKET_NAME=my-app-media 5CDN_URL=https://media.yourdomain.dev
Restart Strapi. Now, when you upload a file, Strapi will automatically save the CDN URL to the database!
Part 4: 🖼️ Configuring Next.js (To Display Optimized Images)
Because Strapi now returns the correct URL, the Next.js part is incredibly simple.
4.1. Configure next.config.js
We need to "whitelist" our CDN domain for the Next.js <Image> component.
javascript1// next.config.js 2import type { NextConfig } from 'next'; 3 4const nextConfig: NextConfig = { 5 images: { 6 remotePatterns: [ 7 // Add a pattern for your new CDN domain 8 { 9 protocol: 'https', 10 hostname: 'media.yourdomain.dev', 11 port: '', 12 pathname: '/**', 13 }, 14 ], 15 }, 16}; 17 18export default nextConfig;
Restart your Next.js server.
4.2. Use in Your Component
No replace() functions or helpers are needed. Just use the URL straight from the API.
javascript1import Image from 'next/image'; 2 3function MyPost({ post }) { 4 // This URL is already "https://media.yourdomain.dev/file.jpg" 5 const imageUrl = post.attributes.image.data.attributes.url; 6 7 return ( 8 <Image 9 src={imageUrl} 10 alt="An image description" 11 width={500} 12 height={300} 13 /> 14 ); 15}
Part 5: 🚨 Handling the "Gotchas" (CORS & CSP)
After the steps above, you'll see images on your Next.js site, but they will be broken in the Strapi Admin Panel. Here's why:
5.1. CORS Error (CloudFront blocks Strapi Admin)
- The Problem: Your browser (running Strapi Admin on
localhost:1337) tries to load an image frommedia.yourdomain.dev, but CloudFront doesn't allow it. - The Solution: Create a Response Headers Policy in CloudFront.
- Go to CloudFront > Policies > Response headers > Create.
- Name it (e.g.,
Strapi-Admin-CORS-Policy). - Enable CORS.
- Access-Control-Allow-Origins: Add two lines:
http://localhost:1337and your production Strapi domain (e.g.,https://admin.yourdomain.dev). - Access-Control-Allow-Methods: Choose
ALL. - Access-Control-Allow-Headers: Choose
*. - Attach this policy to your CloudFront Distribution (in the Behaviors tab > Edit).
5.2. CSP Error (Strapi Admin blocks itself)
- The Problem: Strapi has its own security layer (Content Security Policy) that says "I only trust images from
self,data:, etc." It doesn't trust your newmedia.yourdomain.devdomain. - The Solution: Update your Strapi's
./config/middlewares.jsfile.
javascript1// ./config/middlewares.js 2module.exports = [ 3 'strapi::errors', 4 { 5 name: 'strapi::security', 6 config: { 7 contentSecurityPolicy: { 8 useDefaults: true, 9 directives: { 10 'connect-src': ["'self'", 'https:'], 11 'img-src': [ 12 "'self'", 13 'data:', 14 'blob:', 15 'https://media.yourdomain.dev', // <-- ADD YOUR DOMAIN 16 ], 17 'media-src': [ 18 "'self'", 19 'data:', 20 'blob:', 21 'https://media.yourdomain.dev', // <-- ADD YOUR DOMAIN 22 ], 23 }, 24 }, 25 }, 26 }, 27 'strapi::cors', 28 'strapi::poweredBy', 29 // ...other middlewares 30 'strapi::public', 31];
Restart your Strapi server, and your images will now appear perfectly everywhere.
Part 6: 🔒 A Final Warning: "What if my Access Key is leaked?"
You must understand this: CORS and CSP are browser-level security. They DO NOT protect you if your Access Key is leaked.
If your AWS_SECRET_ACCESS_KEY is leaked, an attacker can use it from their own server (bypassing CORS entirely) to delete your entire S3 bucket.
Your most important security responsibilities are:
- NEVER commit your
.envfile to Git (always add.envto.gitignore). - Use environment variable management services (like Vercel, Netlify, or AWS Secrets Manager) for deployment.
- (Advanced): Restrict your IAM Policy to only accept requests from your Strapi server's static IP address.
Congratulations! You have now built a powerful, secure, and cost-effective media system.
