AWS IAM Best Practices — 12 Production-Tested Security Rules (2026 Guide)
The 12 AWS IAM best practices in 2026, in one sentence each: enable MFA on root and lock it away, replace every IAM user with an IAM role, follow least privilege by scoping both Actions and Resources, use tags as the primary access control boundary, enforce MFA on destructive actions, run IAM Access Analyzer quarterly, define all IAM in Terraform, separate permissions by environment and ideally by AWS account, use permission boundaries to safely delegate IAM creation, use Service Control Policies as account-wide guardrails, rotate any remaining long-lived credentials on a schedule, and monitor IAM with CloudTrail plus GuardDuty. The rest of this guide is how to do each one with real policy examples you can copy into Terraform today.
According to AWS's own security research, over 80% of AWS breaches trace back to IAM misconfiguration — not zero-days, not novel attacks, just people leaving the door open. The root account with no MFA. The developer with AdministratorAccess. The access key committed to a public GitHub repo in 2019 that still works today. The S3 bucket policy with Principal: "*". Every one of these is preventable with patterns that take less than an hour to set up.
This guide is the complete IAM playbook I use when auditing AWS accounts. Every section has a real policy example you can paste into your Terraform today, and every rule solves a specific problem I have seen break production. It is long because IAM is the single most important security boundary in AWS, and cutting corners here is how most breaches happen.
Here is the mental model I want you to hold throughout this guide: IAM has two moving parts — identities (who is asking) and policies (what they are allowed to do). Every security rule below is about making one of those two parts smaller, more specific, and more traceable.
Federated Role] WHO -->|Workload| ROLE[IAM Role
Temporary Creds] WHO -->|3rd Party| USER[IAM User
last resort only] SSO --> POLICY[Identity-Based Policy] ROLE --> POLICY USER --> POLICY POLICY --> SCP[Service Control Policy
Account Guardrail] SCP --> BOUNDARY[Permission Boundary
Max Allowed] BOUNDARY --> RESOURCE[Resource-Based Policy] RESOURCE --> DECISION{Allowed?} DECISION -->|Yes| ALLOW[Action Permitted] DECISION -->|No| DENY[Action Denied] classDef stage fill:#6C3CE1,stroke:#00D4AA,stroke-width:2px,color:#fff; classDef guard fill:#4A1DB5,stroke:#00D4AA,stroke-width:2px,color:#fff; classDef good fill:#047857,stroke:#00D4AA,stroke-width:3px,color:#fff; classDef bad fill:#7f1d1d,stroke:#ef4444,stroke-width:2px,color:#fff; class U,WHO,POLICY,DECISION stage; class SCP,BOUNDARY,RESOURCE guard; class SSO,ROLE,ALLOW good; class USER,DENY bad;
Every AWS API call passes through this entire pipeline. A request is only allowed if every gate says "allowed." Understanding this evaluation order is the difference between writing IAM policies that work and writing IAM policies that you think work.
Rule 1: Lock Down the Root Account
The root account has unrestricted access to everything in your AWS account. It can delete any resource, change billing, close the account, and cannot be restricted by IAM policies.
Do this immediately:
- Enable MFA on the root account — use a hardware key or authenticator app, not SMS
- Do not create access keys for root — ever
- Use root only for tasks that require it (changing account settings, enabling certain services)
- Set up a strong, unique password and store it in a password manager
If your root account gets compromised and has no MFA, the attacker can spin up crypto miners across every region, rack up thousands of dollars in charges, and lock you out of your own account. This happens every week to someone on Reddit. Enable MFA in the next 5 minutes.
Rule 2: Use IAM Roles, Not IAM Users
IAM users have long-lived access keys. If those keys leak through a git commit, a log file, or a compromised machine, someone has permanent access to your AWS account until you notice and revoke them.
IAM roles provide temporary credentials that expire automatically. This is better in every way:
- EC2 instances — use instance profiles (IAM roles attached to the instance)
- ECS tasks — use task roles and execution roles
- Lambda functions — use execution roles
- CI/CD pipelines — use OIDC federation with GitHub Actions
- Human access — use AWS IAM Identity Center (SSO) with federated roles
The only valid reason to create an IAM user with access keys is for third-party services that specifically require them and do not support role assumption.
Rule 3: Write Least Privilege Policies
The most common mistake is using Action: "*" and Resource: "*". This gives the identity full access to everything. Here is what a proper least-privilege policy looks like:
Bad: Too Broad
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": "s3:*",
"Resource": "*"
}
]
}
This allows every S3 action on every bucket in the account. A developer with this policy could delete production backups.
Good: Scoped to Specific Buckets and Actions
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": [
"s3:GetObject",
"s3:PutObject",
"s3:ListBucket"
],
"Resource": [
"arn:aws:s3:::my-app-uploads",
"arn:aws:s3:::my-app-uploads/*"
]
}
]
}
This allows reading, writing, and listing objects only in the my-app-uploads bucket. Nothing else. If these credentials are compromised, the attacker can only access that one bucket.
Best: Add Conditions
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": [
"s3:GetObject",
"s3:PutObject"
],
"Resource": "arn:aws:s3:::my-app-uploads/*",
"Condition": {
"StringEquals": {
"s3:x-amz-server-side-encryption": "aws:kms"
}
}
}
]
}
This adds a condition: objects can only be uploaded if they are encrypted with KMS. Unencrypted uploads are denied. Conditions are the most powerful and most underused feature of IAM policies.
Rule 4: Use Tags for Access Control
Instead of listing every resource ARN in your policies, use tags to control access dynamically:
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": [
"ec2:StartInstances",
"ec2:StopInstances",
"ec2:RebootInstances"
],
"Resource": "*",
"Condition": {
"StringEquals": {
"ec2:ResourceTag/Environment": "dev"
}
}
}
]
}
This allows developers to start, stop, and reboot EC2 instances — but only if the instance is tagged Environment: dev. Production instances with Environment: prod are untouchable. This is how you give developers freedom in dev without risking production.
Rule 5: Enforce MFA for Sensitive Operations
Even for authenticated users, require MFA for destructive actions:
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Deny",
"Action": [
"ec2:TerminateInstances",
"rds:DeleteDBInstance",
"s3:DeleteBucket"
],
"Resource": "*",
"Condition": {
"BoolIfExists": {
"aws:MultiFactorAuthPresent": "false"
}
}
}
]
}
This denies deletion of EC2 instances, RDS databases, and S3 buckets unless the user has authenticated with MFA in the current session. Even if an access key is compromised, the attacker cannot delete anything without the MFA device.
Rule 6: Use IAM Access Analyzer
The hardest part of least privilege is knowing what permissions are actually needed. IAM Access Analyzer solves this:
- Enable Access Analyzer in the IAM console
- Run policy generation — it analyzes 90 days of CloudTrail logs and generates a policy based on actual API calls
- Review and apply — replace overly broad policies with the generated least-privilege version
For example, if a developer has AmazonEC2FullAccess but only uses DescribeInstances and DescribeSecurityGroups, Access Analyzer tells you exactly that. You create a custom policy with just those two actions.
Also check the "Last accessed" column in the IAM console for each service. If a permission has not been used in 90 days, it is probably safe to remove.
Rule 7: Define IAM in Terraform
IAM policies created through the console are hard to audit, hard to review, and impossible to track changes on. Define everything in Terraform:
resource "aws_iam_role" "app" {
name = "my-app-role"
assume_role_policy = jsonencode({
Version = "2012-10-17"
Statement = [{
Effect = "Allow"
Principal = { Service = "ecs-tasks.amazonaws.com" }
Action = "sts:AssumeRole"
}]
})
}
resource "aws_iam_role_policy" "app_s3" {
name = "s3-upload-access"
role = aws_iam_role.app.id
policy = jsonencode({
Version = "2012-10-17"
Statement = [{
Effect = "Allow"
Action = ["s3:GetObject", "s3:PutObject"]
Resource = "${aws_s3_bucket.uploads.arn}/*"
}]
})
}
Now every IAM change goes through code review in a pull request. Someone has to approve it before it reaches production. You have a complete git history of who added what permission and when.
Rule 8: Separate Permissions by Environment
Developers should have broader access in dev and almost no access in production:
- Dev — can create, modify, and delete most resources. Freedom to experiment.
- Staging — can deploy but not modify infrastructure directly. Tests CI/CD pipelines.
- Production — read-only for humans. Only CI/CD pipelines can make changes. Destructive actions require MFA and approval.
The best way to enforce this is separate AWS accounts per environment using AWS Organizations. An IAM mistake in the dev account cannot affect production because they are completely separate accounts.
Rule 9: Use Permission Boundaries for Safe Delegation
Permission boundaries are the most misunderstood IAM feature — and one of the most powerful. They solve a specific problem: how do you let developers create IAM roles for their own services without letting them accidentally grant themselves AdministratorAccess?
A permission boundary is a policy attached to an IAM entity that defines the maximum permissions that entity can have. The effective permissions are always the intersection of the identity policy AND the boundary. Even if someone attaches AdministratorAccess to a role, the boundary clips it back.
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": [
"s3:*",
"dynamodb:*",
"logs:*",
"cloudwatch:*"
],
"Resource": "*"
},
{
"Effect": "Deny",
"Action": [
"iam:*",
"organizations:*",
"account:*"
],
"Resource": "*"
}
]
}
Attach this as a permission boundary to developer-created roles. Even if a developer writes a Terraform module that grants iam:*, the role still cannot touch IAM because the boundary explicitly denies it. This is how you give developers self-service without letting them escalate to admin.
Rule 10: Use Service Control Policies (SCPs) as Account-Wide Guardrails
SCPs are organization-level policies that apply to every identity in an AWS account, including the root user. They define what actions are even possible in the account. Nothing — not an IAM policy, not a role, not a resource-based policy — can override an SCP deny.
Every AWS Organization should have these baseline SCPs applied to all production accounts:
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "DenyLeavingOrg",
"Effect": "Deny",
"Action": "organizations:LeaveOrganization",
"Resource": "*"
},
{
"Sid": "DenyDisablingCloudTrail",
"Effect": "Deny",
"Action": [
"cloudtrail:StopLogging",
"cloudtrail:DeleteTrail"
],
"Resource": "*"
},
{
"Sid": "DenyDisablingGuardDuty",
"Effect": "Deny",
"Action": [
"guardduty:DeleteDetector",
"guardduty:DisableOrganizationAdminAccount"
],
"Resource": "*"
},
{
"Sid": "DenyUnapprovedRegions",
"Effect": "Deny",
"NotAction": [
"iam:*",
"organizations:*",
"route53:*",
"cloudfront:*",
"waf:*",
"support:*"
],
"Resource": "*",
"Condition": {
"StringNotEquals": {
"aws:RequestedRegion": ["ap-south-1", "us-east-1"]
}
}
}
]
}
This single SCP prevents attackers from disabling audit logging, launching crypto miners in unused regions, or removing the account from your organization. These are the four most common post-compromise actions — blocking them at the SCP level means an attacker cannot do them even with stolen root credentials.
Rule 11: Rotate Credentials and Audit Stale Access
If you absolutely must use IAM users with access keys (some legacy third-party integrations still require them), rotate them on a schedule. AWS makes this easy but most teams never do it.
- Maximum key age: 90 days. After that, the key is automatically flagged in IAM credential reports.
- Rotation without downtime: create a new key, update the consuming service, verify, then delete the old key. IAM allows two active keys per user specifically for this.
- Automate the audit: use a Lambda function + EventBridge to generate a weekly IAM credential report and post it to Slack. Flag any keys older than 90 days and any users who have not logged in for 180 days.
- Delete dormant users: if an IAM user has not authenticated in 180 days, disable the login profile and delete the access keys. Six months of inactivity is not coming back.
Generate a credential report from the CLI to see what you have right now:
aws iam generate-credential-report
aws iam get-credential-report --query Content --output text | base64 -d > report.csv
Open the CSV. Any row where access_key_1_last_rotated or access_key_2_last_rotated is older than 90 days is a ticking time bomb. Fix or delete.
Rule 12: Monitor IAM with CloudTrail and GuardDuty
All the preventive rules above help you avoid mistakes. Monitoring catches the mistakes you missed — and catches active attacks.
CloudTrail logs every IAM API call in your account. Enable it, send logs to a dedicated security account (so a compromised prod account cannot delete its own audit trail), and set up alerts for these events:
CreateUser,CreateAccessKey,AttachUserPolicy— sudden new user creation is usually the first sign of a compromiseDeleteTrail,StopLogging,UpdateTrail— attacker trying to cover their tracksConsoleLoginfailures — brute force attempts- Any root account API call — root should basically never touch the API
GuardDuty is AWS's managed threat detection service. It costs a few dollars per month on small accounts and automatically flags things like:
- IAM credentials being used from unusual geographies (your developer is in Mumbai but suddenly logging in from Russia)
- Crypto miner traffic patterns from compromised EC2 instances
- Unauthorized IAM reconnaissance patterns (repeated
ListBucket,ListRoles,GetUsercalls in quick succession) - Known-malicious IP addresses making calls with your credentials
Enable GuardDuty in every region. Route the findings to SNS → email or Slack. This is the cheapest security insurance you will ever buy.
IAM Users vs IAM Roles vs Federated Identities — Which to Use When
The single decision that matters most in IAM design is which type of identity you give to each actor. Here is the decision table:
| Identity Type | Use For | Credential Lifetime | Recommended? |
|---|---|---|---|
| Federated SSO User (IAM Identity Center) |
All human access — developers, admins, auditors | Session-based (1-12 hours) | ✅ Yes — for all human access |
| IAM Role (EC2/ECS/Lambda) | Applications running on AWS compute | Auto-rotated every ~1 hour | ✅ Yes — default for workloads |
| IAM Role (OIDC Federation) | CI/CD pipelines — GitHub Actions, GitLab CI | Session-based (15 min - 12 hours) | ✅ Yes — for all CI/CD |
| IAM Role (Cross-Account) | Sharing access between AWS accounts | Session-based | ✅ Yes — for multi-account setups |
| IAM User | Third-party services that do not support role assumption | Indefinite until rotated | ⚠️ Last resort only |
| Root User | A handful of account-level settings (billing, closing account) | Indefinite | 🚫 Never for daily work |
The pattern is simple: if it is human, use SSO. If it is a workload on AWS, use an instance/task/execution role. If it is CI/CD, use OIDC federation. IAM users exist only for the small number of cases where none of the above is possible.
AWS IAM Roles Best Practices (Deep Dive)
Since roles are the answer 95% of the time, here are the specific patterns that separate good role design from bad:
1. One Role Per Workload, Not One Role Per Developer
Roles should map to what is being done, not who is doing it. A payments service has one role with payment-specific permissions. A feed service has its own. If a single role is used by five different services, you cannot give any of them least-privilege permissions — you have to union all their needs, which is the opposite of least privilege.
2. Trust Policies Are a Security Boundary
The trust policy on a role defines who can assume it. This is just as important as the permissions policy. Never use Principal: "*" in a trust policy — that means any identity in any AWS account in the world can assume your role. Always specify exactly which service or account is allowed:
{
"Version": "2012-10-17",
"Statement": [{
"Effect": "Allow",
"Principal": {
"Service": "ecs-tasks.amazonaws.com"
},
"Action": "sts:AssumeRole",
"Condition": {
"StringEquals": {
"aws:SourceAccount": "123456789012"
},
"ArnLike": {
"aws:SourceArn": "arn:aws:ecs:ap-south-1:123456789012:*"
}
}
}]
}
The SourceAccount and SourceArn conditions prevent the "confused deputy" problem — where a service you trust is tricked into assuming your role on behalf of someone else. Always include them on service roles.
3. Session Duration Matters
By default, assumed roles give you a 1-hour session. For interactive human access, that is usually too short. For automated pipelines, that is usually too long. Set it explicitly based on the use case: humans get 4-12 hours (force re-auth once per workday), CI/CD gets 15-60 minutes (just enough for one pipeline run).
4. Use Session Tags for Fine-Grained Audit Logs
When a role is assumed, you can attach session tags that get recorded in CloudTrail. For example, tag the session with AssumedBy=akshay@company.com so the audit log shows which human assumed which role, not just that "someone" did.
AWS IAM Policy Best Practices (Deep Dive)
Policies are the other half of IAM. Here is the complete set of rules I apply when writing any production policy:
1. Always Scope Both Action AND Resource
Action: "*" on Resource: "*" is the worst-case policy and the most common one. At minimum, scope the action to a service (s3:GetObject, not *). Ideally, scope the resource to a specific ARN (arn:aws:s3:::my-bucket/*, not *).
2. Use Conditions to Add Context
Conditions are the most underused feature in IAM. They let you express things like:
- "Only allow this action if the request came from inside our VPC" —
aws:SourceVpc - "Only allow this action if MFA was used" —
aws:MultiFactorAuthPresent - "Only allow deletion on resources tagged Environment=dev" —
ec2:ResourceTag/Environment - "Only allow uploads from our corporate IP range" —
aws:SourceIp - "Only allow actions during business hours" —
aws:CurrentTime
The best policies combine all of these. A developer can upload to S3, but only from the VPN, only with MFA, only with KMS encryption, only to buckets tagged for their team. That is real least privilege.
3. Explicit Deny Always Wins
IAM evaluation order: an explicit Deny always overrides an Allow, regardless of where it comes from (identity policy, resource policy, SCP, permission boundary). Use this to enforce safety rails:
{
"Version": "2012-10-17",
"Statement": [{
"Sid": "DenyUnencryptedS3Uploads",
"Effect": "Deny",
"Action": "s3:PutObject",
"Resource": "arn:aws:s3:::*/*",
"Condition": {
"StringNotEquals": {
"s3:x-amz-server-side-encryption": "aws:kms"
}
}
}]
}
This policy, attached at the organization level, makes it impossible to upload an unencrypted S3 object anywhere in your organization — regardless of what IAM permissions anyone has. That is the power of explicit deny.
4. Use NotAction Sparingly and Deliberately
NotAction is the opposite of Action — it means "every action except these." It is useful for policies like "allow everything except IAM" but dangerous because it implicitly allows any future AWS action AWS invents. Prefer explicit Action lists unless you have a specific reason not to.
5. Keep Policies Small and Composable
A single IAM policy has a 6,144 character limit. A role can have up to 10 managed policies attached. Rather than writing one giant monolithic policy, split by purpose: one policy for S3 access, one for DynamoDB, one for CloudWatch Logs. Each is easier to review, reuse, and audit. Attach multiple small policies to a role instead of one giant one.
6. Version Everything and Review Every Change in Pull Requests
Every IAM policy change should go through code review. No policy should be written directly in the console. Terraform or CloudFormation are the only acceptable ways to create IAM in production. If someone is changing who can access production data, another human should see that change before it ships.
The Complete IAM Audit Checklist for Production
Run through this checklist on every AWS account. Anything unchecked is a security risk ready to be exploited.
Root Account
- Hardware MFA enabled — not SMS, not email — a YubiKey or authenticator app
- No access keys exist for root — not disabled, not rotated, deleted
- Strong password stored in a password manager — 20+ characters, unique to this account
- Alerts on any root login — CloudTrail → EventBridge → SNS → your phone
Identities and Access
- Zero human IAM users with console access — everyone authenticates through IAM Identity Center / SSO
- Zero long-lived access keys on workloads — all compute uses instance profiles, task roles, or execution roles
- CI/CD uses OIDC federation, not stored access keys — see the GitHub Actions OIDC guide
- Cross-account access uses assume-role with ExternalId, never trust account wildcards
- Every remaining IAM user has MFA — enforced via IAM condition policy
Policies
- Zero uses of
Action: "*"withResource: "*"anywhere in the account - No attached
AdministratorAccessexcept on a single break-glass emergency role - Permission boundaries applied to any role developers can create themselves
- SCPs in place at the organization level blocking: CloudTrail disable, region lockdown, GuardDuty disable, org leave
- Destructive actions require MFA — terminate, delete, modify critical resources
- Explicit deny on unencrypted S3 uploads at the SCP level
Auditing and Monitoring
- IAM Access Analyzer enabled and reviewed quarterly — unused permissions removed
- All IAM defined in Terraform — no console creation allowed in production
- CloudTrail enabled in every region with logs shipped to a separate security account
- GuardDuty enabled in every region with findings piped to Slack/email
- Weekly credential report automated via Lambda + EventBridge, posted to Slack
- Keys rotated every 90 days for any IAM users that still exist
- Dormant users deleted after 180 days of no activity
If you can check all 20+ boxes, you are already in the top 10% of AWS accounts I have audited. Most accounts fail at least 6-8 of these.
Common Mistakes
- Giving developers AdministratorAccess "to move fast." This is the most common shortcut and the most dangerous. Start with PowerUserAccess (no IAM changes) and add specific permissions as needed.
- Using
Resource: "*"everywhere. This means the policy applies to every resource of that type in the account. Scope to specific ARNs or use tag-based conditions. - Not rotating access keys. If you must use IAM users, rotate keys every 90 days. Use
aws iam create-access-keyandaws iam delete-access-keyto rotate without downtime. - Ignoring cross-account access. When you share resources across accounts (S3 buckets, KMS keys), use resource-based policies with explicit account IDs. Never use wildcards for cross-account trust.
- Not using service control policies (SCPs). SCPs in AWS Organizations set guardrails that no IAM policy can override. Use them to prevent dangerous actions like disabling CloudTrail or leaving a region you do not use.
Frequently Asked Questions
What are the most important AWS IAM security best practices?
The five highest-impact AWS IAM security best practices are: (1) enable MFA on the root account and lock it away, (2) replace every IAM user with an IAM role for workloads and SSO for humans, (3) follow least privilege by scoping both the Action and the Resource in every policy, (4) enforce MFA on destructive actions through an explicit Deny, and (5) enable IAM Access Analyzer and review unused permissions quarterly. These five rules alone eliminate roughly 80% of real-world IAM breach patterns.
What is the principle of least privilege in AWS IAM?
Least privilege means granting only the minimum permissions required to perform a task — nothing more. Instead of giving a developer s3:* on all buckets, you give them s3:GetObject and s3:PutObject on the one specific bucket they work with. Instead of AdministratorAccess, you create a role-specific policy that allows exactly the actions needed and explicitly denies everything else. The practical goal is: if these credentials leak, how much damage can an attacker do? Least privilege is the answer to making that number small.
How do I implement IAM least privilege in practice?
Start with broader permissions, watch what is actually used, then shrink. AWS IAM Access Analyzer is designed for this — it reads 90 days of CloudTrail logs and generates a least-privilege policy based on actual API calls. The workflow is: start the role with a broader managed policy, let it run in dev or staging for a week, run Access Analyzer to see which permissions were actually used, and replace the broad policy with a scoped custom one. Repeat every quarter as the workload changes.
Should I use IAM users or IAM roles?
Use IAM roles wherever possible. Roles provide temporary credentials that expire automatically (usually within an hour), while IAM users have long-lived access keys that can leak. For human access, use AWS IAM Identity Center (SSO) with federated roles. For applications on AWS, use EC2 instance profiles, ECS task roles, or Lambda execution roles. For CI/CD, use OIDC federation with GitHub Actions. The only valid reason to create an IAM user is for third-party services that specifically require access keys and do not support role assumption.
What are the AWS IAM roles best practices?
Four rules for IAM role design: (1) one role per workload, not one role shared across services — sharing forces you to union permissions, which defeats least privilege. (2) Trust policies must specify exactly which principal can assume the role — never use wildcards. (3) Always use aws:SourceAccount and aws:SourceArn conditions on service trust policies to prevent the "confused deputy" problem. (4) Use session tags to record which human assumed a role, not just that "someone" did — this is critical for audit logs in shared environments.
What are the AWS IAM policy best practices?
Six rules for writing good IAM policies: (1) scope both Action AND Resource — never leave either as a wildcard. (2) Use Condition keys to add context like aws:MultiFactorAuthPresent, aws:SourceVpc, and aws:RequestedRegion. (3) Remember that explicit Deny always wins — use it to enforce safety rails like blocking unencrypted S3 uploads. (4) Prefer explicit Action lists over NotAction — NotAction implicitly allows future AWS actions. (5) Keep policies small and composable — multiple 1KB policies are easier to review than one 6KB policy. (6) Define every policy in Terraform and review changes via pull request. Never create IAM policies in the console for production.
How do I find unused IAM permissions?
Use IAM Access Analyzer's policy generation feature. It reads 90 days of CloudTrail logs and shows you exactly which permissions the identity actually used versus what it was granted. You can generate a least-privilege policy from real usage data with one click. Also check the "Last accessed" column in the IAM console for each service on each role — if a permission has not been used in 90 days, it is almost always safe to remove. Run this audit quarterly.
Should I use AWS managed policies or custom policies?
Managed policies are great for getting started quickly, but they are almost always too broad for production. A managed policy like AmazonS3FullAccess grants access to every S3 bucket in the account, including ones that do not exist yet. Custom policies let you restrict by resource ARN, condition keys, and tags. The best approach is: start with a managed policy in dev, use Access Analyzer to capture the actual usage pattern, then replace the managed policy with a custom least-privilege one for production.
What is the difference between identity-based and resource-based policies?
Identity-based policies are attached to IAM users, groups, or roles — they define what the identity can do. Resource-based policies are attached to AWS resources like S3 buckets, SQS queues, KMS keys, or Lambda functions — they define who can access the resource. Both are evaluated together when someone makes a request. For cross-account access, resource-based policies are often simpler because you can grant access to another account's role directly on the resource, without needing to set up assume-role flows.
What is a permission boundary and when should I use it?
A permission boundary is a policy attached to an IAM entity that defines the maximum permissions that entity can have. The effective permissions are always the intersection of the entity's identity policy AND the permission boundary. Use permission boundaries when you want to let developers create IAM roles for their own services but prevent them from accidentally granting those roles more power than they should have. It is the safe way to delegate IAM creation without opening the door to privilege escalation.
What is the difference between SCPs and IAM policies?
Service Control Policies (SCPs) are organization-level policies that apply to every identity in an AWS account, including the root user. They define what actions are possible in the account — nothing can override an SCP deny. IAM policies are per-identity and define what a specific user or role is allowed to do. Think of SCPs as guardrails at the account level and IAM policies as permissions at the identity level. Both are needed — SCPs prevent catastrophic mistakes, IAM policies grant day-to-day permissions.
How often should I rotate AWS access keys?
Every 90 days at most. But the better answer is: do not use long-lived access keys at all. Use IAM roles, OIDC federation, and IAM Identity Center so there are no long-lived keys to rotate in the first place. If you have third-party integrations that require access keys, AWS allows two active keys per user so you can rotate without downtime — create the new key, update the service, verify it works, then delete the old key. Automate the 90-day rotation with a Lambda function and EventBridge schedule.
📖 Want all 30 interview scenarios in one PDF?
The DevOps Interview Playbook — 30 real-world scenarios with production-tested answers, mental models, exact commands, and salary data.
Get the Playbook — $15Put It Into Practice
IAM is the foundation of every secure AWS deployment. Get it right, and every VPC, database, and container service you build inherits that security posture.
Start with the checklist above. Enable MFA on root today. Replace one IAM user with a role this week. Run Access Analyzer and remove unused permissions this month. Security is not a one-time project — it is a habit.