Tighten IAM Permissions with Least Privilege
Learn how to apply the principle of least privilege by scoping Lambda execution roles to specific AWS actions and resources.
All services used in this lesson are covered by the AWS Free Tier.
AWS Services Used
Learning Outcomes
By the end of this lesson, you will be able to:
- Explain what least privilege means for this project.
- Identify which Lambda function needs which AWS actions.
- Replace overly broad permissions with narrower, resource-scoped policies.
- Explain why S3 object actions and DynamoDB table actions should be limited to specific resources.
- Understand the lifecycle of permissions from development to production.
The Core Idea
Now that the dashboard works, the next hardening step is to stop relying on permissions that are broader than necessary. AWS IAM and Lambda guidance both recommend least privilege: grant only the permissions required to do the task, and restrict those permissions to the specific resources the workload actually needs.
Mapping Functions to Permissions
In this project, each Lambda function has a narrow job. Their IAM permissions should reflect that:
1) Upload-Ticket Lambda
- Action:
s3:PutObject(required to generate a presigned upload request). - Resource:
arn:aws:s3:::YOUR_BUCKET/incoming/*.
2) Metadata-Writer Lambda
- Action:
dynamodb:PutItem. - Resource:
arn:aws:dynamodb:REGION:ACCOUNT:table/upload_metadata.
3) Metadata-Detail Lambda
- Actions:
dynamodb:GetItemands3:GetObject(for download links). - Resources: Specific table and specific bucket prefix.
4) Metadata-List Lambda
- Action:
dynamodb:Query(not Scan, and definitely not Put/Delete). - Resource: Specific table.
The One Permission Every Function Needs
Your Lambda functions always need permission to write logs to CloudWatch. Least privilege does not mean removing logging; it means keeping everything else as narrow as possible.
Standard logging actions:
logs:CreateLogGrouplogs:CreateLogStreamlogs:PutLogEvents
Example: Narrow Policy for Upload Ticket
Instead of using s3:* on all resources, use a policy like this:
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "AllowUploadPrefixOnly",
"Effect": "Allow",
"Action": ["s3:PutObject"],
"Resource": "arn:aws:s3:::YOUR_UPLOAD_BUCKET/incoming/*"
},
{
"Sid": "WriteLogs",
"Effect": "Allow",
"Action": [
"logs:CreateLogGroup",
"logs:CreateLogStream",
"logs:PutLogEvents"
],
"Resource": "*"
}
]
}
What to Remove
If you see these in your production roles, they are candidates for tightening:
"Action": "s3:*""Action": "dynamodb:*""Resource": "*"(except for logging where*is often required for the log group creation).
Lab Checklist
| Step | Success Condition |
|---|---|
| Audit Roles | Every Lambda has its own execution role |
| Scope S3 | Object actions are limited to a specific bucket and prefix |
| Scope DynamoDB | Table actions are limited to the metadata table only |
| Remove Wildcards | * is removed from non-logging Actions |
| Verify Function | Functions still work after tightening permissions |
Micro-activity 1: Audit Your Project
For each function, write the narrowest permission it actually needs and the specific resource ARN it should be scoped to.
Micro-activity 2: Reflection
Think about it
Why is s3:GetObject on a specific prefix better than s3:* on all resources? Why is dynamodb:Query preferred over broader permissions for a list function?
Summary
Least privilege is the most important security habit in the cloud. By stopping the use of shared, broad roles and moving to narrow, per-function permissions, you significantly reduce the "blast radius" of any potential security event.