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
| Step | Account | Action |
|---|---|---|
| 1 | Prod Account | Create an IAM Role with a trust policy allowing the dev account to assume it |
| 2 | Prod Account | Attach an S3 permission policy to that role |
| 3 | Dev Account | Grant the developer's IAM identity permission to call sts:AssumeRole |
| 4 | Developer | Call 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:AssumeRolefor 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.
- Developer in the dev account calls
sts:AssumeRoletargeting the prod role ARN. - STS checks the dev account IAM policy — does this identity have
sts:AssumeRolepermission for this ARN? - STS checks the prod role's trust policy — does it trust the dev account principal?
- Both checks pass → STS issues short-lived temporary credentials (Access Key ID, Secret Access Key, Session Token).
- Developer uses those temporary credentials to call S3 APIs in the prod account.
- 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
- The Dev Account contains Alice's IAM user with an
sts:AssumeRolepermission policy scoped to the prod role ARN. - AWS STS acts as the cross-account broker — it validates both the permission policy (dev side) and the trust policy (prod side).
- The Prod Account hosts the
ProdS3ReadRolewith a trust policy allowing the dev account and a permission policy scoped to the specific S3 bucket. - Temporary credentials issued by STS are short-lived and automatically expire, eliminating the risk of long-lived credential leakage.
Security Hardening Checklist
| Control | Implementation | Why It Matters |
|---|---|---|
| Require MFA | Add aws:MultiFactorAuthPresent: true condition to trust policy | Prevents credential theft from enabling role assumption |
| Scope the Principal | Use a specific IAM user/role ARN instead of :root in the trust policy | Limits blast radius if dev account is compromised |
| Least Privilege S3 Policy | Grant only the specific S3 actions and bucket paths needed | Prevents accidental or malicious data modification |
| Use External ID | Add sts:ExternalId condition for third-party or automated access | Mitigates the confused deputy problem |
| Enable CloudTrail | Ensure CloudTrail is active in both accounts | Provides 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:AssumeRoleallow policy. Forgetting this is the #1 cause ofAccessDeniederrors. - 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
MaxSessionDurationon the role in the prod account.
Glossary
| Term | Definition |
|---|---|
| Trust Policy | A resource-based policy attached to an IAM role that defines which principals are allowed to assume it. |
| Permission Policy | An 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 Session | A temporary identity created when a role is assumed, identified by the role ARN and a session name. |
| Confused Deputy | A 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
- 📄 AWS IAM Policy Structure: Decoding Effect, Action, Resource, and Condition
- 📄 Lambda 403 on S3: Diagnosing & Fixing Execution Role Permissions
- 📄 S3 Access Denied Despite Public Object: How Block Public Access Overrides Object ACLs
- 📄 IAM User vs. IAM Role: Why Your EC2 Instance Should Never Use a User
Comments
Post a Comment