Skip to content

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

a rack of electronic equipment in a dark room
Photo by Tyler on Unsplash

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.
turned-on gray laptop computer
Photo by Safar Safarov on Unsplash

2. Install the AWS‑S3 upload provider

The official provider is published as @strapi/provider-upload-aws-s3. Install it with your package manager:

bash
# Using npm
npm install @strapi/provider-upload-aws-s3 --save

# Using yarn
yarn add @strapi/provider-upload-aws-s3

The 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.

js
// 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 use public-read, URLs are returned as plain S3 URLs.
  • signedUrlExpires controls 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:

json
[
  {
    "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):

js
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:

OptionDescription
storageClassChoose S3 storage class, e.g., INTELLIGENT_TIERING or STANDARD_IA.
encryptionEnable server‑side encryption. Example: { type: 'aws:kms', kmsKeyId: env('AWS_KMS_KEY_ID') }.
preventOverwrite: trueStops accidental overwrites of existing objects.
checksumAlgorithmSpecify checksum, e.g., CRC64NVME.
tagsAdd S3 object tags for cost allocation or lifecycle rules.
multipartUploadFine‑tune multipart thresholds and part size.

These options are added under providerOptions.s3Options in the same configuration file.

8. Testing the configuration

  1. Upload a file through the Strapi admin UI (Media Library → Upload).
  2. 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=...
  3. Paste the URL into a new browser tab. It should download the file within the expiration window.
  4. 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 credentials object; the old top‑level accessKeyId/secretAccessKey keys still work but emit deprecation warnings.
  • The default signedUrlExpires moved from a provider‑level setting to the params object.

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:

Dockerfile
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

SymptomLikely causeFix
Signed URL returns 403 even though bucket is privateCORS rule missing or mis‑typed AllowedOriginsAdd 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 omittedSet ACL: 'private' in s3Options.params.
Upload fails with InvalidAccessKeyIdCredentials not read from environment or typo in variable nameVerify env('AWS_ACCESS_KEY_ID') and env('AWS_ACCESS_SECRET') are correctly defined.
CSP blocks images from S3Bucket domain not added to img-src/media-src directivesUpdate config/middlewares.js to include the bucket domain.
After Strapi 4 upgrade, provider throws "providerOptions.s3Options is undefined"Old configuration still uses top‑level keysMove 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.

A white cloud icon on a glass square with floating data charts and cylinders
Photo by Hazel Z on Unsplash

12. Summary

Configuring Strapi 5 to use Amazon S3 with private signed URLs involves four main steps:

  1. Install the official @strapi/provider-upload-aws-s3 package.
  2. Add a provider configuration that sets the bucket to private, defines signedUrlExpires, and supplies credentials via environment variables.
  3. Adjust bucket CORS and Strapi CSP so browsers can fetch the signed URLs.
  4. (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.

TagsStrapi 5Amazon S3media uploadprivate assetssigned URLsheadless CMS

Sources

  1. Amazon S3 upload provider – Strapi Documentation
  2. Media Library Providers – Strapi Documentation
  3. AWS S3 provider source code – Strapi GitHub
  4. @strapi/provider-upload-aws-s3 npm package
  5. Migration guide: Amazon S3 provider credentials – Strapi v4 to v5
  6. Provider README – Amazon S3 (Strapi)

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.

All articles