Set up Amazon S3 Upload Provider Plugin for Your Strapi App

As far as I understand we don’t support private s3 buckets.

I have a similar issue, but in my case there’s no public access issue in AWS to fix. The image URLs that end up in Postgres simply lack the region.

Followed the tutorial at: How to Set up Amazon S3 Upload Provider Plugin for Your Strapi App

Expected example URL: https://my-bucket.s3.us-east-1.amazonaws.com/my-image.png
Actually generated URL: https://my-bucket.s3.amazonaws.com/my-image.png

Relevant parts of my ./backend/config/plugins.js:

upload: {
	config: {
		provider: 'aws-s3',
		providerOptions: {
			accessKeyId: env('AWS_ACCESS_KEY_ID'),
			secretAccessKey: env('AWS_ACCESS_SECRET'),
			region: env('AWS_REGION'),
			params: {
				Bucket: env('AWS_BUCKET'),
			},
		},
	      // These parameters could solve issues with 
		actionOptions: {
			upload: {ACL: null},
			uploadStream: {ACL: null},
			delete: {},
		},
	},
},

Relevant parts of my ./backend/config/middlewares.js:

{
	name: "strapi::security",
	config: {
		contentSecurityPolicy: {
			useDefaults: true,
			directives: {
				"connect-src": ["'self'", "https:"],
				"img-src": [
					"'self'",
					"data:",
					"blob:",
				//	"dl.airtable.com",
					"my-bucket.s3.us-east-1.amazonaws.com",
				],
				"media-src": [
					"'self'",
					"data:",
					"blob:",
				//	"dl.airtable.com",
					"my-bucket.s3.us-east-1.amazonaws.com",
				],
				upgradeInsecureRequests: null,
			},
		},
	},
},

Any hint would be appreciated! :heart:

I think there is an exception for US east that the region is not included.

Thanks for replying… can you elaborate a little bit? Who made the exception? Strapi or AWS? Why would it not be “included”? Included in what list? Maintained by whom?

Ok I figured it out. Turns out I was wrong about the region being a requirement in the URL. In fact, AWS is smart enough to figure out the region even if omitted and just redirects to the “final” URL with region in it. So Strapi is not wrong to omit the region in the stored image URLs in the database.

The reason why my set-up didn’t work is that the tutorial is actually incorrect in two important ways (which may be new conditions as of now 01/2023):

1.) The suggested AWS security settings don’t actually enable default public access for all objects uploaded to the S3 bucket.

I followed these instructions instead to make the bucket and all new objects in it, truly public:

2.) Also, the suggested middlewares.js > strapi::security settings do not work for me.

They effectively prevent the display of objects from your bucket, even if AWS S3 is configured to allow public access to all new objects.

The correct settings actually omit the region. These settings are used as a kind of security filter before displaying the images. Since they are stored in the database without region, the “match filter” applied by security needs to also spell out the hostname without region, like so:

config: {
	contentSecurityPolicy: {
		useDefaults: true,
		directives: {
			"connect-src": ["'self'", "https:"],
			"img-src": [
				"'self'",
				"data:",
				"blob:",
				`${process.env.AWS_BUCKET}.s3.amazonaws.com`,
			],
			"media-src": [
				"'self'",
				"data:",
				"blob:",
				`${process.env.AWS_BUCKET}.s3.amazonaws.com`,
			],
			upgradeInsecureRequests: null,
		},
	},
},

Hope this helps someone who’s stuck with a similar problem.

For me it’s working on my localhost but when i try to upload files on the deployed production admin I get an Internal server error. Does anyone know how to fix this?

1 Like

Hi, Sorry for not responding sooner. I was on vacation. I will take a look at this and make sure the documentation for the providers is updated accordingly.

I have the same problem, tearing my hairs out at the moment.

I added the plugins.js to the config/env/production directory and added the APP KEYS to Digital Ocean but to no avail.

This code in the middleware finally solve it for me:

module.exports = ({ env }) => [
  "strapi::errors",
  {
    name: "strapi::security",
    config: {
      contentSecurityPolicy: {
        useDefaults: true,
        directives: {
          "connect-src": ["'self'", "https:"],
          "img-src": [
            "'self'",
            "data:",
            "blob:",
            "dl.airtable.com",
            `https://${env("AWS_BUCKET")}.s3.${env("AWS_REGION")}.amazonaws.com/`,
          ],
          "media-src": [
            "'self'",
            "data:",
            "blob:",
            "dl.airtable.com",
            `https://${env("AWS_BUCKET")}.s3.${env("AWS_REGION")}.amazonaws.com/`,
          ],
          upgradeInsecureRequests: null,
        },
      },
    },
  },
  "strapi::cors",
  "strapi::poweredBy",
  "strapi::logger",
  "strapi::query",
  "strapi::body",
  "strapi::session",
  "strapi::favicon",
  "strapi::public",
];

I’ll update the article and try to address some of the issues raised.

Is there any way to use this plugin with a self hosted minio s3 bucket?

Nope since this provider uses the aws-sdk

I just thought I’d add a working example with the config I have made to my code to get it to work seeing as I had a lot of trouble getting things to display correctly on both Strapi and my front-end.
I use cloudformation to deploy my service on the cloud so I have attached a couple of code extract in the hope it helps someone.

I have added the following variables in my .env file. Note for local development I still use the local upload provider.

CORS_ALLOWED_ORIGINS=["*"]

#AWS + s3 upload provider variables
AWS_ACCESS_KEY_ID=
AWS_SECRET_ACCESS_KEY=
AWS_REGION=
UPLOAD_AWS_BUCKET_NAME=
UPLOAD_IMAGE_PROVIDER=local
UPLOAD_MEDIA_SOURCE=["'self'", "data:", "blob:", "dl.airtable.com"]

This is my middlewares.ts config file:

export default ({ env }) => [
  "strapi::errors",
  {
    name: "strapi::security",
    config: {
      contentSecurityPolicy: {
        useDefaults: true,
        directives: {
          "connect-src": ["'self'", "https:"],
          "img-src": env.array("UPLOAD_MEDIA_SOURCE", ["*"]),
          "media-src": env.array("UPLOAD_MEDIA_SOURCE", ["*"]),
          upgradeInsecureRequests: null,
        },
      },
    },
  },
  {
    name: "strapi::cors",
    config: {
      origin: env.array("CORS_ALLOWED_ORIGINS", ["*"]),
    },
  },
  "strapi::poweredBy",
  "strapi::logger",
  "strapi::query",
  "strapi::body",
  "strapi::session",
  "strapi::favicon",
  "strapi::public",
];

Finally, this is my plugins.ts files

export default ({ env }) => ({
  "users-permissions": {
    config: {
      jwt: {
        expiresIn: "7d",
      },
    },
  },
  upload: {
    config: {
      provider: env("UPLOAD_IMAGE_PROVIDER", "local"),
      providerOptions: {
        accessKeyId: env("AWS_ACCESS_KEY_ID"),
        secretAccessKey: env("AWS_SECRET_ACCESS_KEY"),
        region: env("AWS_REGION"),
        params: {
          Bucket: env("UPLOAD_AWS_BUCKET_NAME"),
        },
      },
    },
  },
});

These are the values I pass into my task definition - this is a simple AWS::ECS::TaskDefinition to host strapi using Fargate:

          Environment:
            - Name: NODE_ENV
              Value: !Ref Environment
            - Name: CORS_ALLOWED_ORIGINS
              Value: !Sub "[https://www.${DomainAddress}, https://${ApiDomainAddress}]"
            - Name: DATABASE_NAME
              Value: !Sub "strapi_${Environment}"
            - Name: DATABASE_PORT
              Value: !Ref DatabasePort
            - Name: DATABASE_HOST
              Value: !GetAtt DBInstance.Endpoint.Address
            - Name: DATABASE_USERNAME
              Value: !Sub "{{resolve:secretsmanager:${RDSInstanceRotationSecret}::username}}"
            - Name: DATABASE_PASSWORD
              Value: !Sub "{{resolve:secretsmanager:${RDSInstanceRotationSecret}::password}}"
            - Name: UPLOAD_AWS_BUCKET_NAME
              Value: !Ref StrapiImageUploadBucket
            - Name: UPLOAD_MEDIA_SOURCE
              Value: !Sub "['self', data:, blob:, dl.airtable.com, ${StrapiImageUploadBucket.RegionalDomainName}]"
            - Name: UPLOAD_IMAGE_PROVIDER
              Value: !Ref UploadImageProvider

The main thing to consider is that the AWS credentials and region are read from the Task’s AWS::IAM::Role so I don’t pass these into the task itself. The bucket declaration itself is a very basic AWS::S3::Bucket with mostly the default configuration. See below:

  StrapiImageUploadBucket:
    Type: AWS::S3::Bucket
    DeletionPolicy: Delete
    Properties:
      BucketName: !Sub "${ServiceName}-strapi-image"
      BucketEncryption:
        ServerSideEncryptionConfiguration:
          - ServerSideEncryptionByDefault:
              SSEAlgorithm: "AES256"
      LifecycleConfiguration:
        Rules:
          - Id: !Sub "${ServiceName}-strapi-image"
            NoncurrentVersionExpirationInDays: !Ref BucketLifecycleRetention
            Status: Enabled

Happy to provide any help or further examples should anybody require these.

Cheers

I am using Strapi v4. I am using S3 which is secured so there won’t be public access. I use a signed URL to access the S3. But the media library cannot be modified to display the preview with the signed URL and the media library cannot be modified so that when we copy the link will be signed.

Is there any way to modify the URL in the media library.

Hello,
thank you for the tutorial.
I succeeded to implement a custom aws upload provider with a custom url to get the thumbnail in the media library section using this line of code.

file.url = `${CDN}/${Key}/${file.hash}${file.ext}`

Right now strapi store the file.url inside the database, how i can say to strapi to not store the file url inside the database but instead use directly the environment variable when it display the thumbnails ?

FYI - if anyone else noticed the broken images in the markplace it’s due to the content security policy. Just add an additional line of code: 'https://market-assets.strapi.io/', to both image-src and media-src to fix this.

1 Like

I just wanted to add my 2 cents since I just spent a great deal of time on this. The instructions provided on how to do this are correct. I unfortunately had uploaded a ton of images before getting the instructions correct and did not have the ACL: env(‘AWS_ACL’, ‘public-read’) set in the plugins file earlier.

If this happens to you, the images will not be accessible and it will throw you for a loop if you have all the other settings correct everywhere. You have to mark the uploaded files ‘public-read’ in the S3 bucket. This can be done in different ways, but if you have files somewhere in 100-1000 count like I did, you can simple do it from the AWS console by selecting the files and marking them public-read ACL. Hope this helps someone!

1 Like

Thank you for the correction.

Hi All, I have strapi v4.20 and I am unable to upload files to the media library. I tried with 2 plugins first time with aws-s3 and second time with “local server”. It gives me the same error message like in the screenshot below with both the options.
image

when I inspect the response from the browser, I get a message error 403 with both the plugins above. Can someone help to point me the right direction to resolve this issue?

plugins.ts with local upload
export default ({ env }) => ({
upload: {
config: {
providerOptions: {
localServer: {
maxage: 300000
},
},
},
},
});

middleware.ts when with local and s3 upload option
export default [
‘strapi::logger’,
‘strapi::errors’,
‘strapi::cors’,
‘strapi::poweredBy’,
‘strapi::query’,
‘strapi::session’,
‘strapi::favicon’,
‘strapi::public’,

{
name: ‘strapi::security’,
config: {
contentSecurityPolicy: {
useDefaults: true,
directives: {
‘connect-src’: [“‘self’”, ‘https:’],
‘img-src’: [
“‘self’”,
‘data:’,
‘blob:’,
market-assets.strapi.io’,
https://bucketname.s3.ap-southeast-1.amazonaws.com/’, //tried with various URL (with region, without region none works, same error)
],
‘media-src’: [
“‘self’”,
‘data:’,
‘blob:’,
market-assets.strapi.io’,
https://bucketname.s3.ap-southeast-1.amazonaws.com/’, //tried with various URL (with region, without region none works, same error)

      ],
      upgradeInsecureRequests: null,
    },
  },
},

},
// …

// …
{
name: “strapi::body”,
config: {
formLimit: “256mb”, // modify form body
jsonLimit: “256mb”, // modify JSON body
textLimit: “256mb”, // modify text body
formidable: {
maxFileSize: 250 * 1024 * 1024, // multipart data, modify here limit of uploaded file size
},
},
},
// …
];

pluginst.ts with s3 upload
module.exports = ({ env }) => ({
// …
upload: {
config: {
provider: ‘aws-s3’,
providerOptions: {
s3Options: {
accessKeyId: env(‘AWS_ACCESS_KEY_ID’),
secretAccessKey: env(‘AWS_ACCESS_SECRET’),
region: env(‘AWS_REGION’),
params: {
ACL: env(‘AWS_ACL’, ‘private’),
Bucket: env(‘AWS_BUCKET’),
},
},
},
actionOptions: {
upload: {},
uploadStream: {},
delete: {},
},
},
},
// …
});
.env
HOST=0.0.0.0
PORT=1337
APP_KEYS=xxxxx
API_TOKEN_SALT=xxxxx
ADMIN_JWT_SECRET=xxxxxx
TRANSFER_TOKEN_SALT=xxxxx

Database

DATABASE_CLIENT=mysql
DATABASE_HOST=xxxx
DATABASE_PORT=3306
DATABASE_NAME=strapi
DATABASE_USERNAME=xxx
DATABASE_PASSWORD=xxx
DATABASE_SSL=true
JWT_SECRET=xxxx
#AWS-S3-Bucket-to-upload-assets
AWS_BUCKET_NAME=strapi-poc-bucket
AWS_REGION=ap-southeast-1
AWS_ACCESS_KEY_ID=xxxx
AWS_ACCESS_SECRET=xxxx
CORS_ALLOWED_ORIGINS=[“*”]

This actually worked for me, but is there anything i can do which will help me upload inside a folder in s3 bucket. Coz right now the images are directly going on the root of the bucket…