Lambda functions can’t do anything without explicit permission. That’s the core of least privilege for IAM roles, and it’s a surprisingly powerful concept when you’re debugging why your aws lambda invoke command just failed.
Let’s say you have a Lambda function that needs to read an object from an S3 bucket. The simplest way to get it working is to attach a broad S3 read policy to its execution role. But that’s not least privilege. Least privilege means the Lambda function only has permission to read that specific object from that specific bucket.
Here’s a Lambda function, my-s3-reader, that’s supposed to read data.json from my-data-bucket.
// index.js
const AWS = require('aws-sdk');
const s3 = new AWS.S3();
exports.handler = async (event) => {
const bucket = 'my-data-bucket';
const key = 'data.json';
try {
const data = await s3.getObject({ Bucket: bucket, Key: key }).promise();
console.log(`Successfully read object: ${key}`);
return {
statusCode: 200,
body: JSON.stringify({ message: `Successfully read ${key}`, content: data.Body.toString() }),
};
} catch (error) {
console.error(`Error reading object ${key} from bucket ${bucket}:`, error);
return {
statusCode: 500,
body: JSON.stringify({ message: `Error reading ${key}`, error: error.message }),
};
}
};
The Lambda function needs an IAM role to execute. This role has two main parts:
- Trust Policy: This defines who can assume the role. For Lambda, it’s always the Lambda service.
- Permissions Policy: This defines what actions the assumed role can perform. This is where least privilege comes in.
Here’s what a non-least-privilege trust policy might look like for the Lambda service:
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Principal": {
"Service": "lambda.amazonaws.com"
},
"Action": "sts:AssumeRole"
}
]
}
This is standard and correct for Lambda. Now, let’s look at the permissions policy that’s often attached by default or through quick setup:
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": [
"s3:GetObject"
],
"Resource": "*"
}
]
}
This policy allows s3:GetObject on any S3 resource (*). This is bad. If your Lambda function is ever compromised, or if you accidentally deploy it to the wrong account, it could potentially read sensitive data from any bucket in your AWS account.
The least-privilege version of this permissions policy would be:
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": [
"s3:GetObject"
],
"Resource": "arn:aws:s3:::my-data-bucket/data.json"
}
]
}
This policy only allows s3:GetObject on the specific object data.json within the my-data-bucket. If the Lambda function tries to read another object, or read from a different bucket, it will fail.
When you create a Lambda function, you can either create a new IAM role or choose an existing one. To implement least privilege, you’d typically:
- Create a new IAM role.
- Attach the standard Lambda trust policy.
- Attach a new permissions policy that is tailored to the function’s needs.
For our my-s3-reader function, the minimum required permissions are:
s3:GetObjectonarn:aws:s3:::my-data-bucket/data.jsonlogs:CreateLogGroupandlogs:CreateLogStream,logs:PutLogEventsonarn:aws:logs:us-east-1:123456789012:log-group:/aws/lambda/my-s3-reader:*(these are for CloudWatch Logs, which Lambda needs to write its output and errors).
So, the complete least-privilege permissions policy for my-s3-reader would look like this:
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": [
"s3:GetObject"
],
"Resource": "arn:aws:s3:::my-data-bucket/data.json"
},
{
"Effect": "Allow",
"Action": [
"logs:CreateLogGroup",
"logs:CreateLogStream",
"logs:PutLogEvents"
],
"Resource": "arn:aws:logs:us-east-1:123456789012:log-group:/aws/lambda/my-s3-reader:*"
}
]
}
The Resource for CloudWatch Logs is a wildcard because the Lambda service creates log streams dynamically. The specific ARN format is arn:aws:logs:<region>:<account-id>:log-group:/aws/lambda/<function-name>:*. Replace <region> and <account-id> with your actual values.
When you apply this policy, and you try to invoke the Lambda function via the AWS CLI:
aws lambda invoke --function-name my-s3-reader --payload "{}" output.json
If data.json exists in my-data-bucket, the output.json file will contain a 200 status code and the content. If you try to read another.json from the same bucket, or data.json from another-bucket, the output.json will show a 500 error, and the Lambda logs will indicate a AccessDenied error from S3.
The key takeaway is that you should always be as specific as possible with your IAM Resource ARNs. For S3, this means specifying the bucket and optionally the object. For other services, it means specifying the correct ARN format for the resource the Lambda function needs to interact with. This granular control is the foundation of a secure AWS environment.
The next error you’ll hit after mastering S3 object permissions is understanding how to grant write access to S3 objects, which involves different actions and potentially different resource specifications.