Principle of Least Privilege in AWS
The principle of least privilege means every IAM identity in your AWS account should have only the permissions it needs to do its job. Nothing more. This is the single most effective practice for limiting the damage when credentials are compromised, misconfigured, or misused.
Most AWS accounts fail to follow least privilege consistently. Developers attach broad managed policies during development, those policies ship to production, and nobody revisits them. Over time, roles accumulate permissions far beyond what they actually use. This page shows you how to avoid that.
You will learn how AWS permissions work at a high level, how to build least-privilege policies step by step, and how to use AWS tools like IAM Access Analyzer to tighten permissions over time. The examples cover DynamoDB, S3, EC2 tag restrictions, and CI/CD pipeline scoping.
Simple explanation
Least privilege is a straightforward idea: give people and services access to only what they need.
Analogy: hotel key cards
Think of IAM roles like hotel key cards. A guest’s card opens their room and the gym. It does not open every room on every floor, the manager’s office, or the maintenance closet. If the card is lost, the worst that happens is someone gets into one room, not the entire building. In AWS, a role is the key card. The policy attached to it defines which doors it opens. Least privilege means configuring that policy so it opens only the doors the role actually needs.
Why least privilege matters in AWS
Permissions that are not needed are not harmless. They are a liability. When a credential is compromised through a leaked access key, a misconfigured service, or a supply chain attack, the attacker inherits every permission attached to that credential.
If a Lambda function that processes order confirmations has AdministratorAccess, a single exploit on that function gives the attacker full control of your AWS account. If the same function has only dynamodb:PutItem on the orders table, the worst outcome is a bad database row.
Least privilege does not eliminate breaches, but it controls how far a breach can spread. Security practitioners call this reducing the blast radius. A role with AdministratorAccess has a massive blast radius. A role scoped to one S3 bucket has a tiny one.
Beyond security, least privilege also reduces operational risk. A developer who accidentally runs a destructive command against production cannot cause damage if their role does not have delete permissions. Tighter permissions create guardrails that protect teams from their own mistakes.
How AWS permissions work
Before applying least privilege, you need a basic understanding of how AWS decides whether to allow or deny a request. For the full details, see IAM policy structure in AWS.
Identity-based policies
These are JSON documents attached to IAM users, groups, or roles. They define what actions that identity can perform on which resources. Most of the policies you write for least privilege are identity-based.
Resource-based policies
Some AWS resources (S3 buckets, KMS keys, SQS queues) have their own policies that define who can access them. These work alongside identity-based policies. For cross-account access, resource-based policies are often required.
Actions, resources, and conditions
Every IAM policy statement has three core parts:
- Action: the specific AWS API calls being allowed or denied (e.g.,
s3:GetObject,dynamodb:PutItem) - Resource: the specific AWS resources the actions apply to, defined by ARN (e.g., a single S3 bucket or DynamoDB table)
- Condition: optional constraints like IP range, MFA requirement, time of day, or resource tags. See IAM policy conditions explained for details.
Explicit deny vs allow
AWS defaults to deny. Everything is denied unless explicitly allowed. If any policy explicitly denies an action, that deny wins, even if another policy allows it. This means you do not need to write deny rules for most scenarios. You simply omit the allow.
Why wildcards are dangerous
Using "Action": "*" or "Resource": "*" in a policy means “everything.” A policy that allows all actions on all resources is functionally AdministratorAccess. Wildcards are the most common source of over-permissioning. Always specify the exact actions and resource ARNs when possible.
Where SCPs and permissions boundaries fit
Service control policies (SCPs) set maximum permission limits across an entire AWS Organization or account. Permissions boundaries set limits on individual roles. Neither grants access on its own. They only restrict what identity-based policies can allow. These are useful for enforcing least privilege at the organizational level.
How to apply least privilege step by step
Applying least privilege is an iterative process, not a one-time configuration. Here is a practical workflow:
1. Identify the exact API actions needed
Before writing any policy, ask: what AWS API calls will this service actually make? A web application that reads product data from DynamoDB needs dynamodb:GetItem and dynamodb:Query on that table. It does not need dynamodb:DeleteTable.
Check the AWS documentation for the service to find the specific action names. Write them down before you start writing the policy.
2. Scope access to specific resources
Use specific resource ARNs in your policies. A policy that allows s3:GetObject on arn:aws:s3:::my-app-uploads/* is far safer than one that allows it on * (all buckets).
3. Use conditions where possible
Conditions add another layer of restriction. You can limit access based on resource tags, source IP, MFA status, or AWS region. Tag-based conditions are especially useful when you cannot predict resource ARNs in advance.
4. Prefer customer managed policies for production workloads
AWS managed policies are convenient for development but often grant more permissions than a specific workload needs. For production, write customer managed policies scoped to your workload’s actual requirements.
5. Start broad only temporarily, then reduce
If you are uncertain which exact permissions a workload needs, it is acceptable to start with a broader policy in development. But schedule a follow-up to tighten it. Never ship a broad policy to production without review.
A useful pattern: attach a broader policy in dev, let the workload run for a few weeks, then use IAM Access Analyzer to generate a policy based on what the workload actually used. Replace the broad policy with the generated one before promoting to production.
6. Use IAM Access Analyzer policy generation
IAM Access Analyzer analyzes CloudTrail logs and generates a policy recommendation based on actual usage. If a role has 20 permissions but only uses 4 of them over 90 days, Access Analyzer flags the unused 16.
# Generate a policy based on CloudTrail activity for a role
aws accessanalyzer start-policy-generation \
--policy-generation-details '{"principalArn": "arn:aws:iam::123456789012:role/MyAppRole"}' \
--cloud-trail-details '{
"accessRole": "arn:aws:iam::123456789012:role/AccessAnalyzerRole",
"trailArn": "arn:aws:cloudtrail:us-east-1:123456789012:trail/MyTrail",
"startTime": "2026-01-01T00:00:00Z"
}'The generated policy includes only the actions actually observed in the logs. Use this as a starting point for writing your production policy.
7. Review last accessed information
In the IAM console, every role shows “last accessed” data for each service. If a role has not used a permission in months, it is a strong candidate for removal.
8. Validate policies before rollout
Use the IAM Policy Simulator to test policies against specific API calls before attaching them to production roles. Use Access Analyzer policy validation to check for syntax issues, overly broad access, and security warnings.
9. Separate dev and prod access
A developer role in a dev account should not have access to production resources. Use separate roles per environment, with separate policies scoped to that environment’s resources. Keeping environments in separate AWS accounts (using AWS Organizations) ensures a compromised dev credential cannot touch production data.
10. Prefer roles and temporary credentials over long-lived keys
IAM roles issue temporary credentials through STS that expire automatically. Long-lived access keys are a persistent risk. If they leak, they work until someone manually rotates them. For why this matters, see why long-lived access keys are dangerous.
Least privilege is not a destination. It is a practice. Start with tighter permissions than you think you need, then expand based on actual errors. It is much easier to add permissions than to track down and remove them later.
Practical examples
DynamoDB table access
This policy allows a Lambda function to read from and write to a single DynamoDB table.
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "DynamoDBProductsTableReadWrite",
"Effect": "Allow",
"Action": [
"dynamodb:GetItem",
"dynamodb:Query",
"dynamodb:BatchGetItem",
"dynamodb:PutItem",
"dynamodb:UpdateItem"
],
"Resource": "arn:aws:dynamodb:us-east-1:123456789012:table/products"
}
]
}What it allows: Reading and writing individual items in the products table.
What it does NOT allow: Deleting items, deleting the table, scanning the entire table, or accessing any other DynamoDB table. It also does not allow access to the table’s indexes. If the function needs to query a GSI, you would add the index ARN separately.
Why this is least privilege: The function can only perform the specific read/write operations it needs, on the one table it uses. Compare this to AmazonDynamoDBFullAccess, which allows all DynamoDB actions on all tables in the account.
S3 bucket access
This policy allows an application to read and write objects in a specific S3 bucket.
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "S3AppUploadsReadWrite",
"Effect": "Allow",
"Action": ["s3:GetObject", "s3:PutObject"],
"Resource": "arn:aws:s3:::my-app-uploads/*"
},
{
"Sid": "S3AppUploadsList",
"Effect": "Allow",
"Action": "s3:ListBucket",
"Resource": "arn:aws:s3:::my-app-uploads"
}
]
}What it allows: Reading and writing objects in the my-app-uploads bucket, and listing the bucket contents.
What it does NOT allow: Deleting objects, deleting the bucket, modifying bucket policies, enabling/disabling versioning, or accessing any other bucket. Note that s3:ListBucket targets the bucket ARN itself, while object actions target the /* path.
Why this is least privilege: The application can upload and retrieve files but cannot delete data or modify bucket configuration. An attacker who compromises this role cannot exfiltrate data from other buckets or destroy existing uploads.
Tag-based EC2 restrictions
This policy allows starting and stopping EC2 instances, but only those tagged with a specific environment.
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "EC2DevInstancesOnly",
"Effect": "Allow",
"Action": ["ec2:StartInstances", "ec2:StopInstances"],
"Resource": "*",
"Condition": {
"StringEquals": {
"aws:ResourceTag/Environment": "dev"
}
}
}
]
}What it allows: Starting and stopping EC2 instances that have the tag Environment=dev.
What it does NOT allow: Starting or stopping production instances, terminating any instances, or performing any other EC2 actions. The Resource: "*" looks broad, but the condition restricts it to only dev-tagged instances.
Why this is least privilege: Tag-based conditions let you scope access dynamically without hardcoding instance IDs. Developers can manage their own dev instances without any risk to production.
Analogy: tag-based conditions
Tag-based restrictions work like colour-coded wristbands at a festival. Everyone enters the main grounds, but only the red wristband gets you backstage. The gate does not check your name. It checks your wristband colour. In AWS, the tag is the wristband. The condition in the policy is the gate.
CI/CD deployment role
This policy scopes a CI/CD pipeline role to deploy only specific Lambda functions and update their code.
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "LambdaDeploy",
"Effect": "Allow",
"Action": [
"lambda:UpdateFunctionCode",
"lambda:UpdateFunctionConfiguration",
"lambda:PublishVersion"
],
"Resource": "arn:aws:lambda:us-east-1:123456789012:function:order-*"
},
{
"Sid": "S3ArtifactRead",
"Effect": "Allow",
"Action": "s3:GetObject",
"Resource": "arn:aws:s3:::deploy-artifacts/order-service/*"
}
]
}What it allows: Updating code and configuration for Lambda functions whose names start with order-, and reading deployment artifacts from a specific S3 path.
What it does NOT allow: Creating or deleting Lambda functions, modifying IAM roles, accessing other S3 buckets, or deploying to any function outside the order- namespace. The pipeline cannot escalate its own privileges or deploy to unrelated services.
Why this is least privilege: A compromised CI/CD pipeline is a serious risk. Scoping its role to specific deployment actions on specific resources limits what an attacker can do if they gain control of the pipeline.
The AdministratorAccess policy and the wildcard "Action": "*" should exist only on break-glass emergency roles with strict audit logging and MFA requirements. Attaching it to application roles or CI/CD pipelines is a high-severity security finding.
When to use least privilege
Least privilege applies everywhere permissions exist, but some use cases benefit the most:
- Application roles: every application running on EC2, ECS, or Lambda should have a role scoped to only the AWS services and resources it actually uses.
- Lambda and compute workloads: Lambda functions often need access to one or two services. Giving them broad access is unnecessary and dangerous given how frequently functions are deployed.
- Developer access: developers need enough access to build and debug, but rarely need production write access. Use separate roles for dev and prod, and prefer read-only prod access for most developers.
- CI/CD pipelines: pipelines should deploy specific resources, not manage your entire AWS account. A deployment role should only have permissions for the services the pipeline deploys.
- Cross-account access: when granting access across AWS accounts (via role assumption), scope the assumed role tightly. The trust relationship defines who can assume the role. The role’s policy defines what they can do.
- Production access: standing production access should be minimal. Most production work should happen through deployment pipelines, not manual console access.
- Break-glass emergency roles: even emergency roles should be scoped where possible. If they must have broad access, require MFA, log all activity to CloudTrail, and alert on usage.
Least privilege vs broad access
The difference between least privilege and over-permissioned access is significant in both security posture and operational risk:
| Factor | Least privilege | Broad access (e.g., AdministratorAccess) |
|---|---|---|
| Blast radius if compromised | Limited to specific resources and actions | Full account access: data deletion, resource creation, privilege escalation |
| Accidental damage risk | Low. Destructive actions are not permitted. | High. Any mistake can affect any resource. |
| Audit clarity | Clear. Each role’s purpose is visible in its policy. | Unclear. You cannot tell what a role is supposed to do vs what it can do. |
| Compliance readiness | Meets most compliance frameworks (SOC 2, ISO 27001, PCI DSS) | Fails most compliance audits |
| Initial setup effort | Higher. Requires identifying specific permissions. | Lower. Attach one broad policy and move on. |
| Long-term maintenance | Lower. Permissions are intentional and documented. | Higher. Unknown permissions create debugging and security overhead. |
AWS managed policies vs customer managed policies
AWS managed policies are pre-built by AWS for common use cases. Customer managed policies are written by you. For least privilege in production, the distinction matters:
| Aspect | AWS managed policies | Customer managed policies |
|---|---|---|
| Scope | Broad. Designed for general use cases. | Narrow. Scoped to your specific workload. |
| Resource specificity | Usually * (all resources) | Specific ARNs for your resources |
| Maintenance | AWS updates them, which may add permissions you do not want | You control every change through version control |
| Best for | Development, prototyping, learning | Production workloads, compliance environments |
| Least privilege fit | Poor. Too broad for most production roles. | Strong. Built specifically for the permissions needed. |
AWS managed policies are a fine starting point. Use them to learn which permissions a workload needs, then write a customer managed policy that includes only those permissions on specific resources. Manage your policies in version control using tools like Terraform so changes go through code review.
The over-permissioning trap
Over-permissioning happens in predictable stages. A developer builds a new Lambda function and attaches AmazonS3FullAccess to unblock themselves quickly. The function ships. The permission is never revisited. Six months later, nobody remembers why it has full S3 access, and removing it feels risky because it might break something.
This pattern compounds over time. An account that started with tight permissions gradually accumulates broad, unexplained policies. Each time someone hits an access denied error, the fastest fix is to add a broader policy, which makes the problem worse.
Analogy: the junk drawer
Over-permissioned IAM roles are like the junk drawer in your kitchen. It starts with one item you did not know where to put. A year later, the drawer has batteries, tape, keys to unknown locks, and a phone charger for a phone you no longer own. Nobody wants to clean it out because something in there might be important. The same thing happens with IAM permissions. The fix is the same too: go through it regularly, throw out what you do not need, and label what you keep.
The countermeasure is building least privilege into your deployment pipeline. Use IAM Access Analyzer in CI/CD to flag new policies that are broader than necessary. Require policy reviews before deployment. Use Terraform for IAM so permissions are tracked in version control and changes go through pull request review.
Common mistakes
- Using wildcard actions or wildcard resources.
"Action": "s3:*"or"Resource": "*"are short-term fixes that become long-term liabilities. Always scope to the specific actions and resource ARNs needed. - Leaving broad access in place after testing. Developers often attach a broad policy to unblock themselves during development. The policy ships to production, and nobody revisits it. Schedule a permission review before any production deployment.
- Giving CI/CD pipelines too much power. A deployment pipeline needs to deploy specific resources, not manage your entire AWS account. A compromised pipeline with
AdministratorAccessis a full account compromise. - Not separating dev and prod. Using the same role or the same permissions across environments means a mistake in dev can affect production. Use separate accounts through AWS Organizations and separate roles per environment.
- Not distinguishing read from write. Many applications need to read from a resource but should never write to it. Always split read and write permissions into separate statements so you can grant them independently.
- Relying only on AWS managed policies in production. Managed policies are a reasonable starting point, but they are too broad for production workloads. Transition to customer managed policies that name specific actions and resources.
- Ignoring review and validation after launch. Your application changes over time. Run IAM Access Analyzer quarterly to identify unused permissions. Review last accessed data. Remove permissions that are no longer needed.
Summary
- Least privilege means granting only the permissions needed for a specific task. Nothing more.
- Smaller permission sets reduce blast radius when credentials are compromised.
- Identify the exact API actions a workload needs, then scope policies to specific resource ARNs.
- Use conditions (tags, IP, MFA) to add additional restrictions without hardcoding ARNs.
- Prefer customer managed policies over AWS managed policies for production workloads.
- Use IAM Access Analyzer and last accessed information to find and remove unused permissions.
- Separate dev and prod access using different roles in different accounts.
- Prefer IAM roles with temporary credentials over long-lived access keys.
- Build permission reviews into your deployment process, not as an afterthought.
Frequently asked questions
What does least privilege mean in AWS?
Least privilege means granting an IAM identity only the specific permissions it needs to perform its job. Nothing more. A Lambda function that reads from one DynamoDB table should have read access to that table only, not to all DynamoDB tables and not write access.
How is least privilege different from just using AWS managed policies?
AWS managed policies are pre-built by AWS and designed for broad use cases. They often include more permissions than any single workload needs. Least privilege requires writing customer managed policies scoped to the exact actions and resources your workload uses.
What AWS tools help me build least-privilege policies?
IAM Access Analyzer generates least-privilege policies from CloudTrail activity logs. IAM last accessed information shows which permissions a role actually uses. The IAM Policy Simulator lets you test policies before applying them. Together these tools help you write and validate tightly scoped policies.
Should developers have production access?
Most developers should not have standing production access. Use read-only roles for debugging, break-glass roles with MFA and audit logging for emergencies, and separate accounts for dev and prod through AWS Organizations.
What is the difference between roles, policies, and permissions in this context?
A role is an IAM identity that a service, user, or application can assume. A policy is a JSON document that defines what actions are allowed or denied on which resources. Permissions are the individual allow or deny rules inside a policy. You attach policies to roles to grant permissions.