DEV Community

Cover image for How to Design IAM Roles for Multi-Account AWS Environments
Ilyas Rufai
Ilyas Rufai

Posted on • Edited on

How to Design IAM Roles for Multi-Account AWS Environments

Your team starts with one AWS account. Six months later, you have five: production, staging, shared services, security, and a sandbox nobody remembers creating. Someone creates an Admin IAM user in each account. CI/CD stores access keys in three different secret stores. An auditor asks who assumed full access in production last Tuesday, and the answer is a spreadsheet.

Multi-account AWS is a security win, but only if access is role-based, scoped, and observable. Copying single-account IAM habits into an organization creates credential sprawl, unclear blast radius, and reviews that fail the moment someone says "shared admin user."

In this article, you will learn five IAM role design patterns that scale across AWS Organizations, when to use each one, and how to implement them with trust policies, guardrails, and Terraform.

Who this is for: Platform engineers, DevSecOps engineers, and cloud architects building or maturing multi-account AWS environments.

What you will learn: How to design roles for humans, automation, audit, cross-account resources, and third-party access, without defaulting to long-lived keys or duplicated admin policies.

Prerequisites:

  • Basic familiarity with IAM roles and policies
  • An AWS Organization (or plans to adopt one)
  • Optional: Terraform for the implementation snippets

Related work: If you are new to role-based EC2 access in a single account, start with Terraform EC2 and S3 Upload Access.

TL;DR

  • Use IAM Identity Center (successor to AWS SSO) for human access across accounts, not IAM users in every account.
  • Give CI/CD a dedicated role in a tooling account that assumes short-lived deployment roles in workload accounts.
  • Centralize audit with read-only cross-account roles into a security/logging account wired to CloudTrail and AWS Config.
  • For shared resources (S3, KMS), combine resource policies with role trust and do not make roles overly broad to "make it work."
  • Lock the org with Service Control Policies (SCPs); use permission boundaries on custom roles so teams cannot escalate past intent.
  • Every cross-account trust policy should name a specific principal, add conditions (aws:PrincipalOrgID, aws:PrincipalAccount), and be visible in CloudTrail.

Why single-account IAM thinking fails at scale

Habit from one account What breaks in multi-account
IAM users with access keys per engineer Keys multiply; rotation and offboarding become error-prone
One AdministratorAccess role cloned everywhere No separation between prod and sandbox; blast radius is the whole org
CI/CD keys stored per environment Leaked key in staging can affect production if policies are loose
"We'll clean it up later" bucket policies Cross-account access becomes permanent Principal: "*" debt

Key idea: In multi-account AWS, identity lives in one place, authorization is scoped per account and per job function, and guardrails apply above IAM with SCPs.

Reference account model

You do not need a perfect landing zone on day one, but roles make more sense when accounts have intent:

Organization
├── Security OU
│   └── security-audit (log archive, read-only aggregation)
├── Infrastructure OU
│   └── shared-networking (optional)
├── Workloads OU
│   ├── prod
│   ├── staging
│   └── dev
└── Sandbox OU
    └── sandbox
Enter fullscreen mode Exit fullscreen mode

AWS Organizations OU hierarchy with security, workload, and sandbox accounts

Multi-account IAM role patterns across security, tooling, and workload accounts

Each pattern below maps to a different trust direction. Mixing them into one "super role" is the most common design mistake.

Pattern 1 — Human access with IAM Identity Center

Use when: Engineers, operators, or auditors need console or CLI access to multiple accounts.

Do not: Create IAM users in every account and email access keys.

IAM Identity Center issues short-lived credentials via permission sets mapped to groups (for example, PlatformAdmins, Developers, ReadOnlyAuditors). AWS automatically creates corresponding roles in member accounts.

IAM Identity Center human access flow across permission sets and member accounts

Design rules:

  1. Map job function to permission sets, not individuals to admin.
  2. Prefer ReadOnlyAccess or custom read sets for most engineers; reserve power access for break-glass workflows.
  3. Use account assignments so Developers get PowerUserAccess in dev but ReadOnlyAccess in prod.
  4. Enable IAM Identity Center in the management account (or a dedicated identity account if your org design uses one).

Verification signal: A user in the Developers group can open the dev account console but receives AccessDenied when attempting the same action in prod.

IAM Identity Center → Permission sets; predefined
IAM Identity Center Predefined1
IAM Identity Center Predefined2

IAM Identity Center → Permission sets; custom
IAM Identity Center Custom

Pattern 2 — CI/CD deployment role (tooling → workload)

Use when: GitHub Actions, GitLab CI, Jenkins, or CodePipeline deploys infrastructure or applications into workload accounts.

Trust flow:

Tooling account (CI role)
        |
        | sts:AssumeRole
        v
Workload account (deployment role)
        |
        v
Deploy EC2 / ECS / Lambda / Terraform resources
Enter fullscreen mode Exit fullscreen mode

CI/CD deployment role trust flow from tooling account to workload account

Trust policy in the workload account (deployment role)

This role lives in production (or staging) and trusts a specific role in the tooling account, not the whole organization.

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Sid": "AllowToolingDeployRole",
      "Effect": "Allow",
      "Principal": {
        "AWS": "arn:aws:iam::111111111111:role/github-actions-deploy"
      },
      "Action": "sts:AssumeRole",
      "Condition": {
        "StringEquals": {
          "aws:PrincipalOrgID": "o-a1b2c3d4e5"
        }
      }
    }
  ]
}
Enter fullscreen mode Exit fullscreen mode

Permissions policy on the deployment role (example)

Scope to what the pipeline actually does, not AdministratorAccess.

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Sid": "DeployApplicationStack",
      "Effect": "Allow",
      "Action": [
        "ec2:Describe*",
        "ecs:UpdateService",
        "ecs:DescribeServices",
        "iam:PassRole"
      ],
      "Resource": "*",
      "Condition": {
        "StringEquals": {
          "iam:PassedToService": "ecs-tasks.amazonaws.com"
        }
      }
    }
  ]
}
Enter fullscreen mode Exit fullscreen mode

Terraform: workload account role

variable "tooling_account_id" {
  type = string
}

variable "organization_id" {
  type = string
}

variable "tooling_deploy_role_name" {
  type    = string
  default = "github-actions-deploy"
}

resource "aws_iam_role" "workload_deploy" {
  name = "deployment-role"

  assume_role_policy = jsonencode({
    Version = "2012-10-17"
    Statement = [{
      Sid    = "AllowToolingDeployRole"
      Effect = "Allow"
      Principal = {
        AWS = "arn:aws:iam::${var.tooling_account_id}:role/${var.tooling_deploy_role_name}"
      }
      Action = "sts:AssumeRole"
      Condition = {
        StringEquals = {
          "aws:PrincipalOrgID" = var.organization_id
        }
      }
    }]
  })
}
Enter fullscreen mode Exit fullscreen mode

Design rules:

  1. One deployment role per environment (deployment-role-prod, deployment-role-staging).
  2. CI authenticates to the tooling account first (prefer OIDC federation, not stored keys).
  3. Pipeline assumes the workload role immediately before deploy steps.
  4. Add aws:PrincipalAccount if multiple accounts exist in the org and you want extra-tight trust.

Verification signal: CloudTrail shows AssumeRole from the tooling role ARN into the workload deployment role, followed only by expected API calls, no stray iam:CreateUser.

Pattern 3 — Central audit and security read role

Use when: A security account aggregates CloudTrail, AWS Config, GuardDuty, or Security Hub findings from member accounts.

Trust flow: Member accounts expose a read-only role that the security account (or a security automation role) can assume, or use organization-wide services (Security Hub, GuardDuty delegated admin) where applicable.

Central audit read role flow from member accounts to security account

Example member-account role trust:

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Principal": {
        "AWS": "arn:aws:iam::999999999999:role/security-read-automation"
      },
      "Action": "sts:AssumeRole",
      "Condition": {
        "StringEquals": {
          "aws:PrincipalOrgID": "o-a1b2c3d4e5"
        }
      }
    }
  ]
}
Enter fullscreen mode Exit fullscreen mode

Attach a read-only policy (SecurityAudit or tighter custom read).

Design rules:

  1. Security account is for read and detect, not daily deploys.
  2. Prefer organization-level service delegation (GuardDuty, Security Hub) where available instead of hundreds of custom trust relationships.
  3. Log every cross-account assumption; alert on unexpected principals.

Verification signal: A security account can describe Config rules in member accounts but cannot ec2:TerminateInstances.

Pattern 4 — Cross-account resource access (S3 and KMS)

Use when: A workload in account A must read/write a bucket or key in account B (logs, artifacts, shared datasets).

Do not: Give the compute role in account A s3:* on * and call it done.

You need both sides:

  1. Identity policy on the role in account A (what the role is allowed to request).
  2. Resource policy on the bucket or KMS key in account B (what the resource allows).

Cross-account S3 access requiring identity policy and resource policy on both sides

S3 bucket policy (account B)

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Sid": "AllowWorkloadRoleUpload",
      "Effect": "Allow",
      "Principal": {
        "AWS": "arn:aws:iam::222222222222:role/app-upload-role"
      },
      "Action": [
        "s3:PutObject",
        "s3:AbortMultipartUpload"
      ],
      "Resource": "arn:aws:s3:::shared-artifacts-prod/uploads/*"
    }
  ]
}
Enter fullscreen mode Exit fullscreen mode

Identity policy (account A, on app-upload-role)

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Sid": "WriteSharedArtifacts",
      "Effect": "Allow",
      "Action": [
        "s3:PutObject",
        "s3:AbortMultipartUpload"
      ],
      "Resource": "arn:aws:s3:::shared-artifacts-prod/uploads/*"
    }
  ]
}
Enter fullscreen mode Exit fullscreen mode

For KMS, include kms:Encrypt, kms:GenerateDataKey, and a key policy that names the same principal.

Design rules:

  1. Name the exact role ARN in the resource policy; avoid account-root principals unless you truly mean the entire account.
  2. Scope to prefix (uploads/*) or specific key aliases.
  3. Prefer moving the resource closer to the consumer account if daily cross-account access is heavy.

Verification signal: Upload succeeds from the app role; s3:DeleteObject or access from a different role fails with AccessDenied.

Pattern 5 — Third-party and vendor access with ExternalId

Use when: A SaaS vendor, partner, or integration must assume a role in your account (monitoring, backup, security scanning).

Always require sts:ExternalId in the trust policy to mitigate confused deputy problems.

Third-party vendor assume role flow with ExternalId condition

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Principal": {
        "AWS": "arn:aws:iam::333333333333:root"
      },
      "Action": "sts:AssumeRole",
      "Condition": {
        "StringEquals": {
          "sts:ExternalId": "your-unique-external-id-from-vendor"
        }
      }
    }
  ]
}
Enter fullscreen mode Exit fullscreen mode

Design rules:

  1. One role per vendor integration; never reuse a vendor role for internal automation.
  2. Rotate or revoke ExternalId when offboarding the vendor.
  3. Prefer vendor-specific managed policies over ReadOnlyAccess if the vendor documents minimum requirements.

SCPs and permission boundaries: guardrails above IAM

IAM roles express intent. SCPs express organizational maximums. Permission boundaries cap what a role can ultimately grant, even if someone attaches a broader policy (by mistake).

IAM guardrails layered with allow policies, permission boundaries, and SCP deny ceiling

Control What it does Example
SCP Org-wide deny ceiling Deny all actions outside af-south-1 and eu-west-1
Permission boundary Max permissions for a custom role Allow EC2 and ECS, deny IAM and Organizations
Session policy One-time session scoping CLI assume-role with a tighter inline session policy

A common SCP pattern for workload accounts:

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Sid": "DenyIAMUserAndAccessKeys",
      "Effect": "Deny",
      "Action": [
        "iam:CreateUser",
        "iam:CreateAccessKey"
      ],
      "Resource": "*"
    }
  ]
}
Enter fullscreen mode Exit fullscreen mode

SCPs do not grant access; instead, they filter it. IAM roles still need explicit allow policies.

Decision matrix: which pattern when?

Need Pattern Primary mechanism
Engineer console/CLI access Identity Center Permission sets + account assignments
Pipeline deploys to prod CI/CD deployment role Cross-account AssumeRole + OIDC in tooling account
Security team visibility Audit read role Cross-account read + delegated security services
App writes to shared bucket Resource + identity policy S3/KMS policies naming specific role ARNs
Vendor integration Vendor assume-role Trust policy + sts:ExternalId
Prevent IAM users org-wide SCP guardrail Deny iam:CreateUser / CreateAccessKey

How to verify your design

Run this review before calling the model "done":

  1. No long-lived IAM user keys in workload or tooling accounts for automation.
  2. Every cross-account trust policy names a specific principal ARN (or Identity Center pattern), not *.
  3. aws:PrincipalOrgID (or aws:PrincipalAccount) appears on cross-account trust where possible.
  4. CloudTrail logs show who assumed which role and which API calls followed.
  5. A negative test fails as expected (for example, staging deploy role cannot assume production role).
  6. SCP deny rules block the risk you fear most (root usage, unapproved regions, public S3 ACLs).

Quick CLI negative test (deployment roles)

From the tooling role session, attempt to assume a role that you should not reach:

aws sts assume-role \
  --role-arn arn:aws:iam::PROD_ACCOUNT_ID:role/deployment-role \
  --role-session-name deploy-test
Enter fullscreen mode Exit fullscreen mode

Repeat with the allowed workload role and confirm success. Then run a single deploy action and confirm that CloudTrail captures the chain.

When this breaks down

  1. Account vending without role standards: New accounts get bespoke admin users because there is no template for trust policies.
  2. Role chaining depth: Chaining multiple AssumeRole calls hits session limits and makes debugging painful; keep chains shallow.
  3. Overloaded deployment roles: One role deploys EC2, edits IAM, and reads Secrets Manager; incident response becomes guesswork.
  4. SCP vs IAM confusion: Teams think an IAM Allow fixes an SCP Deny; it does not.
  5. Missing break-glass: Identity Center power access with no emergency process leads to shared root usage in outages.

Frequently asked questions

Q: Should I use IAM users at all in member accounts?

For human access, no, use IAM Identity Center. Limited exceptions exist for break-glass automation accounts with strict controls, but that is not the default pattern.

Q: Is AdministratorAccess ever okay for deployment roles?

Rarely in mature environments. Start tight; broaden only with recorded justification. Admin deploy roles turn pipeline compromise into organization compromise.

Q: Do I need a separate tooling account?

Not on day one, but CI/CD roles benefit from isolation. Many teams start with a shared-services account and split later.

Q: How does this relate to AWS Control Tower?

Control Tower encodes opinionated OU and account factory patterns. The role designs here still apply; Control Tower helps enforce account baseline and guardrails.

What I learned designing roles across accounts

  • Trust policies are the real contract between accounts; review them like production code.
  • Conditions are cheap insurance; aws:PrincipalOrgID stops a surprising amount of cross-account trust mistakes.
  • Readable role names (deployment-role-prod, security-read-member) beat generic cross-account-role when auditors and on-call engineers ask questions at 2 a.m.

References

Top comments (0)