Configuring AWS services for Laravel

By Kelvin Ngunyi

Laravel provides support for various AWS services such as DynamoDB, SES, S3, SQS, etc. To use these services, it is standard practice to configure access by setting the AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY in the .env file. Using access keys is not the most recommended approach for configuring access, as it has some security flaws:

  • Permissions scope: Our access keys might be incorrectly scoped (e.g. using AmazonS3FullAccess permission) allowing for access to more AWS services and resources than required.

  • Access control: If an attacker retrieves our access keys, they will be able to access your services and resources from a different machine.

  • Traceability: It is not easy to trace back which user owns which access keys.

In addition to the above, long-lived tokens are considered a security risk because of:

  • Exposure: If credentials are leaked in version control systems, logs, or shared externally, they can be exploited until explicitly revoked. You may have heard stories about leaked AWS keys where the scammers have created many servers to mine crypto in the exposed AWS accounts.

  • Persistence: Without rotation policies, these tokens can remain valid for years, creating a wide attack surface for potential breaches.

  • Compliance issues: Some organizations need to comply with standards like SOC 2, ISO 27001 or HIPAA, which often require minimization of long-lived secrets.

Identity and Access Management (IAM) roles allow us to grant specific permissions (on specific resources) to specific entities. These entities can be users, applications or even other resources. IAM roles follow the principle of least privilege. This means we should grant only the minimum permissions required to the least number of entities.

Let's explore how IAM roles can be used, using AWS S3 storage as an example:

Laravel allows us to use various drivers to store our application's files. Using AWS S3 buckets is a popular choice because of how reliable, easy to manage, secure and scalable the driver is.

Laravel configuration

Let's create our Laravel application using the Laravel installer. If you already have an application to use, feel free to skip this step

laravel new storage-app

Once our application is setup, let's install the Flysystem S3 package. Currently, Laravel 12 recommends using the latest version (v3):

composer require league/flysystem-aws-s3-v3 "^3.0" --with-all-dependencies

The Flysystem package is the file storage library Laravel uses under the hood to interact with S3.

Once we finish the above steps, we can push up the folder to our remote repository for deployment.

AWS setup

S3 bucket setup

Within our AWS account, let's create an S3 storage bucket. Once we input the bucket name, feel free to leave the other sensible configurations as default. Let's name our bucket test iam-storage-bucket . Our bucket name should be globally unique, not just unique within our S3 account.

EC2 setup

Let's create an EC2 instance to host our application. We can use any name and the sensible default configurations. The settings and method we use to create our instance doesn’t change how we will assign the IAM roles. You can also use your already existing instances.

Once our EC2 instance is ready, let's deploy our application as well. This can be through manual deployment scripts or through a server management solution such as Laravel Forge.

Once we've deployed our application, let's configure our .env configuration to use our storage bucket:

AWS_BUCKET=test-iam-storage-bucket
AWS_DEFAULT_REGION=eu-north-1
AWS_USE_PATH_STYLE_ENDPOINT=false

Let’s not configure the AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY environment keys. When they are not included, our application will default to using the IAM role attached to the EC2 instance.

Let's access our tinker shell using php artisan tinker and run the following to test if we can connect to our S3 bucket:

Storage::disk('s3')->files();

We receive the following error:

Reason: Error retrieving credentials from the instance profile metadata service. (Client error: `GET http://xxx.xxx.xxx.xxx/latest/meta data/iam/security-credentials/` resulted in a `404 Not Found` response: xml version="1.0" encoding="iso-8859-1"?>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://ww (truncated...)
).

This is because we have no IAM role attached to our EC2 instance.

IAM Configuration

Permission setup

Before we set up our role, we need to create the permission policy we'll attach to the role. Let's navigate to IAM > Policies > Create Policy . Let's switch to the JSON editor instead of the visual editor for a faster experience.

Once the JSON editor is open, add the following permissions within the Statement > Action section:

  • s3:ListBucket

  • s3:GetObject

  • s3:GetObjectAcl

  • s3:PutObject

  • s3:PutObjectAcl

  • s3:ReplicateObject

  • s3:DeleteObject

Within the Statement > Resource section, let's specify the exact S3 bucket we'll be connecting to by fetching the Amazon Resource Name (ARN) from our S3 bucket's properties' tab. (E.g. arn:aws:s3:::test-iam-storage-bucket)

Thus, our full configuration should be similar to this:

{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "Statement1",
"Effect": "Allow",
"Action": [
"s3:ListBucket",
"s3:GetObject",
"s3:GetObjectAcl",
"s3:PutObject",
"s3:PutObjectAcl",
"s3:ReplicateObject",
"s3:DeleteObject"
],
"Resource": [
"arn:aws:s3:::test-iam-storage-bucket",
"arn:aws:s3:::test-iam-storage-bucket/*"
]
}
]
}

Once configured, proceed to give your policy an easy to identify name (e.g. s3-test-iam storage-bucket-access ) and submit for creation.

Role setup

We now need to create an IAM role for our EC2 instance to use. Let's navigate to IAM > Roles > Create role and select AWS Service as the Trusted Entity Type. This will allow us to assign this IAM role to our EC2 instance. For our use-case, let's select EC2.

On the next page, let's search and add our configured policy:

With our policy attached to our role, we can give it an appropriate name (.e.g s3-test-iam storage-bucket-access ) and save our role.

EC2 IAM role

Back on our EC2 instance view page, let's modify the IAM roles via the Actions dropdown: Actions > Security > Modify IAM role

Within this page, we should be able to select our new IAM role and attach it to our EC2 instance.

Verification

Once the above steps are configured, let's verify that we're able to interact with the S3 bucket as via the following php artisan tinker commands:

Storage::disk('s3')->put('file.txt','Hello, World');
Storage::disk('s3')->files();
Storage::disk('s3')->get('file.txt');
Storage::disk('s3')->delete('file.txt');
Storage::disk('s3')->files();

Amazing, right? With the above setup, our S3 bucket is more secure while our EC2 instance has all the required access. We can also apply the same principles if we want to grant read-only access or if we want to prevent deletions on an AWS level.

Kelvin Ngunyi
Software Developer
Author Image

Interested in speaking with a developer?

Connect with us.
©2025 Kirschbaum Development Group LLC Privacy Policy Terms of Service