Configure Strapi 5 Media Uploads to Amazon S3 with Private Signed URLs
Learn step‑by‑step how to install the AWS‑S3 upload provider, set up private buckets, generate signed URLs, and adjust security policies for Strapi 5 media handling.
By the Revisionary team · · 4 min read
Strapi 5 can store all uploaded media in an Amazon S3 bucket and automatically return signed URLs when the bucket is private. The process consists of installing the official @strapi/provider-upload-aws-s3 package, adding a provider configuration in config/plugins.{js|ts}, setting the bucket ACL to private (or omitting the public ACL), defining the signedUrlExpires value, and adjusting both CORS on the bucket and Strapi’s Content Security Policy (CSP). The following guide walks you through each step, shows the full configuration snippet, and highlights migration considerations from Strapi 4.
1. Prerequisites
- Strapi 5 (any 5.x version) running on Node 18‑24.
- An Amazon S3 bucket that you control.
- IAM user or role with the following permissions:
s3:PutObject,s3:GetObject,s3:ListBucket,s3:DeleteObject,s3:PutObjectAcl. - Access to the bucket’s CORS settings.
- (Optional) Environment variables for credentials and region to keep secrets out of source control.
2. Install the AWS‑S3 upload provider
The official provider is published as @strapi/provider-upload-aws-s3. Install it with your package manager:
# Using npm
npm install @strapi/provider-upload-aws-s3 --save
# Using yarn
yarn add @strapi/provider-upload-aws-s3The package works with both JavaScript and TypeScript projects.
3. Basic configuration
Create or edit the plugin configuration file at config/plugins.js (or config/plugins.ts for TypeScript). The provider expects a nested object under upload.config.
// config/plugins.js
module.exports = ({ env }) => ({
upload: {
config: {
provider: 'aws-s3',
providerOptions: {
// Provider‑specific options
s3Options: {
credentials: {
accessKeyId: env('AWS_ACCESS_KEY_ID'),
secretAccessKey: env('AWS_ACCESS_SECRET'),
},
region: env('AWS_REGION'),
params: {
Bucket: env('AWS_BUCKET'),
// Private bucket – do NOT set ACL to public-read
ACL: 'private',
// Signed URL expiration in seconds (default 900 s)
signedUrlExpires: env.int('AWS_SIGNED_URL_EXPIRES', 15 * 60),
},
},
// Optional CDN base URL (e.g., CloudFront)
// baseUrl: env('AWS_CDN_URL'),
},
},
},
});Key points
provider: 'aws-s3'tells Strapi to use the AWS S3 provider.- All AWS SDK v3 options live under
s3Options. - Setting
ACL: 'private'triggers automatic signed‑URL generation for every uploaded file. If you usepublic-read, URLs are returned as plain S3 URLs. signedUrlExpirescontrols how long the signed URL is valid. The default is 15 minutes (900 seconds).
4. Enabling private access and signed URLs
When the bucket is private, Strapi’s upload service automatically calls getSignedUrl for each file. The signed URL includes the required X-Amz-* query parameters, so front‑end consumers can download the asset without exposing the bucket publicly.
If you need a custom signing strategy (e.g., rotating keys), you can override the provider by creating a directory ./providers/aws-s3 and implementing isPrivate() and getSignedUrl(file) methods. For most projects the built‑in logic is sufficient.
5. Configure bucket CORS
A private bucket blocks browser access unless CORS allows the request origin. Add a CORS rule similar to the following in the AWS S3 console:
[
{
"AllowedHeaders": ["*"],
"AllowedMethods": ["GET", "HEAD"],
"AllowedOrigins": ["https://your-strapi-domain.com"],
"ExposeHeaders": [],
"MaxAgeSeconds": 3000
}
]This rule permits the Strapi front end (or any trusted domain) to fetch signed URLs via GET requests.
6. Adjust Strapi’s Content Security Policy
Strapi ships with a security middleware (strapi::security) that defines CSP directives. To allow images and media from your S3 bucket, extend the contentSecurityPolicy in config/middlewares.js (or the new config/middlewares.{js|ts} format):
module.exports = ({ env }) => [
// ... other middlewares
{
name: 'strapi::security',
config: {
contentSecurityPolicy: {
useDefaults: true,
directives: {
// Add the bucket domain to img-src and media-src
'img-src': ["'self'", env('AWS_BUCKET_DOMAIN')],
'media-src': ["'self'", env('AWS_BUCKET_DOMAIN')],
},
},
},
},
];Replace AWS_BUCKET_DOMAIN with something like my-bucket.s3.amazonaws.com. This ensures the browser will not block the signed URLs.
7. Advanced provider options (optional)
The provider supports several extra features that can be useful for large‑scale or compliance‑focused deployments:
| Option | Description |
|---|---|
storageClass | Choose S3 storage class, e.g., INTELLIGENT_TIERING or STANDARD_IA. |
encryption | Enable server‑side encryption. Example: { type: 'aws:kms', kmsKeyId: env('AWS_KMS_KEY_ID') }. |
preventOverwrite: true | Stops accidental overwrites of existing objects. |
checksumAlgorithm | Specify checksum, e.g., CRC64NVME. |
tags | Add S3 object tags for cost allocation or lifecycle rules. |
multipartUpload | Fine‑tune multipart thresholds and part size. |
These options are added under providerOptions.s3Options in the same configuration file.
8. Testing the configuration
- Upload a file through the Strapi admin UI (Media Library → Upload).
- After the upload completes, inspect the file entry. The URL shown should look like:
code https://my-bucket.s3.amazonaws.com/your-folder/filename.jpg?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=...&X-Amz-Expires=900&X-Amz-SignedHeaders=host&X-Amz-Signature=... - Paste the URL into a new browser tab. It should download the file within the expiration window.
- Verify that a direct, unsigned URL (without the query string) returns 403 Forbidden, confirming the bucket is private.
9. Migration from Strapi 4
Strapi 4 also supported an S3 provider, but the configuration shape changed in v5:
- All S3‑specific options must now be nested under
s3Options. - Credentials are required inside a
credentialsobject; the old top‑levelaccessKeyId/secretAccessKeykeys still work but emit deprecation warnings. - The default
signedUrlExpiresmoved from a provider‑level setting to theparamsobject.
If you are upgrading, replace your old config/plugins.js with the v5 structure shown in Section 3, run npm install @strapi/provider-upload-aws-s3@latest, and restart Strapi.
10. Deploying to production
When deploying to environments such as Vercel, Railway, or traditional VPS, ensure that:
- Environment variables (
AWS_ACCESS_KEY_ID,AWS_ACCESS_SECRET,AWS_REGION,AWS_BUCKET, etc.) are defined in the hosting platform. - The IAM role attached to the host (if using EC2 or ECS) has the required S3 permissions.
- The CSP middleware file is included in the build (it lives in
config/which is bundled with the Strapi project).
A typical Docker Dockerfile snippet for production might look like:
FROM node:18-alpine AS builder
WORKDIR /app
COPY package*.json .
RUN npm ci
COPY . .
RUN npm run build
FROM node:18-alpine AS runner
WORKDIR /app
COPY --from=builder /app/build ./
COPY --from=builder /app/node_modules ./node_modules
COPY --from=builder /app/config ./config
EXPOSE 1337
CMD ["node", "server.js"]The config folder contains the S3 provider configuration and CSP adjustments, ensuring the container starts with the correct settings.
11. Common pitfalls and troubleshooting
| Symptom | Likely cause | Fix |
|---|---|---|
| Signed URL returns 403 even though bucket is private | CORS rule missing or mis‑typed AllowedOrigins | Add your Strapi domain to the bucket CORS configuration. |
Uploaded file URL is a plain S3 URL (no X-Amz- query) | ACL set to public-read or omitted | Set ACL: 'private' in s3Options.params. |
| Upload fails with InvalidAccessKeyId | Credentials not read from environment or typo in variable name | Verify env('AWS_ACCESS_KEY_ID') and env('AWS_ACCESS_SECRET') are correctly defined. |
| CSP blocks images from S3 | Bucket domain not added to img-src/media-src directives | Update config/middlewares.js to include the bucket domain. |
| After Strapi 4 upgrade, provider throws "providerOptions.s3Options is undefined" | Old configuration still uses top‑level keys | Move all S3 options under s3Options as shown in the v5 example. |
If problems persist, consult the Strapi community forum or the official provider README for more details.
12. Summary
Configuring Strapi 5 to use Amazon S3 with private signed URLs involves four main steps:
- Install the official
@strapi/provider-upload-aws-s3package. - Add a provider configuration that sets the bucket to private, defines
signedUrlExpires, and supplies credentials via environment variables. - Adjust bucket CORS and Strapi CSP so browsers can fetch the signed URLs.
- (Optional) Enable advanced options such as encryption, storage class, or custom signing logic.
Once set up, every media asset uploaded through Strapi is stored securely in S3, and the CMS automatically returns time‑limited signed URLs that protect your files from public exposure while keeping the front‑end integration seamless.
For teams that need full‑history versioning, the Revisionary plugin can be added on top of this setup, providing field‑level versioning, audit logs, and a trash bin—see the product documentation for details.
Sources
The Revisionary team
Developers who build Revisionary, a version history plugin for Strapi 5. We write about Strapi, web development, AI and application security.
This article was written with the help of AI.