AWS
15 min readMay 7, 2026

AWS IAM: Roles, Policies, Permission Boundaries, and IRSA for EKS

IAM is the authorization layer for everything in AWS. Getting it wrong means either over-privileged workloads that expand your blast radius, or under-privileged ones that fail at runtime. This covers IAM identity types, policy evaluation logic, permission boundaries, IRSA for EKS pod-level AWS access, cross-account role assumption, IAM Access Analyzer, and the patterns that work at scale across multiple AWS accounts.

CO
Coding Protocols Team
Platform Engineering
AWS IAM: Roles, Policies, Permission Boundaries, and IRSA for EKS

IAM controls what every principal in AWS can do — EC2 instances, Lambda functions, EKS pods, CI/CD pipelines, and humans. The access control model is flexible enough to model complex multi-account architectures, but that flexibility means there are many ways to misconfigure it. IAM errors tend to surface at runtime, after deployment, in production.

This covers the mechanics of IAM that matter for platform engineering: how policies are evaluated, how to scope permissions correctly, and how to give Kubernetes pods AWS access without long-lived credentials.


IAM Principals

IAM Users — long-lived identities with access keys. Avoid these for applications. Use roles instead. Users are appropriate for human access with MFA enforcement and short-lived session tokens via aws sts get-session-token.

IAM Roles — assumed by services, instances, Lambda functions, EKS pods, or other AWS accounts. A role is a set of permissions with no permanent credentials. Instead, the STS service issues short-lived tokens (up to 12 hours) when a principal assumes the role.

IAM Groups — collections of users that share policies. Groups don't have credentials and can't be assumed by services.

Federated identities — SSO users who authenticate via an IdP (Okta, Azure AD, Google Workspace) and receive temporary AWS credentials via SAML or OIDC federation.


Policy Types

IAM has six policy types that participate in the access decision:

Policy TypeAttached ToPurpose
Identity-basedUser, role, or groupWhat the principal can do
Resource-basedS3 bucket, SQS queue, KMS key, etc.Who can access this resource
Permission boundaryUser or roleMaximum permissions a principal can ever have
SCP (Service Control Policy)AWS Organization account or OUMaximum permissions any principal in the account can have
Session policyAssumed role sessionFurther restrict a specific session
ACLS3, VPCLegacy; prefer bucket policies and security groups

Identity-Based Policy Example

json
1{
2  "Version": "2012-10-17",
3  "Statement": [
4    {
5      "Effect": "Allow",
6      "Action": [
7        "s3:GetObject",
8        "s3:PutObject"
9      ],
10      "Resource": "arn:aws:s3:::my-app-bucket/*",
11      "Condition": {
12        "StringEquals": {
13          "s3:prefix": ["uploads/", "exports/"]
14        }
15      }
16    },
17    {
18      "Effect": "Deny",
19      "Action": "s3:DeleteObject",
20      "Resource": "arn:aws:s3:::my-app-bucket/*"
21    }
22  ]
23}

Policy Evaluation Order

IAM evaluates policies in this order (explicit deny beats everything; for same-account access, a resource-based policy allow is sufficient without a matching identity policy allow; for cross-account access, both must allow):

  1. Explicit Deny — if any policy (anywhere in the evaluation chain) has an explicit Deny, access is denied. Denies always win.
  2. SCP — if the account has SCPs, the action must be allowed by the SCP.
  3. Resource-based policy — for cross-account access, the resource policy must allow the cross-account principal.
  4. Permission boundary — if present, the effective permissions are the intersection of the identity policy and the boundary.
  5. Identity-based policy — the principal's own policies must allow the action.
  6. Session policy — if present, further restricts the session.
  7. Default Deny — if no explicit allow was found, access is denied.

The mental model: explicit deny beats everything; then you need an allow in each applicable layer.


Permission Boundaries

A permission boundary sets the maximum permissions a role or user can have. It doesn't grant permissions — it caps them. If a role has AdministratorAccess but a boundary of S3FullAccess, the role can only do S3 actions.

The effective permissions are the intersection of the identity policy, the boundary, and any session policy:

Effective permissions = identity_policy ∩ permission_boundary ∩ session_policy

SCPs also cap permissions at the account level. Note that for same-account access via resource-based policies, the boundary is not evaluated — a resource policy can grant access to a same-account principal even if the action isn't in the permission boundary.

Use case: delegating IAM role creation to developers without giving them full IAM access. Set a permission boundary on any roles they create, and constrain them to only create roles with that boundary:

json
1{
2  "Version": "2012-10-17",
3  "Statement": [
4    {
5      "Sid": "AllowRoleCreationWithBoundary",
6      "Effect": "Allow",
7      "Action": [
8        "iam:CreateRole",
9        "iam:PutRolePolicy",
10        "iam:AttachRolePolicy"
11      ],
12      "Resource": "arn:aws:iam::012345678901:role/*",
13      "Condition": {
14        "StringEquals": {
15          "iam:PermissionsBoundary": "arn:aws:iam::012345678901:policy/DeveloperBoundary"
16        }
17      }
18    }
19  ]
20}

Without this pattern, allowing developers to create IAM roles means they can create a role with AdministratorAccess and use it to escalate privileges.


IRSA: IAM Roles for Service Accounts

IRSA gives individual Kubernetes pods AWS credentials without long-lived access keys, node-level instance profiles, or kube2iam/kiam sidecars.

How it works:

  1. EKS creates an OIDC identity provider for the cluster
  2. Kubernetes service accounts are annotated with an IAM role ARN
  3. When a pod starts, the EKS Pod Identity Webhook injects two environment variables: AWS_WEB_IDENTITY_TOKEN_FILE (path to the projected service account token) and AWS_ROLE_ARN (the role ARN from the service account annotation)
  4. The AWS SDK exchanges this token with STS via AssumeRoleWithWebIdentity to get temporary credentials
  5. The temporary credentials are scoped to the annotated role

Setup

Step 1: Enable OIDC provider for the cluster (once per cluster):

bash
eksctl utils associate-iam-oidc-provider \
  --cluster my-cluster \
  --region us-east-1 \
  --approve

Verify:

bash
aws iam list-open-id-connect-providers | grep $(aws eks describe-cluster \
  --name my-cluster \
  --query "cluster.identity.oidc.issuer" \
  --output text | cut -d '/' -f 5)

Step 2: Create the IAM role with trust policy:

json
1{
2  "Version": "2012-10-17",
3  "Statement": [
4    {
5      "Effect": "Allow",
6      "Principal": {
7        "Federated": "arn:aws:iam::012345678901:oidc-provider/oidc.eks.us-east-1.amazonaws.com/id/EXAMPLED539D4633E53DE1B71EXAMPLE"
8      },
9      "Action": "sts:AssumeRoleWithWebIdentity",
10      "Condition": {
11        "StringEquals": {
12          "oidc.eks.us-east-1.amazonaws.com/id/EXAMPLED539D4633E53DE1B71EXAMPLE:sub": "system:serviceaccount:payments:payments-api",
13          "oidc.eks.us-east-1.amazonaws.com/id/EXAMPLED539D4633E53DE1B71EXAMPLE:aud": "sts.amazonaws.com"
14        }
15      }
16    }
17  ]
18}

The sub condition locks the role to a specific namespace and service account — system:serviceaccount:<namespace>:<service-account-name>. Without this condition, any pod in any namespace using any service account could assume the role.

Step 3: Annotate the Kubernetes ServiceAccount:

yaml
1apiVersion: v1
2kind: ServiceAccount
3metadata:
4  name: payments-api
5  namespace: payments
6  annotations:
7    eks.amazonaws.com/role-arn: arn:aws:iam::012345678901:role/PaymentsApiRole

Step 4: Pods using this service account automatically get AWS credentials:

yaml
1apiVersion: apps/v1
2kind: Deployment
3metadata:
4  name: payments-api
5  namespace: payments
6spec:
7  template:
8    spec:
9      serviceAccountName: payments-api    # Must match the annotated ServiceAccount
10      containers:
11        - name: payments-api
12          image: my-payments-api:latest
13          # AWS SDK automatically discovers credentials via:
14          # AWS_WEB_IDENTITY_TOKEN_FILE + AWS_ROLE_ARN env vars (injected by the webhook)

IRSA vs EKS Pod Identity

AWS released EKS Pod Identity in late 2023, a simpler alternative to IRSA. Key differences:

IRSAEKS Pod Identity
SetupOIDC provider + trust policy + SA annotationEKS Pod Identity association (no OIDC setup)
Trust policyMust specify OIDC provider URL + sub conditionTrust principal is pods.eks.amazonaws.com
Cross-accountWorks nativelyRequires additional configuration
SDK requirementAny AWS SDK with web identity supportAny AWS SDK with container credentials provider support (AWS_CONTAINER_CREDENTIALS_FULL_URI)
AvailabilityAll EKS versionsEKS 1.24+ with the Pod Identity Agent add-on

For new clusters on EKS 1.24+, EKS Pod Identity is simpler to operate. For existing clusters with IRSA configured, there's no urgent reason to migrate.


Cross-Account Role Assumption

A role in account B can be assumed by a principal in account A. The trust policy on the role in account B must allow the principal from account A:

json
1{
2  "Version": "2012-10-17",
3  "Statement": [
4    {
5      "Effect": "Allow",
6      "Principal": {
7        "AWS": "arn:aws:iam::111111111111:role/DeploymentRole"
8      },
9      "Action": "sts:AssumeRole",
10      "Condition": {
11        "StringEquals": {
12          "sts:ExternalId": "unique-external-id-for-this-relationship"
13        }
14      }
15    }
16  ]
17}

The ExternalId condition prevents the confused deputy problem: if a third-party SaaS has your account number and knows your role ARN, they can't assume it without the external ID that only your account knows.

The principal in account A also needs permission to call sts:AssumeRole:

json
{
  "Effect": "Allow",
  "Action": "sts:AssumeRole",
  "Resource": "arn:aws:iam::222222222222:role/CrossAccountRole"
}

Role Chaining Limit

You can assume a role that itself assumes another role, but STS caps the session duration for chained assumptions at 1 hour regardless of what MaxSessionDuration says. Design your role chains to avoid requiring long sessions.


Service Control Policies

SCPs apply to all principals in an AWS account or OU. They're the organization-level guardrail — even AdministratorAccess can't exceed what the SCP allows.

Common SCP patterns:

json
1{
2  "Version": "2012-10-17",
3  "Statement": [
4    {
5      "Sid": "DenyLeavingOrganization",
6      "Effect": "Deny",
7      "Action": "organizations:LeaveOrganization",
8      "Resource": "*"
9    },
10    {
11      "Sid": "RequireIMDSv2",
12      "Effect": "Deny",
13      "Action": "ec2:RunInstances",
14      "Resource": "arn:aws:ec2:*:*:instance/*",
15      "Condition": {
16        "StringNotEquals": {
17          "ec2:MetadataHttpTokens": "required"
18        }
19      }
20    },
21    {
22      "Sid": "DenyRegionsOutsideUS",
23      "Effect": "Deny",
24      "NotAction": [
25        "iam:*",
26        "sts:*",
27        "cloudfront:*",
28        "route53:*",
29        "route53resolver:*",
30        "support:*",
31        "budgets:*",
32        "organizations:*",
33        "account:*",
34        "health:*",
35        "globalaccelerator:*",
36        "waf:*",
37        "trustedadvisor:*"
38      ],
39      "Resource": "*",
40      "Condition": {
41        "StringNotEquals": {
42          "aws:RequestedRegion": ["us-east-1", "us-west-2"]
43        }
44      }
45    }
46  ]
47}

SCPs use NotAction rather than Action for region restriction — IAM and global services don't have a region and would otherwise be blocked. The NotAction in the deny means "deny everything except these global services."


IAM Access Analyzer

IAM Access Analyzer identifies resources shared with external principals — intentional or not. It analyzes resource-based policies on S3 buckets, IAM roles, KMS keys, SQS queues, Lambda functions, and Secrets Manager secrets.

bash
1# Enable Access Analyzer for the account (one per region)
2aws accessanalyzer create-analyzer \
3  --analyzer-name my-account-analyzer \
4  --type ACCOUNT    # Or ORGANIZATION for org-wide analysis
5
6# List findings
7aws accessanalyzer list-findings \
8  --analyzer-arn arn:aws:access-analyzer:us-east-1:012345678901:analyzer/my-account-analyzer
9
10# Archive a finding (acknowledge it's intentional)
11aws accessanalyzer update-findings \
12  --analyzer-arn arn:aws:access-analyzer:us-east-1:012345678901:analyzer/my-account-analyzer \
13  --status ARCHIVED \
14  --ids "finding-id-here"

Access Analyzer also validates IAM policies and generates least-privilege policies from CloudTrail events:

bash
# Generate a least-privilege policy based on actual CloudTrail usage
aws accessanalyzer start-policy-generation \
  --policy-generation-details '{"principalArn": "arn:aws:iam::012345678901:role/MyRole"}' \
  --cloud-trail-details '{"trails": [{"allRegions": true, "cloudTrailArn": "arn:aws:cloudtrail:us-east-1:012345678901:trail/my-trail"}], "startTime": "2026-01-01T00:00:00Z", "endTime": "2026-02-01T00:00:00Z"}'

This is the practical path to least-privilege: deploy with broad permissions, observe what's actually used in CloudTrail, generate a tighter policy, and iterate.


Frequently Asked Questions

What's the difference between an IAM role and an instance profile?

An IAM role is the set of policies and trust relationships. An instance profile is a container that holds exactly one IAM role and can be attached to an EC2 instance. When you launch an EC2 instance with an IAM role via the console, it automatically creates an instance profile with the same name. Via the CLI and CloudFormation, you must create the instance profile explicitly:

bash
aws iam create-instance-profile --instance-profile-name MyInstanceProfile
aws iam add-role-to-instance-profile --instance-profile-name MyInstanceProfile --role-name MyRole
aws ec2 run-instances ... --iam-instance-profile Name=MyInstanceProfile

Why do my EKS pods get 403 errors even though IRSA is configured?

Common causes in order of frequency:

  1. Service account name mismatch: the trust policy's sub condition has a typo in namespace or service account name. Verify: kubectl get sa <name> -n <namespace> -o yaml — check the annotation ARN matches the role ARN.

  2. Missing aud condition or wrong value: the token audience must match sts.amazonaws.com. Some older IRSA setups use the cluster OIDC URL as the audience — check the trust policy's aud condition.

  3. OIDC provider thumbprint expired: the OIDC provider thumbprint is used to validate the EKS token. AWS rotates intermediate CAs periodically — update the thumbprint if tokens suddenly fail.

  4. Incorrect region: the role is in us-east-1 but the pod is calling an endpoint in another region. The STS endpoint needs to be regional: set AWS_STS_REGIONAL_ENDPOINTS=regional to use the regional STS endpoint.

How do I audit what a role can actually do?

IAM Policy Simulator evaluates policies without making real API calls:

bash
aws iam simulate-principal-policy \
  --policy-source-arn arn:aws:iam::012345678901:role/PaymentsApiRole \
  --action-names s3:GetObject s3:PutObject s3:DeleteObject \
  --resource-arns arn:aws:s3:::my-bucket/*

The output shows allowed or implicitDeny/explicitDeny for each action. For complex multi-account scenarios with SCPs and permission boundaries, the simulator accounts for all layers.

What's the maximum session duration for an assumed role?

The default session duration is 1 hour. You can set it up to 12 hours on the role with --max-session-duration 43200 (seconds). However, role chaining caps any chained session at 1 hour regardless of this setting.

For long-running processes (CodeBuild builds, ECS tasks), use the role's session refresh — the AWS SDK automatically refreshes credentials before they expire if the application uses the credential provider chain correctly.


For Kubernetes-native secrets that complement IRSA-scoped AWS Secrets Manager access, see External Secrets Operator: Syncing AWS Secrets Manager to Kubernetes. For EKS-specific VPC networking that security groups and NACLs secure, see AWS VPC Design for EKS: Subnets, NAT, and Security Groups.

Implementing IRSA for a multi-team EKS cluster, designing a least-privilege IAM structure across multiple AWS accounts, or investigating IAM permission escalation paths before a security audit? Talk to us at Coding Protocols — we help platform teams design IAM architectures that are secure without becoming an operational bottleneck.

Related Topics

AWS
IAM
IRSA
EKS
Security
Platform Engineering

Read Next