惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

有赞技术团队
有赞技术团队
美团技术团队
博客园 - 司徒正美
freeCodeCamp Programming Tutorials: Python, JavaScript, Git & More
阮一峰的网络日志
阮一峰的网络日志
S
SegmentFault 最新的问题
博客园_首页
雷峰网
雷峰网
V
V2EX
The Cloudflare Blog
博客园 - 三生石上(FineUI控件)
量子位
Last Week in AI
Last Week in AI
人人都是产品经理
人人都是产品经理
爱范儿
爱范儿
奇客Solidot–传递最新科技情报
奇客Solidot–传递最新科技情报
钛媒体:引领未来商业与生活新知
钛媒体:引领未来商业与生活新知
博客园 - 聂微东
V
Visual Studio Blog
Hugging Face - Blog
Hugging Face - Blog
博客园 - 【当耐特】
Jina AI
Jina AI
月光博客
月光博客
L
LangChain Blog

DEV Community

Authentication Security Deep Dive: From Brute Force to Salted Hashing (With Java Examples) Why AI Systems Don’t Fail — They Drift Spilling beans for how i learn for exam😁"Reinforcement Learning Cheat Sheet" I Replaced Chrome with Safari for AI Browser Automation. Here's What Broke (and What Finally Worked) How Python Borrows Other People's Work The $40 Architecture: Processing 1 Billion API Requests with 99.99% Uptime Vibe Coding: A Workflow Guide (From Zero to SaaS) Most webhook security guides protect the wrong side. The scary part is delivery. Headless CMS for TanStack Start: Build a Blog with Cosmic EU Age Verification App "Hacked in 2 Minutes" — What Actually Happened Comfy Cloud’s delete function does not actually remove files Running AI Models on GPU Cloud Servers: A Beginner Guide Event-driven media intelligence with AWS Step Functions and Bedrock I scored 500 AI prompts across 8 quality dimensions — here's what broke How to Call Google Gemini API from Next.js (Free Tier, No Backend Needed) The Portal Protocol: Reclaiming Human Connection in the Age of AI How to Fix Your Team's Scattered Knowledge Problem With a Self-Hosted Forum Intro to tc Cloud Functors: A Graph-First Mental Model for the Modern Cloud Designing Multi-Tenant Backends With Both Ownership and Team Access I Built a Neumorphic CSS Library with 77+ Components — Here's What I Learned PostgreSQL Performance Optimization: Why Connection Pooling Is Critical at Scale Cómo construí un SaaS multi-rubro para gestionar expensas en Argentina con FastAPI + Vue 3 🚀 I Built an Ethical Hacking Scanner Tool – Open Source Project I Replaced /usage and /context in Claude Code With a Single Statusline A Pythonic Way to Handle Emails (IMAP/SMTP) with Auto-Discovery and AI-Ready Design I Collected 8.9 Million Polymarket Price Points — Here's What I Found About How Markets Really Move EcoTrack AI — Carbon Footprint Tracker & Dashboard Everyone's Using AI. No One Agrees How. 5 self-hosted ebook managers worth trying in 2026 Building Your First AI Agent with LangChain: From Chatbot to Autonomous Assistant
AWS IAM Explained: Users, Roles, Policies, and Least Priv...
Raghvendra Pandey · 2026-06-20 · via DEV Community

AWS IAM (Identity and Access Management) is the authorization layer for everything in AWS. Every API call — whether from the console, CLI, an EC2 instance, or a Lambda function — is evaluated against IAM policy. Get it wrong and you either lock out legitimate services or leave your infrastructure open to lateral movement. Despite its importance, IAM is where most teams accumulate the most technical debt, usually in the form of overly broad policies that were "temporary" and became permanent.

This guide covers how IAM actually works — the principal model, how policies are evaluated, role assumption, and the patterns used most in production infrastructure.

Principals: who is making the request

Every IAM interaction starts with a principal — the entity making the API call. AWS has four types of principals:

IAM users are long-lived identities with permanent credentials. They authenticate with a username/password for the console or an access key pair for the CLI/API. Creating IAM users for human access is legacy practice now — most organizations use SSO (AWS IAM Identity Center, formerly SSO) to federate human identities from an identity provider like Okta or Azure AD, then assume roles within AWS accounts. IAM users still make sense for service accounts that need permanent, programmatic API access, though roles are preferred even there when possible.

IAM roles are identities that can be assumed. They don't have permanent credentials — instead, when a principal assumes a role, STS (Security Token Service) issues temporary credentials that expire (typically 1–12 hours). Roles are the preferred mechanism for granting AWS services access to other AWS services, for cross-account access, and for federated human access.

IAM groups are collections of users. A group has policies attached to it; all users in the group inherit those permissions. Groups don't support role assumption — they're only for organizing users and applying policies in bulk.

AWS service principals represent AWS services (EC2, Lambda, ECS, etc.) acting on your behalf. When you create an EC2 instance profile or a Lambda execution role, you're granting an AWS service the ability to assume that role. The service principal looks like ec2.amazonaws.com or lambda.amazonaws.com.

Policies: what the principal is allowed to do

Policies are JSON documents that specify what actions are allowed or denied on which resources under what conditions. IAM has six types of policies:

Identity-based policies are attached directly to users, groups, or roles. They specify what that principal is allowed (or denied) to do.

Resource-based policies are attached to resources (S3 buckets, SQS queues, KMS keys, Lambda functions, etc.) rather than to principals. They specify who is allowed to do what with that resource. S3 bucket policies are the most common example. Resource-based policies support cross-account access without role assumption — you can grant a principal in another account direct access to an S3 bucket.

Permission boundaries cap the maximum permissions an IAM user or role can have. Even if an identity-based policy grants s3:*, if the permission boundary only allows s3:GetObject, only GetObject is allowed. This is useful for delegating IAM management — you can let teams create their own roles as long as those roles don't exceed the boundary.

Service Control Policies (SCPs) apply to entire AWS Organizations accounts. An SCP can prevent all accounts in an OU from using specific regions or services regardless of what IAM policies in those accounts say. SCPs are a ceiling, not a floor — they don't grant permissions, they only restrict them.

Session policies are optional policies passed at role assumption time via sts:AssumeRole or sts:GetFederationToken. They further restrict the permissions for that specific session without modifying the role itself.

Endpoint policies restrict what specific VPC endpoints can access.

How IAM evaluates a request

When an API call arrives, IAM evaluates it through a multi-step decision tree. Understanding this order is essential for diagnosing access issues:

  1. Explicit deny check: If any applicable policy (identity, resource, SCP, permission boundary) has an explicit Deny for the action, the request is denied immediately. Explicit denies always win.
  2. SCP check: If the account is in an Organization and an SCP doesn't allow the action, the request is denied regardless of IAM policies in the account.
  3. Resource-based policy check: If a resource-based policy explicitly allows the request (and there's no explicit deny), it's allowed. This is how cross-account access without role assumption works — the resource policy grants access to a principal in another account directly.
  4. Identity-based policy check: If an identity-based policy attached to the principal allows the action, it's allowed.
  5. Permission boundary check: If a permission boundary is set, the intersection of what the identity-based policy allows and what the boundary allows is the effective permission set.
  6. Default deny: If none of the above granted access, the request is denied. AWS defaults to deny-everything.

The practical takeaway: cross-account access needs to be allowed in both the identity-based policy on the source side AND the resource-based policy on the target side. A common mistake is adding a resource-based policy without giving the source principal permission to assume the role or access the resource.

Policy anatomy

An IAM policy document is JSON with a Statement array. Each statement has an Effect (Allow or Deny), Action (which API operations), Resource (which ARNs), and optionally a Condition block:

{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": [
"s3:GetObject",
"s3:PutObject"
],
"Resource": "arn:aws:s3:::my-company-artifacts/*"
},
{
"Effect": "Allow",
"Action": "s3:ListBucket",
"Resource": "arn:aws:s3:::my-company-artifacts",
"Condition": {
"StringLike": {
"s3:prefix": ["releases/*"]
}
}
}
]
}

The Condition block can restrict access by source IP, time of day, MFA status, required tags, and many other attributes. Conditions don't grant permissions — they further restrict when an Allow applies.

The Resource field is where least privilege is enforced. Instead of "Resource": "*", use the actual ARN of the specific resource. For S3, arn:aws:s3:::bucket-name grants access to the bucket itself (for ListBucket), while arn:aws:s3:::bucket-name/* grants access to objects inside it (for GetObject, PutObject).

Trust policies and role assumption

Every IAM role has two components: an identity-based policy (what the role can do) and a trust policy (who can assume the role). The trust policy is a resource-based policy on the role itself.

{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Principal": {
"Service": "ec2.amazonaws.com"
},
"Action": "sts:AssumeRole"
}
]
}

This trust policy allows EC2 instances to assume the role. For human cross-account access, the principal would be an IAM role or user ARN from another account. For OIDC-based access (GitHub Actions, EKS pods via IRSA), the principal is an OIDC provider with a condition that matches the federated identity claim.

Common patterns with Terraform

EC2 instance profile

An instance profile is the mechanism that attaches an IAM role to an EC2 instance. When EC2 starts an instance with an instance profile, the instance metadata endpoint (169.254.169.254 or IMDSv2 equivalent) provides temporary credentials for the attached role.

resource "aws_iam_role" "app_instance" {
name = "app-instance-role"

assume_role_policy = jsonencode({
Version = "2012-10-17"
Statement = [{
Effect    = "Allow"
Principal = { Service = "ec2.amazonaws.com" }
Action    = "sts:AssumeRole"
}]
})
}

resource "aws_iam_role_policy" "app_instance" {
role = aws_iam_role.app_instance.id

policy = jsonencode({
Version = "2012-10-17"
Statement = [
{
Effect   = "Allow"
Action   = ["s3:GetObject"]
Resource = "arn:aws:s3:::${var.config_bucket}/*"
},
{
Effect   = "Allow"
Action   = ["secretsmanager:GetSecretValue"]
Resource = "arn:aws:secretsmanager:${var.region}:${var.account_id}:secret:app/*"
}
]
})
}

resource "aws_iam_instance_profile" "app" {
name = "app-instance-profile"
role = aws_iam_role.app_instance.name
}

Lambda execution role

Lambda functions need a role with at minimum the AWSLambdaBasicExecutionRole managed policy (for CloudWatch Logs). Add only the additional permissions the function actually needs:

resource "aws_iam_role" "lambda_execution" {
name = "my-function-execution-role"

assume_role_policy = jsonencode({
Version = "2012-10-17"
Statement = [{
Effect    = "Allow"
Principal = { Service = "lambda.amazonaws.com" }
Action    = "sts:AssumeRole"
}]
})
}

resource "aws_iam_role_policy_attachment" "lambda_basic" {
role       = aws_iam_role.lambda_execution.name
policy_arn = "arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole"
}

resource "aws_iam_role_policy" "lambda_specific" {
role = aws_iam_role.lambda_execution.id

policy = jsonencode({
Version = "2012-10-17"
Statement = [{
Effect   = "Allow"
Action   = ["dynamodb:GetItem", "dynamodb:PutItem"]
Resource = aws_dynamodb_table.data.arn
}]
})
}

IRSA — IAM Roles for Service Accounts (EKS)

IRSA allows Kubernetes pods to assume IAM roles without sharing credentials at the node level. The trust policy uses the cluster's OIDC provider:

data "aws_iam_openid_connect_provider" "eks" {
url = aws_eks_cluster.main.identity[0].oidc[0].issuer
}

resource "aws_iam_role" "pod_role" {
name = "my-app-pod-role"

assume_role_policy = jsonencode({
Version = "2012-10-17"
Statement = [{
Effect = "Allow"
Principal = {
Federated = data.aws_iam_openid_connect_provider.eks.arn
}
Action = "sts:AssumeRoleWithWebIdentity"
Condition = {
StringEquals = {
"${trimprefix(data.aws_iam_openid_connect_provider.eks.url, "https://")}:sub" =
"system:serviceaccount:${var.namespace}:${var.service_account_name}"
}
}
}]
})
}

The Kubernetes ServiceAccount is annotated with the role ARN. When a pod runs with that ServiceAccount, the EKS Pod Identity webhook injects environment variables that direct the AWS SDK to use WebIdentity tokens for authentication.

Cross-account access

Cross-account access requires granting permission on both sides. In the target account, create a role with a trust policy that allows the source account:

# In the target account (account B)
resource "aws_iam_role" "cross_account" {
name = "cross-account-access-role"

assume_role_policy = jsonencode({
Version = "2012-10-17"
Statement = [{
Effect    = "Allow"
Principal = { AWS = "arn:aws:iam::${var.source_account_id}:root" }
Action    = "sts:AssumeRole"
Condition = {
StringEquals = {
"sts:ExternalId" = var.external_id  # prevents confused deputy attacks
}
}
}]
})
}

# In the source account (account A), grant the permission to assume
resource "aws_iam_role_policy" "assume_cross_account" {
role = aws_iam_role.deployer.id

policy = jsonencode({
Version = "2012-10-17"
Statement = [{
Effect   = "Allow"
Action   = "sts:AssumeRole"
Resource = "arn:aws:iam::${var.target_account_id}:role/cross-account-access-role"
}]
})
}

Implementing least privilege

The principle of least privilege means granting only the permissions needed to perform a specific function and nothing more. In practice, it means:

  • Scope actions precisely: Use s3:GetObject not s3:*. Use ec2:DescribeInstances not ec2:*.
  • Scope resources precisely: Use the specific resource ARN rather than *. For DynamoDB: arn:aws:dynamodb:region:account:table/specific-table, not *.
  • Use conditions: Add conditions to restrict when permissions apply — source IP, MFA requirement, resource tags, etc.
  • Avoid wildcard patterns in sensitive services: iam:*, sts:*, and kms:* are especially dangerous. Wildcard access to IAM means the principal can grant itself any permission.

AWS's IAM Access Analyzer and the IAM Policy Simulator help audit existing policies. Access Analyzer also has a feature that generates least-privilege policies from CloudTrail logs — it observes what an identity actually calls over a period and generates a policy based on observed usage. This is the most practical approach for brownfield permissions: run the role in production, observe for 30–90 days, then generate a tight policy from actual usage.

Common mistakes

AdministratorAccess on service roles. The policy arn:aws:iam::aws:policy/AdministratorAccess grants full access to everything in the account. Attaching this to a Lambda function or EC2 instance because "it needs to do a lot of things" is a security catastrophe waiting to happen. A compromised instance or function with AdministratorAccess can exfiltrate data, create new users, disable logging, or delete everything.

Storing long-lived access keys in code or environment variables. Access keys associated with IAM users don't expire. If they leak (in a git commit, an S3 bucket, logs), they're valid indefinitely until rotated. Use IAM roles and instance metadata instead. For CI/CD, use OIDC federation (GitHub Actions OIDC, GitLab OIDC) rather than static keys.

Shared IAM users across services or teams. One IAM user shared by multiple services means you can't audit which service made which API call, can't rotate credentials for one service without affecting others, and can't revoke access for one service without affecting all. Each service gets its own role.

Permission boundaries not used for delegated IAM. If you let application teams create their own IAM roles, they can escalate their own privileges. A permission boundary constrains what roles they can create — the team can create roles, but those roles can't exceed what the permission boundary allows.

Debugging access issues

When an IAM error appears (AccessDeniedException or is not authorized to perform: on resource:), the debug workflow is:

  1. Identify the principal in the error — it's in the message as "User: arn:aws:iam::..."
  2. Use the IAM Policy Simulator in the console to test whether that principal has the required permission
  3. Check for explicit denies — SCPs, permission boundaries, resource policies
  4. Check that resource-based policies allow the cross-account action if applicable
  5. Enable CloudTrail data events if not already enabled — access denials are logged there with full context

IAM is complex enough that AWS's documentation for specific services often lists exactly which IAM permissions are needed. The IAM actions reference at docs.aws.amazon.com/service-authorization/latest/reference/ is the authoritative source for every service, action, resource type, and condition key.

Related articles