---
title: AWS IAM Best Practices: Least Privilege Access Control
description: Least privilege is a slogan. Working IAM at production scale is a different problem. Roles vs users, permission boundaries, SCPs, identity federation, and the access-control patterns that keep teams fast without leaving keys lying around.
url: https://www.factualminds.com/blog/aws-iam-best-practices-least-privilege-access-control/
datePublished: 2026-02-20T00:00:00.000Z
dateModified: 2026-05-14T00:00:00.000Z
author: Palaniappan P
category: Security & Compliance
tags: iam, security, aws, compliance, architecture
---

# AWS IAM Best Practices: Least Privilege Access Control

> Least privilege is a slogan. Working IAM at production scale is a different problem. Roles vs users, permission boundaries, SCPs, identity federation, and the access-control patterns that keep teams fast without leaving keys lying around.

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.

**May 2026 refresh:** Treat **IAM Access Analyzer** as part of your continuous least-privilege loop—not a one-off audit. It surfaces external/public/cross-account access, validates policies against AWS security checks, and highlights unused permissions so generated policies reflect observed usage rather than guesses. Feature overview and rollout guidance: [IAM Access Analyzer features](https://aws.amazon.com/iam/access-analyzer/features/).

## 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:

| Feature      | IAM Users                              | IAM Roles                                |
| ------------ | -------------------------------------- | ---------------------------------------- |
| Credentials  | Long-lived access keys                 | Temporary (15 min - 12 hours)            |
| Rotation     | Manual (must rotate keys)              | Automatic (new credentials each session) |
| Sharing risk | Keys can be copied, emailed, committed | Cannot be shared (assumed per session)   |
| Auditing     | Track key usage                        | Track 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](#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:

```json
{
  "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:

```json
{
  "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):**

```json
{
  "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:**

```json
{
  "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 Key                 | Use Case                                 |
| ----------------------------- | ---------------------------------------- |
| `aws:RequestedRegion`         | Restrict to approved Regions             |
| `aws:PrincipalOrgID`          | Allow access only from your Organization |
| `aws:ResourceTag/Environment` | Scope by environment tag                 |
| `aws:MultiFactorAuthPresent`  | Require MFA for sensitive actions        |
| `aws:SourceIp`                | Restrict to corporate IP ranges          |
| `ec2:InstanceType`            | Limit instance types                     |
| `s3:prefix`                   | Restrict S3 access to specific prefixes  |

**MFA for sensitive operations:**

```json
{
  "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:

```json
{
  "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](/blog/aws-multi-account-strategy-landing-zone-best-practices/), 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:**

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

**Restrict to approved Regions:**

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

**Prevent leaving the Organization:**

```json
{
  "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.

## Multi-Factor Authentication

MFA is a non-negotiable control for any human access to AWS. The question is no longer _whether_ to require it, but _which form_ to use. Since 2024, AWS supports passkeys (FIDO2) as a co-equal alternative to physical hardware security keys — both are phishing-resistant, both work for IAM users, IAM Identity Center users, and the root account.

| MFA Type        | Use Case                         | Strength           |
| --------------- | -------------------------------- | ------------------ |
| Passkey (FIDO2) | Default for all users incl. root | Phishing-resistant |
| Hardware key    | High-assurance privileged users  | Phishing-resistant |
| Virtual MFA     | Fallback only                    | Phishable          |
| SMS             | Do not use                       | Deprecated         |

**Root account specifically:** AWS now allows up to eight MFA devices on the root user. Register at least two — one passkey on a primary device and a hardware key stored in a sealed envelope as the break-glass option. A single MFA device is a single point of failure for the most privileged account in the organization.

> **Password policy (legacy users only):** If you still operate IAM users with console passwords, enforce a strong password policy — minimum 14 characters, complexity requirements, no reuse of the last 24, expire after 90 days. Set this once via `iam:UpdateAccountPasswordPolicy`. The right long-term move is to delete those users and migrate human access to IAM Identity Center, where the password policy lives in your corporate IdP (Okta, Entra ID) and you get SSO, automatic deprovisioning, and centralized session controls for free.

## 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:**

```json
{
  "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](/services/aws-cloud-security/) 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](/blog/aws-cloudwatch-observability-metrics-logs-alarms-best-practices/) for critical IAM events:

| Event                     | Alert Condition                                     |
| ------------------------- | --------------------------------------------------- |
| Root user login           | Any `ConsoleLogin` by root                          |
| Access key creation       | `CreateAccessKey` for any user                      |
| Policy changes            | `PutUserPolicy`, `AttachRolePolicy`, `CreatePolicy` |
| Role trust policy changes | `UpdateAssumeRolePolicy`                            |
| Failed authentication     | `ConsoleLogin` 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 every other AWS compute service supports IAM roles with temporary credentials — use them. For external systems, OIDC federation works with GitHub Actions, GitLab CI, Bitbucket, CircleCI, and Buildkite. For on-premises workloads that genuinely cannot reach STS via OIDC, **IAM Roles Anywhere** issues short-lived credentials against an X.509 certificate from a trust anchor of your choice — eliminating the need for static IAM-user keys on physical servers. The "rotate keys every 90 days" rule of thumb is a legacy fallback for the credentials that genuinely cannot be eliminated, not a substitute for eliminating them.

### 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. Stale access is one of the easiest paths to compromise — an attacker who finds an old key in a public repository, a contractor's laptop, or a backup tape gets a free pass into your account.

**Quarterly review workflow:**

1. **Download a credential report** (`aws iam generate-credential-report` then `get-credential-report`). Review the `password_last_used`, `access_key_1_last_used_date`, and `access_key_2_last_used_date` columns.
2. **Run IAM Access Analyzer unused-access findings** — it surfaces roles, users, and granted actions that have not been used in the lookback window (default 90 days).
3. **Apply action thresholds:** at 90 days unused → disable the credential or detach the policy; at 180 days → delete it. Document exceptions in a register.
4. **Cross-check with CloudTrail** — for any credential flagged as unused, verify there are no recent `errorCode` events suggesting the credential is being attempted but failing.

For the residual on-prem workloads that need long-lived authentication, replace IAM users with **IAM Roles Anywhere** (X.509 certificate trust anchor → short-lived STS credentials) so there is nothing to rotate in the first place.

## 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 (passkeys or hardware keys; SMS removed)
- [ ] Root user access keys deleted, multiple MFA devices registered (passkey + hardware key), 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](/blog/aws-guardduty-threat-detection-production-guide/) for threat detection, [CloudWatch](/blog/aws-cloudwatch-observability-metrics-logs-alarms-best-practices/) for monitoring, and [SCPs](/blog/aws-multi-account-strategy-landing-zone-best-practices/) for organizational guardrails, it provides the access control layer that production workloads require.

For IAM architecture design, [security assessments](/services/aws-cloud-security/), and [multi-account access control](/services/aws-architecture-review/), talk to our team.

[Contact us to secure your AWS access controls →](/contact-us/)

## FAQ

### What does least privilege mean in AWS IAM?
Granting only the actions and resources a workload actually needs — neither broader (`Action: '*'`) nor so narrow that teams build workarounds. The practical pattern is to start from an AWS managed policy, monitor real usage with IAM Access Analyzer's policy generation, then replace it with a custom policy scoped to the observed access pattern.

### When should I use IAM users vs IAM roles?
Default to IAM roles for everything: EC2 instance profiles, ECS task roles, Lambda execution roles, cross-account role assumption, and human access via IAM Identity Center federation. The only legitimate use of IAM users with access keys is external CI/CD systems that genuinely cannot assume roles via OIDC — and even GitHub Actions, GitLab CI, and CircleCI now support OIDC federation, so the list is shrinking.

### What is the difference between an IAM permission boundary and an SCP?
Permission boundaries cap the maximum permissions an IAM user or role can have within an account — typically used to delegate IAM permissions safely (a developer can create roles, but only roles bounded by your policy). SCPs cap maximum permissions for every entity in an account or OU at the AWS Organizations level — they cannot grant access, only restrict it. Use SCPs for organization-wide guardrails (deny region, deny disabling CloudTrail) and boundaries for in-account delegation.

### How do explicit deny statements work in IAM evaluation?
IAM evaluation order is explicit deny → explicit allow → implicit deny. An explicit deny in any policy (identity-based, resource-based, SCP, permission boundary, or session policy) wins over every allow. Use this for preventive controls: deny instance launches outside an approved type list, deny disabling encryption, deny region usage outside compliance boundaries — even if a future IAM policy is too permissive, the deny still blocks the action.

### How do I federate human access to AWS without IAM users?
Use IAM Identity Center as the federation hub. Connect your IdP (Okta, Microsoft Entra ID, Google Workspace, or Identity Center directory) once, define permission sets (RBAC roles), assign groups to accounts, and users get short-lived credentials via the AWS access portal or CLI. SCIM keeps user/group state in sync. With identity propagation enabled, the workforce identity also flows into Q Business, Redshift, QuickSight, and S3 Access Grants for per-user audit and row-level security.

---

*Source: https://www.factualminds.com/blog/aws-iam-best-practices-least-privilege-access-control/*
