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
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.
Design rules:
- Map job function to permission sets, not individuals to admin.
- Prefer
ReadOnlyAccessor custom read sets for most engineers; reserve power access for break-glass workflows. - Use account assignments so
DevelopersgetPowerUserAccessindevbutReadOnlyAccessinprod. - 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 → Permission sets; 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
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"
}
}
}
]
}
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"
}
}
}
]
}
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
}
}
}]
})
}
Design rules:
- One deployment role per environment (
deployment-role-prod,deployment-role-staging). - CI authenticates to the tooling account first (prefer OIDC federation, not stored keys).
- Pipeline assumes the workload role immediately before deploy steps.
- Add
aws:PrincipalAccountif 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.
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"
}
}
}
]
}
Attach a read-only policy (SecurityAudit or tighter custom read).
Design rules:
- Security account is for read and detect, not daily deploys.
- Prefer organization-level service delegation (GuardDuty, Security Hub) where available instead of hundreds of custom trust relationships.
- 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:
- Identity policy on the role in account A (what the role is allowed to request).
- Resource policy on the bucket or KMS key in account B (what the resource allows).
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/*"
}
]
}
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/*"
}
]
}
For KMS, include kms:Encrypt, kms:GenerateDataKey, and a key policy that names the same principal.
Design rules:
- Name the exact role ARN in the resource policy; avoid account-root principals unless you truly mean the entire account.
- Scope to prefix (
uploads/*) or specific key aliases. - 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.
{
"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"
}
}
}
]
}
Design rules:
- One role per vendor integration; never reuse a vendor role for internal automation.
- Rotate or revoke ExternalId when offboarding the vendor.
- Prefer vendor-specific managed policies over
ReadOnlyAccessif 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).
| 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": "*"
}
]
}
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":
- No long-lived IAM user keys in workload or tooling accounts for automation.
- Every cross-account trust policy names a specific principal ARN (or Identity Center pattern), not
*. -
aws:PrincipalOrgID(oraws:PrincipalAccount) appears on cross-account trust where possible. - CloudTrail logs show who assumed which role and which API calls followed.
- A negative test fails as expected (for example, staging deploy role cannot assume production role).
- 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
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
- Account vending without role standards: New accounts get bespoke admin users because there is no template for trust policies.
-
Role chaining depth: Chaining multiple
AssumeRolecalls hits session limits and makes debugging painful; keep chains shallow. - Overloaded deployment roles: One role deploys EC2, edits IAM, and reads Secrets Manager; incident response becomes guesswork.
-
SCP vs IAM confusion: Teams think an IAM
Allowfixes an SCPDeny; it does not. - 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:PrincipalOrgIDstops a surprising amount of cross-account trust mistakes. -
Readable role names (
deployment-role-prod,security-read-member) beat genericcross-account-rolewhen auditors and on-call engineers ask questions at 2 a.m.





















