MediumFixing S3 Access Denied Errors in Node.js Applications | by Adekola Olawale | Medium
Sitemap

Fixing S3 Access Denied Errors in Node.js Applications

5 min readOct 20, 2025

--

Press enter or click to view image in full size

Amazon S3 (Simple Storage Service) is one of the most commonly used storage solutions for developers today, particularly for handling uploads, static assets, or backups in Node.js applications.

But there’s one error that seems to haunt developers of all experience levels:

“Access Denied” when trying to upload, read, or delete files in S3.

This error can appear simple, but like a locked door, it often hides multiple possible causes behind it. In this article, we’ll break down why “Access Denied” happens, use analogies to make it intuitive, and then walk through code-level fixes and IAM configuration steps to solve it permanently.

Understanding “Access Denied” — What It Really Means

Think of S3 as a secure warehouse that stores files (objects) inside containers (buckets).

To get inside and retrieve something:

  • You need keys (your AWS credentials).
  • You need permission (IAM policy and bucket policy).
  • And you need to enter through the right door (region, bucket, and endpoint).

If any of these don’t match, wrong key, locked door, or restricted permission, AWS responds with a polite but firm “Access Denied.”

Common Causes of “Access Denied” in Node.js Apps

Before diving into code, let’s map the major culprits:

Press enter or click to view image in full size
Common causes of the “Access Denied” error when using Amazon S3 in Node.js applications

Step 1: Setting Up AWS SDK in Node.js

To start, you’ll need the AWS SDK for JavaScript:

npm install aws-sdk

Then initialize it in your Node.js app:

const AWS = require('aws-sdk');

// Configure AWS credentials and region
AWS.config.update({
accessKeyId: process.env.AWS_ACCESS_KEY_ID,
secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY,
region: 'us-east-1' // Replace with your bucket's region
});

const s3 = new AWS.S3();

And in this code, you can see that:

  • We’re importing the AWS SDK.
  • Then setting up credentials and the region, both must match your IAM and S3 bucket setup.
  • Finally, we create an S3 instance we’ll use to interact with the bucket.

Step 2: Trying an Upload and Seeing “Access Denied”

Let’s simulate a file upload to better understand where the error comes from:

const fs = require('fs');
const path = require('path');

const uploadFile = async () => {
const fileContent = fs.readFileSync(path.join(__dirname, 'example.txt'));

const params = {
Bucket: 'my-test-bucket',
Key: 'uploads/example.txt',
Body: fileContent
};

try {
const data = await s3.upload(params).promise();
console.log('File uploaded successfully at:', data.Location);
} catch (err) {
console.error('Error uploading file:', err.message);
}
};

uploadFile();

If this throws:

Error uploading file: Access Denied

Then you know your credentials worked (the request reached AWS), but the user didn’t have the required authorization to upload to that bucket.

Step 3: Fixing IAM User or Role Permissions

Go to your AWS IAM Console → Users → select your user → Permissions.

Attach a policy similar to this one:

{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": [
"s3:PutObject",
"s3:GetObject",
"s3:ListBucket"
],
"Resource": [
"arn:aws:s3:::my-test-bucket",
"arn:aws:s3:::my-test-bucket/*"
]
}
]
}

And in this configuration:

  • "s3:PutObject" allows uploads.
  • "s3:GetObject" allows downloads.
  • "s3:ListBucket" allows listing objects.
  • The /* after the bucket name allows access to all items within that bucket.

If your Node.js app runs on EC2, ECS, or Lambda, make sure the execution role (not just your user) has this policy attached.

Step 4: Checking the Bucket Policy

Even with correct IAM permissions, the bucket’s policy can override access.

Get Adekola Olawale’s stories in your inbox

Join Medium for free to get updates from this writer.

Go to S3 Console → Bucket → Permissions → Bucket Policy and verify it allows your principal or your role.

Here’s an example bucket policy allowing uploads from a specific account:

{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "AllowUploadsFromMyApp",
"Effect": "Allow",
"Principal": {
"AWS": "arn:aws:iam::123456789012:user/my-app-user"
},
"Action": ["s3:PutObject"],
"Resource": "arn:aws:s3:::my-test-bucket/*"
}
]
}

Step 5: Ensuring the Correct Region and Endpoint

Sometimes, you might have full permissions, but still get “Access Denied” because you’re knocking on the wrong door (region mismatch).

Example:
Your bucket is in us-west-2, but your SDK config is set to us-east-1.

You can fix it by updating your configuration:

AWS.config.update({ region: 'us-west-2' });

To confirm, check the bucket’s region in the S3 console under Properties → Bucket region.

Step 6: Handling Public Access Settings and ACLs

If you’re uploading public assets like profile pictures, make sure your objects are allowed public read (if needed):

const params = {
Bucket: 'my-test-bucket',
Key: 'uploads/example.txt',
Body: fileContent,
ACL: 'public-read' // Optional, only for public assets
};

Also, disable the “Block all public access” setting in S3 only if absolutely necessary, and preferably use signed URLs instead for controlled access.

Step 7: Using Pre-Signed URLs (Secure Temporary Access)

For many apps, especially those with frontends that directly upload to S3, it’s safer to use pre-signed URLs:

const params = {
Bucket: 'my-test-bucket',
Key: 'uploads/example.txt',
Expires: 60 // URL expires in 60 seconds
};

const url = s3.getSignedUrl('putObject', params);
console.log('Upload your file using:', url);

Here:

  • The Node.js app generates a temporary upload link.
  • The client (e.g., browser) uploads directly to S3 using that link.
  • This avoids exposing AWS credentials and reduces “Access Denied” errors since permissions are scoped and time-limited.

Step 8: Debugging “Access Denied” in Depth

When you still face the error, add logging and tracing:

s3.upload(params, (err, data) => {
if (err) {
console.error('Full error object:', JSON.stringify(err, null, 2));
} else {
console.log('Success:', data);
}
});

Look out for:

  • Code: AccessDenied
  • Region mismatch
  • SignatureDoesNotMatch (wrong credentials or bucket region)
  • InvalidPrincipal (IAM role not valid or deleted)

You can also test your IAM permissions using AWS CLI:

aws s3 ls s3://my-test-bucket --profile myuser

If the CLI also returns “Access Denied,” then it’s definitely a policy or permission issue, not a Node.js code issue.

Step 9: Final Checklist

✅ AWS credentials are valid and loaded correctly.
✅ IAM policy allows S3 actions (s3:PutObject, s3:GetObject, etc.).
✅ Bucket policy allows your IAM user or role.
✅ Region in code matches the bucket region.
✅ Public access settings or ACLs are correct.
✅ For frontend uploads, pre-signed URLs are used.

Conclusion

“Access Denied” in AWS S3 isn’t really an error, it’s a safety guard doing its job. It means AWS is protecting your bucket from unauthorized actions.

Once you understand the flow of authentication and authorization between IAM, S3 bucket policies, and Node.js configuration, it becomes easy to fix.

So the next time you face “Access Denied,” think of it as your app knocking on the wrong door, and you now have the master key to open the right one.

--

--

Adekola Olawale
Adekola Olawale