AWS IAM Roles and Policies: Complete Security Guide (2026)

AWS IAM (Identity and Access Management) is the security foundation of every AWS deployment. Misconfigured IAM is the root cause of most AWS security incidents — overly permissive roles, hardcoded access keys, and confused deputy vulnerabilities. This guide walks through the IAM model from first principles: identity types, policy evaluation logic, role assumption, and how to enforce least privilege at scale using permission boundaries and SCPs.

Users, Roles, and Groups

IAM has three identity primitives:

  • IAM Users: Long-term identities with username/password and optional access keys. Use for human operators who need AWS console access, or legacy automation that can't use roles. Best practice: use SSO (AWS IAM Identity Center) for humans instead of IAM users.
  • IAM Roles: Temporary identities assumed by services, applications, or users. No long-term credentials — roles issue short-lived tokens via STS. This is what EC2 instances, Lambda functions, and ECS tasks use.
  • IAM Groups: Collections of IAM users that share policies. Groups can't assume roles and can't be referenced in resource policies. Use groups to manage permissions for human users.
Pro Tip: For new AWS accounts, set up AWS IAM Identity Center (formerly SSO) from day one. It integrates with your identity provider (Okta, Azure AD, Google Workspace), enforces MFA, and issues temporary credentials scoped to a permission set. This eliminates the need for long-term IAM user access keys entirely.
IdentityCredential TypeDurationBest Used For
IAM UserPassword + Access KeyPermanentBreak-glass accounts, legacy automation
IAM RoleSTS temporary token15min – 12hrEC2, Lambda, ECS, cross-account
IAM Identity CenterShort-lived credentialsConfigurableHuman developers, ops teams

Policy Types Explained

AWS has six policy types, and understanding which ones apply where is essential for debugging access issues:

  • Identity-based policies: Attached to users, groups, or roles. Define what the principal can do. Most common type.
  • Resource-based policies: Attached to resources (S3 buckets, SQS queues, Lambda functions). Define who can access the resource. Enable cross-account access without assuming a role.
  • Permission Boundaries: IAM managed policies that set the maximum permissions an identity-based policy can grant. Used to delegate IAM management to teams without giving them full admin.
  • Organizations SCPs (Service Control Policies): Applied to accounts or OUs in AWS Organizations. Define the maximum permissions for any principal in the account — even the root user.
  • Session policies: Passed inline when assuming a role via STS. Further restricts permissions for that specific session.
  • ACLs: Legacy, used only in S3 and VPC. Avoid for new designs.
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Sid": "AllowS3ReadForAppBucket",
      "Effect": "Allow",
      "Action": [
        "s3:GetObject",
        "s3:ListBucket"
      ],
      "Resource": [
        "arn:aws:s3:::my-app-bucket",
        "arn:aws:s3:::my-app-bucket/*"
      ]
    },
    {
      "Sid": "AllowSSMParameterRead",
      "Effect": "Allow",
      "Action": [
        "ssm:GetParameter",
        "ssm:GetParameters",
        "ssm:GetParametersByPath"
      ],
      "Resource": "arn:aws:ssm:us-east-1:123456789:parameter/myapp/*"
    },
    {
      "Sid": "AllowCloudWatchLogs",
      "Effect": "Allow",
      "Action": [
        "logs:CreateLogGroup",
        "logs:CreateLogStream",
        "logs:PutLogEvents"
      ],
      "Resource": "arn:aws:logs:us-east-1:123456789:log-group:/myapp/*"
    }
  ]
}

Policy Evaluation Logic

When AWS evaluates whether to allow or deny a request, it follows a specific order. Getting this wrong is the source of most "why is my request denied even though I have Allow?" questions.

  1. Explicit Deny — If any policy has an explicit Deny for this action, it's denied. Full stop. No Allow overrides an explicit Deny (except for the root user in some cases).
  2. SCPs — If the account is in an Organization and the SCP doesn't allow the action, it's denied.
  3. Resource-based policy — If there's a resource policy that allows the action from this principal, it may be allowed (depends on account context).
  4. Permission Boundary — If a permission boundary is attached and it doesn't allow the action, it's denied.
  5. Session policy — If a session policy was passed and it doesn't allow the action, it's denied.
  6. Identity-based policy — If the attached policies allow the action, it's allowed.
  7. Default Deny — If no policy explicitly allows the action, it's denied.
Warning: For cross-account access, both the identity-based policy on the role AND the resource-based policy (if present) must allow the action. An Allow in the resource policy alone is sufficient for same-account access, but not for cross-account.

Least Privilege in Practice

Least privilege means granting exactly the permissions needed — no more. In practice, this means:

# Step 1: Use IAM Access Advisor to see what services a role actually uses
aws iam generate-service-last-accessed-details \
  --arn arn:aws:iam::123456789:role/my-app-role

# Wait for report, then retrieve
aws iam get-service-last-accessed-details \
  --job-id [returned-job-id]

# Step 2: Use IAM policy simulator to test before applying
aws iam simulate-principal-policy \
  --policy-source-arn arn:aws:iam::123456789:role/my-app-role \
  --action-names s3:GetObject \
  --resource-arns arn:aws:s3:::my-bucket/file.txt

Permission Boundaries let you safely delegate IAM management. For example, you can allow a developer to create roles, but those roles can never exceed the boundary policy you've defined. Without boundaries, any developer with iam:CreateRole and iam:AttachRolePolicy can create an admin role and escalate privileges.

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Sid": "AllowIAMWithBoundary",
      "Effect": "Allow",
      "Action": ["iam:CreateRole", "iam:AttachRolePolicy"],
      "Resource": "*",
      "Condition": {
        "StringEquals": {
          "iam:PermissionsBoundary": "arn:aws:iam::123456789:policy/DeveloperBoundary"
        }
      }
    }
  ]
}

STS AssumeRole and Cross-Account Access

Roles are assumed via AWS STS (Security Token Service). The caller provides the role ARN and receives temporary credentials (access key, secret key, session token) valid for 15 minutes to 12 hours. This is how EC2 instances, Lambda functions, CI/CD pipelines, and cross-account access all work.

import boto3

# Assume a role in another account
sts_client = boto3.client('sts')

response = sts_client.assume_role(
    RoleArn='arn:aws:iam::999888777:role/DeploymentRole',
    RoleSessionName='cicd-deploy-session',
    DurationSeconds=3600,
    # Optionally restrict permissions for this session
    Policy=json.dumps({
        "Version": "2012-10-17",
        "Statement": [{"Effect": "Allow", "Action": "s3:*", "Resource": "*"}]
    })
)

credentials = response['Credentials']

# Use the temporary credentials
s3_client = boto3.client(
    's3',
    aws_access_key_id=credentials['AccessKeyId'],
    aws_secret_access_key=credentials['SecretAccessKey'],
    aws_session_token=credentials['SessionToken']
)

For cross-account access to work, two things must be true: (1) the role's trust policy must allow the calling principal to assume it, and (2) the calling principal must have sts:AssumeRole permission for that role ARN.

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Principal": {
        "AWS": "arn:aws:iam::111222333:role/CICDPipelineRole"
      },
      "Action": "sts:AssumeRole",
      "Condition": {
        "StringEquals": {
          "sts:ExternalId": "unique-external-id-12345"
        }
      }
    }
  ]
}
Note: The ExternalId condition prevents the "confused deputy" problem — where an attacker tricks a trusted service into assuming your role on their behalf. Always use ExternalId when a third-party (e.g., a SaaS vendor) needs to assume a role in your account.

IAM Conditions

Conditions make policies context-aware. They're the key to writing precise policies that enforce security controls without over-restricting.

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Action": "ec2:*",
      "Resource": "*",
      "Condition": {
        "StringEquals": {
          "aws:RequestedRegion": ["us-east-1", "us-west-2"]
        }
      }
    },
    {
      "Effect": "Deny",
      "Action": "*",
      "Resource": "*",
      "Condition": {
        "BoolIfExists": {
          "aws:MultiFactorAuthPresent": "false"
        }
      }
    },
    {
      "Effect": "Allow",
      "Action": "s3:PutObject",
      "Resource": "arn:aws:s3:::my-bucket/*",
      "Condition": {
        "StringEquals": {
          "s3:x-amz-server-side-encryption": "AES256"
        }
      }
    }
  ]
}

Common condition keys: aws:RequestedRegion, aws:SourceIp, aws:MultiFactorAuthPresent, aws:PrincipalTag, aws:ResourceTag, aws:CalledVia (for service-to-service), aws:SourceVpc (restrict to VPC traffic only).

IAM Access Analyzer

IAM Access Analyzer continuously monitors resource policies (S3 buckets, IAM roles, KMS keys, SQS queues, Lambda functions) and alerts you when they allow access from outside your AWS account or organization. It's a free service that catches over-permissive policies automatically.

# Create an analyzer scoped to your organization
aws accessanalyzer create-analyzer \
  --analyzer-name org-external-access \
  --type ORGANIZATION

# List active findings (publicly accessible resources, cross-account access)
aws accessanalyzer list-findings \
  --analyzer-arn arn:aws:access-analyzer:us-east-1:123456789:analyzer/org-external-access \
  --filter '{"status": {"eq": ["ACTIVE"]}}'

# Validate a policy before applying it (catches common errors)
aws accessanalyzer validate-policy \
  --policy-document file://my-policy.json \
  --policy-type IDENTITY_POLICY
Pro Tip: Use aws accessanalyzer validate-policy in your CI/CD pipeline to catch policy errors before they reach production. It detects redundant statements, unused condition keys, and policies that are more permissive than intended.

Frequently Asked Questions

Should I use IAM users or IAM roles for my application?

Always roles for applications. IAM roles issue temporary credentials that are auto-rotated by AWS — the application never holds a long-lived secret. IAM user access keys are permanent and frequently leaked via code repositories, Docker images, or environment variables. If your application runs on EC2, Lambda, ECS, or EKS, it can use an instance profile or execution role to get credentials from the metadata service automatically.

What's the difference between a permission boundary and an SCP?

Both cap the maximum permissions, but they operate at different scopes. Permission boundaries apply to individual IAM users or roles within an account. SCPs apply to all principals in an account or OU (including all roles and even the root user for most services). SCPs are the right tool for organization-wide guardrails ("no one in any dev account can disable CloudTrail"). Permission boundaries are for safely delegating IAM administration within an account.

How do I audit who has access to what in my AWS account?

Use three tools together: (1) IAM Access Analyzer for external access findings. (2) IAM Credential Report (aws iam generate-credential-report) for a CSV of all users and their credential status. (3) CloudTrail with Athena queries to find who called what API, when, from where. For ongoing compliance, enable AWS Config with IAM-related managed rules like iam-root-access-key-check and iam-user-mfa-enabled.

What is a service-linked role?

Service-linked roles are IAM roles predefined by AWS services. They have the exact permissions needed by that service, and the trust policy is locked to that service. You can't modify the permission policies, only delete the role (if the service no longer needs it). Examples: AWSServiceRoleForAutoScaling, AWSServiceRoleForECS. They're created automatically when you first use a service, or you can create them explicitly.

How do I rotate IAM access keys without downtime?

The process: (1) Create a second access key for the user (max 2 per user). (2) Update all applications/CI systems to use the new key. (3) Verify the old key is no longer being used (check CloudTrail for the old key ID). (4) Deactivate the old key and wait 24 hours. (5) Delete the old key. This gradual rotation avoids any service interruption. For automation, consider using AWS Secrets Manager to store and auto-rotate access keys.