AI & assistant-friendly summary

This section provides structured content for AI assistants and search engines. You can cite or summarize it when referencing this page.

Summary

A practical guide to AWS IAM — least privilege policies, IAM roles vs users, permission boundaries, SCPs, identity federation, and the access control patterns that secure production workloads without slowing teams down.

Entity Definitions

IAM
IAM is an AWS service discussed in this article.

AWS IAM Best Practices: Least Privilege Access Control

Security & Compliance 8 min read

Quick summary: A practical guide to AWS IAM — least privilege policies, IAM roles vs users, permission boundaries, SCPs, identity federation, and the access control patterns that secure production workloads without slowing teams down.

AWS IAM Best Practices: Least Privilege Access Control
Table of Contents

IAM is the security foundation of every AWS account. Every API call — whether from a user, application, or AWS service — is authenticated and authorized through IAM. A misconfigured IAM policy is the most common root cause of AWS security incidents: overly permissive policies grant access to resources they should not, and overly restrictive policies block legitimate operations and push teams toward workarounds that are even less secure.

The challenge is finding the balance — policies that are tight enough to prevent unauthorized access but flexible enough to let teams operate without friction. This guide covers the IAM patterns that achieve both in production environments.

Core Principles

Least Privilege

Grant only the permissions required to perform a specific task — nothing more:

Too broad:   "Action": "*", "Resource": "*"
Too narrow:  "Action": "s3:GetObject", "Resource": "arn:aws:s3:::prod-bucket/reports/2024/q1/report.csv"
Right:       "Action": "s3:GetObject", "Resource": "arn:aws:s3:::prod-bucket/reports/*"

Least privilege is not “deny everything and add permissions one at a time.” It is scoping permissions to the specific actions and resources a workload needs, with enough flexibility for normal operations.

Start with AWS managed policies for common use cases, then replace them with custom policies that remove unnecessary permissions as you understand the workload’s actual access patterns.

IAM Roles Over IAM Users

IAM users have long-lived credentials (access keys) that can be leaked, shared, or forgotten. IAM roles provide temporary credentials that expire automatically:

FeatureIAM UsersIAM Roles
CredentialsLong-lived access keysTemporary (15 min - 12 hours)
RotationManual (must rotate keys)Automatic (new credentials each session)
Sharing riskKeys can be copied, emailed, committedCannot be shared (assumed per session)
AuditingTrack key usageTrack role assumption + actions

Rules:

  • EC2 instances: Use instance profiles (IAM roles attached to instances) — never store access keys on instances
  • ECS tasks: Use task roles — each container gets its own scoped credentials
  • Lambda functions: Use execution roles — automatically assumed per invocation
  • Cross-account access: Use role assumption — never share access keys between accounts
  • Human users: Use identity federation (SSO) — not IAM users

The only legitimate use for IAM users with access keys is CI/CD pipelines in external systems that cannot assume roles (and even these should use OIDC federation when possible).

Explicit Deny Wins

IAM evaluation order: explicit deny > explicit allow > implicit deny.

An explicit deny in any policy overrides all allow statements. Use this for guardrails:

{
  "Effect": "Deny",
  "Action": ["ec2:RunInstances"],
  "Resource": "*",
  "Condition": {
    "StringNotEquals": {
      "ec2:InstanceType": ["t3.micro", "t3.small", "t3.medium", "m5.large"]
    }
  }
}

This denies launching any instance type not on the approved list — regardless of what other policies allow. Deny statements are the foundation of preventive controls.

Policy Design

Policy Structure

Every IAM policy follows the same structure:

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Sid": "AllowS3ReadAccess",
      "Effect": "Allow",
      "Action": ["s3:GetObject", "s3:ListBucket"],
      "Resource": ["arn:aws:s3:::my-app-data", "arn:aws:s3:::my-app-data/*"],
      "Condition": {
        "StringEquals": {
          "aws:RequestedRegion": "us-east-1"
        }
      }
    }
  ]
}

Best practices:

  • Always include a Sid (Statement ID) for readability and debugging
  • Specify exact actions — never use "Action": "*" in production
  • Scope Resource to specific ARNs — avoid "Resource": "*" whenever possible
  • Use conditions to further restrict (Region, source IP, time of day, MFA)

Common Policy Patterns

Application role (ECS task or Lambda):

{
  "Statement": [
    {
      "Sid": "ReadAppConfig",
      "Effect": "Allow",
      "Action": ["ssm:GetParameter", "ssm:GetParametersByPath"],
      "Resource": "arn:aws:ssm:us-east-1:123456789:parameter/myapp/prod/*"
    },
    {
      "Sid": "ReadSecrets",
      "Effect": "Allow",
      "Action": ["secretsmanager:GetSecretValue"],
      "Resource": "arn:aws:secretsmanager:us-east-1:123456789:secret:myapp/prod/*"
    },
    {
      "Sid": "ReadWriteDynamoDB",
      "Effect": "Allow",
      "Action": ["dynamodb:GetItem", "dynamodb:PutItem", "dynamodb:Query", "dynamodb:UpdateItem"],
      "Resource": "arn:aws:dynamodb:us-east-1:123456789:table/myapp-orders*"
    },
    {
      "Sid": "PublishEvents",
      "Effect": "Allow",
      "Action": ["events:PutEvents"],
      "Resource": "arn:aws:events:us-east-1:123456789:event-bus/myapp-bus"
    }
  ]
}

This role has access to exactly what the application needs: its own configuration, its own secrets, its own DynamoDB table, and its own event bus. Nothing else.

Developer role:

{
  "Statement": [
    {
      "Sid": "ReadOnlyProduction",
      "Effect": "Allow",
      "Action": [
        "ec2:Describe*",
        "ecs:Describe*",
        "ecs:List*",
        "logs:GetLogEvents",
        "logs:FilterLogEvents",
        "cloudwatch:GetMetricData",
        "cloudwatch:DescribeAlarms"
      ],
      "Resource": "*",
      "Condition": {
        "StringEquals": { "aws:ResourceTag/Environment": "production" }
      }
    },
    {
      "Sid": "FullAccessDev",
      "Effect": "Allow",
      "Action": ["ec2:*", "ecs:*", "lambda:*", "s3:*", "dynamodb:*"],
      "Resource": "*",
      "Condition": {
        "StringEquals": { "aws:ResourceTag/Environment": "development" }
      }
    }
  ]
}

Read-only in production, full access in development — enforced by resource tags.

Condition Keys

Conditions add contextual restrictions to policies:

Condition KeyUse Case
aws:RequestedRegionRestrict to approved Regions
aws:PrincipalOrgIDAllow access only from your Organization
aws:ResourceTag/EnvironmentScope by environment tag
aws:MultiFactorAuthPresentRequire MFA for sensitive actions
aws:SourceIpRestrict to corporate IP ranges
ec2:InstanceTypeLimit instance types
s3:prefixRestrict S3 access to specific prefixes

MFA for sensitive operations:

{
  "Sid": "DenyWithoutMFA",
  "Effect": "Deny",
  "Action": ["iam:*", "organizations:*", "account:*"],
  "Resource": "*",
  "Condition": {
    "BoolIfExists": { "aws:MultiFactorAuthPresent": "false" }
  }
}

This denies all IAM, Organizations, and Account actions unless MFA is present — even if other policies allow them.

Permission Boundaries

Permission boundaries define the maximum permissions an IAM entity can have. Even if a policy grants "Action": "*", the permission boundary limits what is actually permitted:

Effective permissions = Identity policy ∩ Permission boundary

Use case: Allow developers to create IAM roles for their applications without granting them full IAM admin:

{
  "Sid": "AllowCreateRolesWithBoundary",
  "Effect": "Allow",
  "Action": ["iam:CreateRole", "iam:AttachRolePolicy", "iam:PutRolePolicy"],
  "Resource": "arn:aws:iam::123456789:role/app-*",
  "Condition": {
    "StringEquals": {
      "iam:PermissionsBoundary": "arn:aws:iam::123456789:policy/AppPermissionBoundary"
    }
  }
}

Developers can create roles prefixed with app- but only if those roles have the permission boundary attached. The boundary prevents those roles from escalating privileges beyond what is allowed.

Service Control Policies

In a multi-account organization, Service Control Policies (SCPs) define guardrails across all accounts:

SCP (Organization level) → Permission Boundary (Account level) → IAM Policy (Role level)
  Effective permissions = SCP ∩ Permission Boundary ∩ IAM Policy

Essential SCPs

Prevent disabling CloudTrail:

{
  "Effect": "Deny",
  "Action": ["cloudtrail:StopLogging", "cloudtrail:DeleteTrail"],
  "Resource": "*"
}

Restrict to approved Regions:

{
  "Effect": "Deny",
  "NotAction": ["iam:*", "sts:*", "organizations:*", "support:*"],
  "Resource": "*",
  "Condition": {
    "StringNotEquals": {
      "aws:RequestedRegion": ["us-east-1", "us-west-2"]
    }
  }
}

Prevent leaving the Organization:

{
  "Effect": "Deny",
  "Action": ["organizations:LeaveOrganization"],
  "Resource": "*"
}

SCPs do not grant permissions — they only restrict them. An SCP that denies an action overrides any IAM policy in any account under that OU.

Identity Federation

AWS IAM Identity Center (SSO)

For human access, use IAM Identity Center instead of IAM users:

Corporate IdP (Okta, Azure AD, Google Workspace)
  → AWS IAM Identity Center
    → Permission Sets (mapped to IAM policies)
      → AWS Accounts (assigned to users/groups)

Benefits:

  • Single sign-on across all AWS accounts
  • Centralized user management in your existing identity provider
  • No long-lived credentials to manage or rotate
  • Automatic deprovisioning when users leave the organization

OIDC Federation for CI/CD

GitHub Actions, GitLab CI, and other CI/CD systems support OIDC federation — assuming IAM roles without access keys:

GitHub Actions workflow
  → Requests OIDC token from GitHub
    → Assumes IAM role via STS with OIDC token
      → Receives temporary credentials (1 hour)

IAM role trust policy for GitHub Actions:

{
  "Effect": "Allow",
  "Principal": { "Federated": "arn:aws:iam::123456789:oidc-provider/token.actions.githubusercontent.com" },
  "Action": "sts:AssumeRoleWithWebIdentity",
  "Condition": {
    "StringEquals": {
      "token.actions.githubusercontent.com:aud": "sts.amazonaws.com"
    },
    "StringLike": {
      "token.actions.githubusercontent.com:sub": "repo:my-org/my-repo:ref:refs/heads/main"
    }
  }
}

This restricts role assumption to a specific GitHub repository and branch. No access keys stored in GitHub secrets.

Access Analysis

IAM Access Analyzer

Access Analyzer identifies resources shared with external entities and policies that grant more access than needed:

  • External access findings — S3 buckets, KMS keys, SQS queues, IAM roles accessible from outside your account or organization
  • Unused access findings — Roles, users, and policies with permissions that have not been used
  • Policy validation — Checks policies for errors, security warnings, and best practices
  • Policy generation — Generates least-privilege policies based on actual CloudTrail activity

Policy generation workflow:

1. Attach broad policy to role (e.g., AmazonS3FullAccess)
2. Run workload for 30-90 days (accumulate CloudTrail data)
3. Use Access Analyzer to generate policy based on actual API calls
4. Replace broad policy with generated least-privilege policy

This is the most reliable way to achieve least privilege — let the workload tell you what it needs.

CloudTrail Analysis

CloudTrail logs every API call. Use it to identify:

  • Unused permissions — Actions in the policy that never appear in CloudTrail
  • Access denied events — Missing permissions that need to be added
  • Unusual access patterns — New principals accessing resources, bulk operations, off-hours activity

Set CloudWatch alarms for critical IAM events:

EventAlert Condition
Root user loginAny ConsoleLogin by root
Access key creationCreateAccessKey for any user
Policy changesPutUserPolicy, AttachRolePolicy, CreatePolicy
Role trust policy changesUpdateAssumeRolePolicy
Failed authenticationConsoleLogin with errorMessage

Common Mistakes

Mistake 1: Wildcard Resources

Using "Resource": "*" when specific ARNs are available. Most AWS actions can be scoped to specific resources. Use wildcards only for actions that genuinely require it (like ec2:DescribeInstances which cannot be resource-scoped).

Mistake 2: Long-Lived Access Keys

Creating IAM users with access keys for applications running on AWS. EC2, ECS, Lambda, and all AWS compute services support IAM roles with temporary credentials. Access keys should be the exception, not the default.

Mistake 3: Shared Credentials

Multiple team members sharing one IAM user’s credentials. This eliminates accountability — CloudTrail shows the user but not which person performed the action. Use IAM Identity Center for individual, traceable access.

Mistake 4: No MFA on Privileged Access

Allowing iam:*, organizations:*, or s3:DeleteBucket without requiring MFA. Sensitive operations should require MFA — explicit deny conditions enforce this regardless of other policies.

Mistake 5: Stale Permissions

Roles and policies created for a project that ended months ago still have active permissions. Use IAM Access Analyzer’s unused access findings and credential reports to identify and remove stale access regularly.

Security Checklist

  • No IAM users with access keys for AWS-hosted workloads (use roles)
  • Permission boundaries on all roles that can create other roles
  • SCPs preventing CloudTrail/GuardDuty/Config disablement
  • Region restriction SCP in place
  • MFA required for all human access
  • Root user access keys deleted, MFA enabled, usage alarmed
  • IAM Access Analyzer enabled in all accounts
  • Credential report reviewed quarterly (stale users/keys)
  • CloudTrail alerts for critical IAM events

Getting Started

IAM is the most critical security control in AWS. Combined with GuardDuty for threat detection, CloudWatch for monitoring, and SCPs for organizational guardrails, it provides the access control layer that production workloads require.

For IAM architecture design, security assessments, and multi-account access control, talk to our team.

Contact us to secure your AWS access controls →

Ready to discuss your AWS strategy?

Our certified architects can help you implement these solutions.

Recommended Reading

Explore All Articles »