SECURITY

AWS IAM Best Practices — Least Privilege Policies That Actually Work

By Akshay Ghalme·March 31, 2026·12 min read

AWS IAM best practices in 2026: never use the root account, enforce MFA on all human users, use IAM roles instead of IAM users with access keys, follow least privilege by granting only the minimum permissions needed for each task, use IAM Access Analyzer to find and remove unused permissions, and define all IAM resources in Terraform for auditability.

Most AWS accounts I have audited have the same problems. The root account has no MFA. Developers have AdministratorAccess. There are IAM users with access keys that have not been rotated in two years. Service accounts have * permissions on everything.

The theory of least privilege is simple — give people and services only the permissions they need. The practice is harder because figuring out exactly what someone needs requires effort. This guide covers the patterns that work in production, with real policy examples you can use today.

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:

  1. Enable MFA on the root account — use a hardware key or authenticator app, not SMS
  2. Do not create access keys for root — ever
  3. Use root only for tasks that require it (changing account settings, enabling certain services)
  4. 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:

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:

  1. Enable Access Analyzer in the IAM console
  2. Run policy generation — it analyzes 90 days of CloudTrail logs and generates a policy based on actual API calls
  3. 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.

IAM Checklist for Production

  1. Root account — MFA enabled, no access keys, password in secure storage
  2. No IAM users with console access — use IAM Identity Center (SSO)
  3. No long-lived access keys — use IAM roles and OIDC federation
  4. All policies scoped to specific resources — no Resource: "*" unless absolutely necessary
  5. MFA required for destructive actions — terminate, delete, modify critical resources
  6. IAM Access Analyzer enabled — reviewing unused permissions quarterly
  7. All IAM resources in Terraform — code-reviewed and version-controlled
  8. CloudTrail enabled — logging all API calls for audit
  9. No AdministratorAccess on any user or role except break-glass emergency access
  10. Access keys rotated every 90 days — for any IAM users that still exist

Common Mistakes

  1. 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.
  2. 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.
  3. Not rotating access keys. If you must use IAM users, rotate keys every 90 days. Use aws iam create-access-key and aws iam delete-access-key to rotate without downtime.
  4. 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.
  5. 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 is the principle of least privilege in AWS IAM?

Grant only the minimum permissions required for a task. Instead of s3:* on all buckets, give s3:GetObject on the specific bucket needed. This limits damage if credentials are compromised.

Should I use IAM users or IAM roles?

IAM roles wherever possible. Roles provide temporary credentials that expire automatically. For human access use SSO, for applications use instance profiles or task roles, for CI/CD use OIDC federation.

How do I find unused IAM permissions?

Use IAM Access Analyzer. It analyzes CloudTrail logs to show which permissions are actually used versus granted. Generate a least-privilege policy from real activity data. Also check the "Last accessed" column in the IAM console.

Should I use AWS managed policies or custom policies?

Start with managed policies for development, but create custom policies for production. Managed policies like AmazonS3FullAccess grant access to ALL buckets. Custom policies let you restrict to specific resources, conditions, and tags.

What is the difference between identity-based and resource-based policies?

Identity-based policies define what a user or role can do. Resource-based policies define who can access a resource. Both are evaluated together. For cross-account access, resource-based policies are often simpler.


Put 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.

AG

Akshay Ghalme

AWS DevOps Engineer with 3+ years building production cloud infrastructure. AWS Certified Solutions Architect. Currently managing a multi-tenant SaaS platform serving 1000+ customers.

More Guides & Terraform Modules

Every guide comes with a matching open-source Terraform module you can deploy right away.