AWS IAM Roles & Policies: A Practical Guide

Blog / Amazon Web Services · March 2, 2025 · Updated June 10, 2026 · 10 min read
AWS IAM Roles & Policies: A Practical Guide

AWS Identity and Access Management (IAM) decides who can do what to which resource in your AWS account. It rests on two ideas. Identities (users, groups, and roles) are who is making a request. Policies are JSON documents that allow or deny specific actions on specific resources. A request is allowed only when an attached policy explicitly permits it and nothing explicitly denies it — IAM is deny-by-default.

The single most important shift in modern AWS access is this: stop handing out long-lived credentials. Do not download root API keys, and avoid creating IAM users with permanent access keys for people or workloads. Instead, give EC2 instances, Lambda functions, containers, and CI/CD pipelines an IAM role they assume to get short-lived, automatically rotated credentials, and give your team human access through IAM Identity Center (the successor to AWS SSO). This guide explains every piece — identities, policy types, the anatomy of a policy, and roles — with valid JSON and CLI you can adapt today.

IAM identities: users, groups, and roles

An IAM identity is anything that can be authenticated and authorized in your account.

  • Users are long-lived identities for a specific person or, historically, an application. A user can have a console password and/or access keys. Treat user access keys as a last resort.
  • Groups are collections of users that share permissions (for example Developers or Billing). You attach policies to the group, and every member inherits them. Groups cannot be nested and cannot be a principal in a policy.
  • Roles are identities with permissions but no permanent credentials. A role is meant to be assumed by a trusted principal — an AWS service, an IAM user, a user from another account, or a federated/workload identity. When assumed, IAM (via AWS STS) issues temporary credentials that expire, so there is nothing long-lived to leak.

The root user is the account owner and has unrestricted access. Lock it away: enable MFA, never create root access keys, and use it only for the rare tasks that genuinely require it.

IAM user vs IAM role

Aspect IAM user IAM role
Credentials Long-lived password and/or access keys Temporary credentials issued by STS on assume
Lifetime Persistent until deleted Session expires (15 min – up to 12 hours)
Who uses it A specific person or app Anyone/anything trusted to assume it
How access is granted Identity-based policies attached directly Permission policy + a separate trust policy
Best for Break-glass / legacy edge cases EC2, Lambda, ECS, CI/CD, cross-account, federation
Rotation Manual (you must rotate keys) Automatic (short-lived by design)

The practical takeaway: prefer roles. Use IAM Identity Center for humans and roles for every workload. Reserve IAM users with keys for narrow, well-justified cases, and rotate or eliminate those keys aggressively.

Policy types

Permissions in IAM come from several kinds of policy, and a request is evaluated against all that apply.

  • Identity-based policies attach to a user, group, or role and say what that identity may do. They can be AWS managed, customer managed, or inline.
  • Resource-based policies attach to a resource (an S3 bucket, SQS queue, KMS key, Lambda function) and say which principals may act on it. They are the only way to grant cross-account access without the caller assuming a role, and they always include a Principal.
  • Service Control Policies (SCPs) are set in AWS Organizations and act as guardrails on entire accounts or organizational units. They never grant permissions — they cap the maximum that any identity in the account can be given.
  • Permission boundaries are an advanced identity-based control that sets the maximum permissions a user or role can have. Effective permissions are the intersection of the boundary and the attached policies. They are ideal for safely delegating IAM creation to developers.
  • Session policies are passed inline when assuming a role (for example via aws sts assume-role) to further restrict that single session.

Effective permissions are the intersection of all applicable allow paths, minus any explicit deny. An explicit Deny anywhere always wins.

Identity-based vs resource-based policies

Identity-based Resource-based
Attached to User, group, or role A resource (bucket, queue, key, function)
Has a Principal? No Yes (required)
Answers "What can this identity do?" "Who can act on this resource?"
Cross-account Caller must assume a role Can grant directly to another account
Typical use Day-to-day permissions S3 bucket policies, KMS key policies, SNS/SQS access

Anatomy of a policy document

An IAM policy is a JSON document made of one or more Statement blocks. The key elements:

  • Version — always use "2012-10-17" (the current policy language version; 2008-10-17 is legacy).
  • EffectAllow or Deny.
  • Action — the API operations, like s3:GetObject or ec2:DescribeInstances. Wildcards are allowed but should be scoped tightly.
  • Resource — the ARN(s) the actions apply to. Avoid "*" unless the action genuinely has no resource scope.
  • Condition — optional tests that must be true (for example, require MFA, restrict by source IP, or limit by aws:SourceArn).
  • Principal — required only in resource-based and trust policies; it names who the statement applies to.

Here is a tightly scoped, least-privilege identity policy that lets a user read and write objects in one specific bucket and prefix only — note the valid bucket name (lowercase, no underscores) and the two-statement pattern that grants object actions plus the bucket-level ListBucket:

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Sid": "ReadWriteAppUploads",
      "Effect": "Allow",
      "Action": [
        "s3:GetObject",
        "s3:PutObject",
        "s3:DeleteObject"
      ],
      "Resource": "arn:aws:s3:::my-app-uploads-prod/reports/*"
    },
    {
      "Sid": "ListOnlyTheReportsPrefix",
      "Effect": "Allow",
      "Action": "s3:ListBucket",
      "Resource": "arn:aws:s3:::my-app-uploads-prod",
      "Condition": {
        "StringLike": { "s3:prefix": "reports/*" }
      }
    }
  ]
}

Compare this with the old, dangerous pattern of "Action": "s3:*" on a whole bucket: that grants bucket deletion, policy changes, and ACL edits — far more than an app needs. Granting only the three object actions plus a prefix-scoped ListBucket is the difference between least privilege and an accident waiting to happen.

IAM Access Analyzer can generate a least-privilege policy like this for you by analysing CloudTrail activity, and its policy validation flags overly broad statements before you ship them.

IAM roles deep-dive

A role has two policies that do different jobs:

  1. Trust policy (the AssumeRolePolicyDocument) — a resource-based policy on the role that says who is allowed to assume it. Its Principal might be an AWS service (ec2.amazonaws.com), another account, or a federated identity provider.
  2. Permission policy — one or more identity-based policies that say what the role can do once assumed.

When a trusted principal calls sts:AssumeRole (directly or implicitly, as EC2 and Lambda do for you), STS returns temporary credentials scoped to the role's permission policies for the session duration.

Common role use cases:

  • EC2 instance profiles — attach a role to an EC2 instance so code on it gets credentials from the instance metadata service (IMDSv2) instead of baked-in keys.
  • Service roles — Lambda execution roles, ECS task roles, CodeBuild/CodePipeline roles, and so on, so AWS services act on your behalf with scoped permissions.
  • Cross-account access — let an identity in Account A assume a role in Account B instead of duplicating users.
  • Federated / workload identity — let users from your SSO/IdP (SAML or OIDC), or GitHub Actions via OIDC, assume roles with no stored AWS keys at all.

Here is a trust policy that lets EC2 assume the role and additionally lets a specific external account assume it — but only when an ExternalId matches and MFA is present (the standard defenses against the confused-deputy problem):

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Sid": "AllowEC2ToAssume",
      "Effect": "Allow",
      "Principal": { "Service": "ec2.amazonaws.com" },
      "Action": "sts:AssumeRole"
    },
    {
      "Sid": "AllowTrustedAccountWithGuardrails",
      "Effect": "Allow",
      "Principal": { "AWS": "arn:aws:iam::111122223333:root" },
      "Action": "sts:AssumeRole",
      "Condition": {
        "StringEquals": { "sts:ExternalId": "acme-prod-2026" },
        "Bool": { "aws:MultiFactorAuthPresent": "true" }
      }
    }
  ]
}

For a service that calls back into your account on your behalf (S3 event notifications, CloudWatch, EventBridge), scope the trust with aws:SourceArn and aws:SourceAccount so only your specific resource can trigger the role — this closes the cross-service confused-deputy gap. Always confirm the latest condition keys and service-specific guidance in the AWS IAM documentation before going to production.

Creating and assuming roles with the AWS CLI

Create a role from a trust policy file, attach a scoped permission policy, then assume it. Save the trust policy above as trust-policy.json and the least-privilege S3 policy as s3-policy.json.

# 1. Create the role with its trust policy (who may assume it)
aws iam create-role \
  --role-name app-uploads-role \
  --assume-role-policy-document file://trust-policy.json \
  --max-session-duration 3600

# 2. Attach a scoped permission policy (what the role can do)
aws iam put-role-policy \
  --role-name app-uploads-role \
  --policy-name s3-uploads-readwrite \
  --policy-document file://s3-policy.json

# 3. Assume the role to get short-lived credentials
aws sts assume-role \
  --role-arn arn:aws:iam::111122223333:role/app-uploads-role \
  --role-session-name dev-session \
  --external-id acme-prod-2026

assume-role returns an AccessKeyId, SecretAccessKey, and SessionToken that expire when the session ends. Export those three values (the SDKs and CLI need all three) and the calling environment temporarily acts as the role — no permanent keys involved. In practice you rarely script this by hand: configure a named profile with role_arn and source_profile (or sso_session) in ~/.aws/config and the CLI assumes the role for you.

Least-privilege best practices

  • Prefer roles and short-lived credentials over access keys. Use IAM Identity Center for human sign-in and IAM roles for every workload (EC2, Lambda, ECS, EKS via IRSA/Pod Identity, CI/CD via OIDC). Eliminate long-lived user keys wherever you can.
  • Lock down root. Enable MFA on root, delete any root access keys, and use root only for the handful of tasks that require it.
  • Grant the minimum. Start from zero and add specific actions and resource ARNs. Use IAM Access Analyzer to generate policies from real usage and to validate and find unused access.
  • Use conditions. Require MFA (aws:MultiFactorAuthPresent), restrict source (aws:SourceArn, aws:SourceAccount, aws:SourceIp), and use ExternalId for third-party cross-account roles.
  • Set guardrails with SCPs and permission boundaries. SCPs cap whole accounts; boundaries cap delegated role/user creation.
  • Enforce IMDSv2 on EC2 so instance role credentials cannot be stolen via SSRF.
  • Rotate, monitor, and review. Log everything to CloudTrail, alert on root and AssumeRole activity, and remove unused users, keys, and roles on a schedule.
  • Use customer-managed policies over inline for reusability and versioning, and tag identities for attribute-based access control (ABAC) where it scales better than per-resource policies.

How MicroPyramid can help

With 12+ years building and operating cloud applications across 50+ projects, MicroPyramid designs IAM that is secure and practical: least-privilege roles, OIDC-based CI/CD with zero stored keys, cross-account guardrails with SCPs and permission boundaries, and Access Analyzer reviews that catch over-broad access before it ships. We bring the same discipline to cloud migration, where getting identity and access right early prevents costly rework — often cutting cloud spend by around 30% along the way. If IAM ties into serverless, see our guides on AWS Lambda best practices (execution roles) and AWS cost and performance optimization.

Frequently Asked Questions

What is the difference between an IAM user and an IAM role?

An IAM user is a long-lived identity with permanent credentials (a password and/or access keys) tied to a specific person or app. An IAM role has no permanent credentials; it is assumed by a trusted principal and grants temporary, auto-expiring credentials via AWS STS. Prefer roles for workloads and cross-account access, and IAM Identity Center for human sign-in, because there is nothing long-lived to leak.

What is a trust policy versus a permission policy on a role?

A role has two policies. The trust policy (the AssumeRolePolicyDocument) is a resource-based policy that defines who may assume the role — its Principal can be an AWS service, another account, or a federated identity provider. The permission policy is an identity-based policy that defines what the role can do once assumed. Both must allow the action for access to succeed.

Should I still create IAM users with access keys in 2026?

Usually no. Long-lived access keys are a common source of breaches. Use IAM Identity Center for people and IAM roles for workloads so they receive short-lived, automatically rotated credentials. CI/CD systems like GitHub Actions can assume roles via OIDC with no stored AWS keys at all. Reserve IAM user keys for narrow, well-justified cases and rotate or remove them aggressively.

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

Identity-based policies attach to a user, group, or role and answer "what can this identity do?" Resource-based policies attach to a resource (such as an S3 bucket or KMS key), include a Principal, and answer "who may act on this resource?" Resource-based policies can grant cross-account access directly, whereas identity-based access across accounts typically requires assuming a role.

How do SCPs and permission boundaries differ?

Service Control Policies (SCPs) are set in AWS Organizations and cap the maximum permissions for entire accounts or organizational units — they never grant access, only limit it. Permission boundaries are an identity-based control that caps the maximum permissions a specific user or role can have. Both are guardrails; effective permissions are the intersection of every applicable limit and grant, minus any explicit deny.

How do I apply least privilege in IAM?

Start from zero permissions and add only the specific actions and resource ARNs needed. Scope Resource to exact ARNs instead of "*", add Condition keys (require MFA, restrict source ARN/IP), and use IAM Access Analyzer to generate policies from real CloudTrail usage, validate them, and find unused access. Reinforce with SCPs and permission boundaries, enforce IMDSv2, and review and remove unused identities regularly.

Share this article