Do you have an AWS account with dozens of users, roles, and policies, and you've lost track of who has access to what? You're not alone. We see it every day when we're called to clean up AWS environments that grew without governance. The result? An S3:DeleteObject permission on an invoice bucket, a policy with Effect:Allow, Action:* on a dev account, and the feeling that a breach is just a matter of time.
We at Meteora Web also come from accounting: balance sheets, double-entry bookkeeping, VAT. That's why we think about the numbers. An overprivileged account is not just a security risk — it's a hidden cost. More permissions mean more attack surface, more audit logs to analyze, more chances for human error. And when a breach happens, remediation costs are exponential.
In this advanced guide we'll apply the principle of least privilege on AWS, managing policies granularly, with real examples and copy‑paste code. We start from theory and end with a command you can run today.
Why Least Privilege Is Not an Optional
Imagine giving every employee the keys to all warehouses, the cash register, and the customer data server. Sounds absurd, but on AWS it's the most common configuration. The principle of least privilege says: every user, role, or service should have only the permissions strictly necessary to do its job, and nothing more.
It's not just security. It's economics: fewer permissions = fewer logs to filter, fewer incidents, less time investigating false positives. When a customer tells us "I have an access that's no longer needed, but I don't know who created it", we immediately understand the problem. That's why AWS policies should be treated like contracts: clear, signed, and revocable.
Deny by Default Approach
AWS starts from an implicit principle: everything is denied until explicitly allowed. It sounds trivial, but most incidents come from policies that grant an overly broad Allow, maybe with Action: * on a critical resource. The golden rule: write policies as if your account will be breached tomorrow.
Concrete example: a policy for an EC2 instance that only needs to read from an S3 bucket.
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": [
"s3:GetObject",
"s3:ListBucket"
],
"Resource": [
"arn:aws:s3:::my-bucket-data",
"arn:aws:s3:::my-bucket-data/*"
]
}
]
}No s3:PutObject, no s3:DeleteObject. If the EC2 is compromised, the attacker cannot delete the bucket. Seems obvious? Yet in reviews of customer accounts, we've seen policies granting s3:* on Resource: "*".
AWS Policies: Types, Structure, and Best Practices
Policies in AWS are divided into identity‑based (attached to users, groups, roles) and resource‑based (attached to resources like S3 buckets or CodeBuild projects). Both can be used for least privilege, but with attention to interactions.
Identity‑Based Policies: the Workhorse
These are the most common. Always write them with specific actions, never iam:* except during initial bootstrap. For each role, ask: "What is the minimum list of APIs this role needs to call?"
We often use conditional policies to further limit. For example, allow access to a bucket only if the request comes from a specific VPC:
{
"Effect": "Allow",
"Action": "s3:GetObject",
"Resource": "arn:aws:s3:::my-bucket/*",
"Condition": {
"StringEquals": {
"aws:SourceVpc": "vpc-12345678"
}
}
}This blocks access from the internet even if credentials are stolen. One of the few times an Allow remains secure because it's conditional.
Resource‑Based Policies: When the Resource Decides
S3 buckets, Lambda functions, SQS queues have their own policies. Least privilege applies here too: never give Principal:"*" without a strong condition. Example for a public bucket hosting a static landing page:
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Principal": "*",
"Action": "s3:GetObject",
"Resource": "arn:aws:s3:::my-site-public/*",
"Condition": {
"IpAddress": {
"aws:SourceIp": ["203.0.113.0/24", "198.51.100.0/24"]
}
}
}
]
}Note: Principal:"*" is acceptable here only because it's limited to trusted IPs and read‑only action. Never use it on critical resources.
Policy Management: Organization and Versioning
As you grow, policies become a tangle. We at Meteora Web have seen accounts with over 200 customer managed policies, many unused. Policies should be curated like code: versioned, commented, tested.
Policy Manager: Tools and Commands
Use the AWS CLI to list and analyze policies. Basic command to see policies attached to a user:
aws iam list-attached-user-policies --user-name john.doeTo find policies that are never used, combine with last-accessed. A command we run often during audits:
aws iam generate-service-last-accessed-details --arn arn:aws:iam::123456789012:policy/MyUselessPolicy
aws iam get-service-last-accessed-details --job-id If a policy has no recent accesses, it probably isn't needed. Consider removing or tightening it.
Naming Convention and Tagging
Adopt a standard: [env]-[service]-[action]-[resource], e.g., prod-s3-get-mybucket. Use AWS tags to mark policies with owner, creation date, severity. This facilitates periodic reviews.
IAM Roles vs Users: When to Use What
One of the most common mistakes: creating human IAM users. For people, always use roles with federation (SAML, SSO). For services, use roles with automatic assumption. IAM users with long‑term keys are time bombs. Keys expire, get forgotten, end up in GitHub repositories. We've seen it dozens of times.
Example: an EC2 that needs to access DynamoDB. Create an IAM role with the necessary policy, attach it to the instance profile. No secret key to manage.
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": [
"dynamodb:GetItem",
"dynamodb:Query"
],
"Resource": "arn:aws:dynamodb:eu-west-1:123456789012:table/Orders"
}
]
}No *, only read on a specific table. The instance cannot PutItem or DeleteTable — perfect least privilege.
Policy Boundaries: An Extra Turn of the Screw
Policy boundaries are the next level. They limit the maximum permissions a role can have, even if the attached policies grant more. Useful for delegating role creation to different teams without losing control.
Example: a boundary that denies any action on production S3 resources, even if the team creates a policy granting s3:*. The boundary prevails.
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Deny",
"Action": "s3:*",
"Resource": "arn:aws:s3:::production-*"
}
]
}Attach this boundary to the development team's role. Even if they make a mistake, they cannot touch production data.
Audit and Monitoring: Trust but Verify
Applying least privilege is an ongoing process. Use AWS Config to detect non‑compliant policies, CloudTrail to track actions, and IAM Access Analyzer to find overly permissive policies. Perform quarterly reviews. We include them in client maintenance contracts: one hour per month to analyze and clean policies.
A practical tool: AWS IAM Access Analyzer automatically identifies policies that grant access to external entities (outside your account). Enable it and configure notifications for every new finding.
In Summary — What to Do Now
Don't wait for an incident. Execute these steps today:
- Audit current policies: use
aws iam list-policies --scope Localand check how many haveAction: "*". For each, ask: is it really necessary? - Remove or restrict wildcard policies. Replace with the list of actions actually needed. Use the official AWS actions list to find specific ones.
- Migrate from IAM users to federated roles. If you have human users with keys, move to SSO or IAM Identity Center within 30 days.
- Implement policy boundaries for every role created by other teams. Create a customer managed boundary policy with a Deny on critical resources (production, sensitive data).
- Enable IAM Access Analyzer and AWS Config. Configure rules to detect non‑compliant policies and receive notifications via SNS.
Security on AWS isn't a product you buy — it's a daily practice. We at Meteora Web live it every day with our clients. If you want an in‑depth check of your account, contact us. In the meantime, start with a policy review. Your future self (and your budget) will thank you.
Sponsored Protocol