Cross-Account IAM Roles: Grant Dev Account Access to Prod S3 Resources

Running separate AWS accounts for dev and prod is a security best practice — but it creates a real operational challenge: how do you let a developer in the dev account securely access resources in the prod account without embedding long-lived credentials? The answer is cross-account IAM role assumption, and it's the AWS-native, credential-free solution to this exact problem.

TL;DR

StepAccountAction
1Prod AccountCreate an IAM Role with a trust policy allowing the dev account to assume it
2Prod AccountAttach an S3 permission policy to that role
3Dev AccountGrant the developer's IAM identity permission to call sts:AssumeRole
4DeveloperCall aws sts assume-role to receive temporary credentials scoped to the prod role

How Cross-Account Role Assumption Works

AWS Security Token Service (STS) is the engine behind this pattern. When a principal in Account A calls sts:AssumeRole targeting a role ARN in Account B, STS validates two things simultaneously:

  • Trust Policy (Account B side): Does the role in Account B explicitly trust Account A (or a specific principal in Account A)?
  • Permission Policy (Account A side): Does the calling principal in Account A have permission to call sts:AssumeRole for that specific role ARN?

Both gates must pass. If either fails, the assumption is denied. This dual-validation is what makes cross-account access secure by default.

sequenceDiagram participant Dev as Developer (Dev Account) participant STS as AWS STS participant ProdRole as ProdS3ReadRole (Prod Account) participant S3 as S3 Bucket (Prod Account) Dev->>STS: AssumeRole (role ARN in prod) STS->>STS: Check dev IAM policy allows sts:AssumeRole STS->>ProdRole: Check trust policy allows dev account ProdRole-->>STS: Trust policy validated STS-->>Dev: Temporary credentials (expires automatically) Dev->>S3: s3:GetObject using temp credentials S3->>S3: Evaluate role permission policy S3-->>Dev: Object data returned
  1. Developer in the dev account calls sts:AssumeRole targeting the prod role ARN.
  2. STS checks the dev account IAM policy — does this identity have sts:AssumeRole permission for this ARN?
  3. STS checks the prod role's trust policy — does it trust the dev account principal?
  4. Both checks pass → STS issues short-lived temporary credentials (Access Key ID, Secret Access Key, Session Token).
  5. Developer uses those temporary credentials to call S3 APIs in the prod account.
  6. S3 evaluates the role's permission policy and serves (or denies) the request.
Analogy: Think of it like a contractor badge system. The prod account is a secure building. The cross-account role is a visitor badge locked in a cabinet. The dev account's IAM policy is the authorization letter that lets the contractor request the badge. STS is the front desk — it checks both the letter and the cabinet's approved visitor list before handing over the badge. The badge (temporary credentials) expires automatically.

Step-by-Step Implementation

Prerequisites

  • Prod Account ID: 111111111111 (replace with your actual prod account ID)
  • Dev Account ID: 222222222222 (replace with your actual dev account ID)
  • Target S3 Bucket: my-prod-data-bucket
  • Dev IAM User/Role ARN: arn:aws:iam::222222222222:user/alice

Step 1 — Create the Cross-Account Role in the Prod Account

In the prod account, create an IAM role named ProdS3ReadRole. The trust policy below grants the entire dev account the ability to assume this role. For tighter security, you can scope it to a specific IAM user or role ARN instead of the root account principal.

🔽 Trust Policy — trust-policy.json (Prod Account)
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Principal": {
        "AWS": "arn:aws:iam::222222222222:root"
      },
      "Action": "sts:AssumeRole",
      "Condition": {
        "BoolIfExists": {
          "aws:MultiFactorAuthPresent": "true"
        }
      }
    }
  ]
}

Create the role via AWS CLI (run in prod account context):

aws iam create-role \
  --role-name ProdS3ReadRole \
  --assume-role-policy-document file://trust-policy.json \
  --description "Allows dev account principals to read prod S3" \
  --profile prod-admin

Step 2 — Attach an S3 Permission Policy to the Role (Prod Account)

Create a least-privilege permission policy that grants only the S3 actions your developers need. The example below grants read-only access to a specific bucket.

🔽 Permission Policy — s3-read-policy.json (Prod Account)
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Sid": "AllowS3ListBucket",
      "Effect": "Allow",
      "Action": "s3:ListBucket",
      "Resource": "arn:aws:s3:::my-prod-data-bucket"
    },
    {
      "Sid": "AllowS3GetObject",
      "Effect": "Allow",
      "Action": "s3:GetObject",
      "Resource": "arn:aws:s3:::my-prod-data-bucket/*"
    }
  ]
}
# Create the inline/managed policy and attach it to the role
aws iam put-role-policy \
  --role-name ProdS3ReadRole \
  --policy-name S3ReadOnlyAccess \
  --policy-document file://s3-read-policy.json \
  --profile prod-admin

Note the role ARN for the next step:

aws iam get-role \
  --role-name ProdS3ReadRole \
  --query "Role.Arn" \
  --output text \
  --profile prod-admin

# Output: arn:aws:iam::111111111111:role/ProdS3ReadRole

Step 3 — Grant the Developer Permission to Assume the Role (Dev Account)

In the dev account, attach a policy to the developer's IAM user or role that explicitly allows sts:AssumeRole for the prod role ARN. Without this, even if the trust policy allows it, the call will be denied.

🔽 AssumeRole Policy — assume-prod-role-policy.json (Dev Account)
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Sid": "AllowAssumeProdS3Role",
      "Effect": "Allow",
      "Action": "sts:AssumeRole",
      "Resource": "arn:aws:iam::111111111111:role/ProdS3ReadRole"
    }
  ]
}
# Attach the policy to the developer's IAM user in the dev account
aws iam put-user-policy \
  --user-name alice \
  --policy-name AssumeProdS3Role \
  --policy-document file://assume-prod-role-policy.json \
  --profile dev-admin

Step 4 — Assume the Role and Access S3 (Developer Action)

The developer now assumes the prod role to receive temporary credentials, then uses those credentials to interact with the prod S3 bucket.

Option A: Manual STS call

# Assume the role — returns temporary credentials
aws sts assume-role \
  --role-arn "arn:aws:iam::111111111111:role/ProdS3ReadRole" \
  --role-session-name "alice-prod-session" \
  --profile dev-alice

# Export the returned credentials as environment variables
export AWS_ACCESS_KEY_ID="<AccessKeyId from output>"
export AWS_SECRET_ACCESS_KEY="<SecretAccessKey from output>"
export AWS_SESSION_TOKEN="<SessionToken from output>"

# Now access the prod S3 bucket using the assumed role credentials
aws s3 ls s3://my-prod-data-bucket/

Option B: AWS CLI Named Profile (Recommended for developer workflow)

Add the following to ~/.aws/config to let the CLI handle role assumption automatically:

[profile prod-s3-access]
role_arn = arn:aws:iam::111111111111:role/ProdS3ReadRole
source_profile = dev-alice
role_session_name = alice-prod-session
mfa_serial = arn:aws:iam::222222222222:mfa/alice
# Use the profile directly — CLI handles assume-role transparently
aws s3 ls s3://my-prod-data-bucket/ --profile prod-s3-access

Architecture Overview

graph LR subgraph DevAccount [Dev Account 222222222222] Alice[IAM User: alice] DevPolicy[Policy: sts:AssumeRole on ProdS3ReadRole] Alice --> DevPolicy end subgraph STSBroker [AWS STS] STS[Validates both sides] end subgraph ProdAccount [Prod Account 111111111111] ProdRole[IAM Role: ProdS3ReadRole] TrustPolicy[Trust Policy: allows dev account] PermPolicy[Permission Policy: s3:GetObject s3:ListBucket] S3Bucket[S3: my-prod-data-bucket] ProdRole --> TrustPolicy ProdRole --> PermPolicy PermPolicy --> S3Bucket end DevPolicy -->|sts:AssumeRole request| STS STS -->|validates trust policy| ProdRole STS -->|issues temp credentials| Alice Alice -->|accesses with temp creds| S3Bucket
  1. The Dev Account contains Alice's IAM user with an sts:AssumeRole permission policy scoped to the prod role ARN.
  2. AWS STS acts as the cross-account broker — it validates both the permission policy (dev side) and the trust policy (prod side).
  3. The Prod Account hosts the ProdS3ReadRole with a trust policy allowing the dev account and a permission policy scoped to the specific S3 bucket.
  4. Temporary credentials issued by STS are short-lived and automatically expire, eliminating the risk of long-lived credential leakage.

Security Hardening Checklist

ControlImplementationWhy It Matters
Require MFAAdd aws:MultiFactorAuthPresent: true condition to trust policyPrevents credential theft from enabling role assumption
Scope the PrincipalUse a specific IAM user/role ARN instead of :root in the trust policyLimits blast radius if dev account is compromised
Least Privilege S3 PolicyGrant only the specific S3 actions and bucket paths neededPrevents accidental or malicious data modification
Use External IDAdd sts:ExternalId condition for third-party or automated accessMitigates the confused deputy problem
Enable CloudTrailEnsure CloudTrail is active in both accountsProvides full audit trail of AssumeRole calls and S3 access

Common Pitfalls

  • Missing the dev-side permission: The trust policy alone is not enough. The calling principal in the dev account must also have an explicit sts:AssumeRole allow policy. Forgetting this is the #1 cause of AccessDenied errors.
  • S3 Bucket Policy conflicts: If the prod S3 bucket has an explicit Deny statement or restricts access to specific principals, the role's permission policy alone won't be sufficient. Verify the bucket policy does not block the assumed role's ARN.
  • Region confusion: STS is a global service but has regional endpoints. IAM roles are global resources. S3 bucket names are global but data is stored regionally — ensure your CLI is targeting the correct region when accessing S3.
  • Session duration: The default and maximum session duration for an assumed role is configurable on the role itself. If your workflow requires longer sessions, adjust the MaxSessionDuration on the role in the prod account.

Glossary

TermDefinition
Trust PolicyA resource-based policy attached to an IAM role that defines which principals are allowed to assume it.
Permission PolicyAn identity-based or inline policy that defines what AWS actions a principal (or assumed role) is allowed to perform.
AWS STS (Security Token Service)The AWS service that issues short-lived, temporary security credentials for assumed roles.
Role SessionA temporary identity created when a role is assumed, identified by the role ARN and a session name.
Confused DeputyA security vulnerability where a trusted service is tricked into performing actions on behalf of an unauthorized party; mitigated with External ID conditions.

Next Steps

  • For automated pipelines (CI/CD), use IAM Roles for the build agent instead of IAM users — the same cross-account pattern applies.
  • At scale, consider AWS IAM Identity Center (SSO) for centralized permission sets across multiple accounts instead of managing individual cross-account roles.
  • Review the official documentation: IAM Tutorial: Delegate access across AWS accounts using IAM roles.

Related Posts

Comments

Popular posts from this blog

EC2 No Internet Access in Custom VPC: Fix Internet Gateway and Route Table

EC2 SSH Connection Timeout: Which Security Group Rules to Check

Difference Between IAM User and IAM Role: Which One Should Your EC2 Use?