Cross-account access
Cross-account role access: trust and caller policies
Let one role in 444455556666 assume a role in 111122223333: the trust policy, the caller's sts:AssumeRole grant, and how to test each side.
Published · Updated .
The problem
A deployment pipeline runs as the role DeployPipeline in a tooling account, 444455556666. It has to deploy into a workload account, 111122223333, where a role named DeploymentTarget holds the deployment permissions. No access keys should cross the account line: the pipeline should trade its own role session for short-lived DeploymentTarget credentials through AWS STS.
That is a cross-account request, so both accounts must allow it. The trust policy on DeploymentTarget says which principals the workload account accepts. An identity policy on DeployPipeline says which roles the tooling account lets it assume. Most broken setups have one half missing or pointing at a slightly different ARN, so this page shows both with the same role names and tests each separately.
The policies
Trust policy on DeploymentTarget (account 111122223333)
IAM role trust policy. Attach to: The trust policy (assume role policy document) of the role DeploymentTarget in account 111122223333
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "TrustDeployPipelineRole",
"Effect": "Allow",
"Principal": {
"AWS": "arn:aws:iam::444455556666:role/DeployPipeline"
},
"Action": "sts:AssumeRole"
}
]
}The form generators on this site don't edit trust policy documents; copy it, then check it with IAM Access Analyzer as shown below.
Statement by statement
- TrustDeployPipelineRole names one role, not the account. A principal of arn:aws:iam::444455556666:root, or the bare account ID, delegates to the whole account: any principal there whose own policies allow sts:AssumeRole on this role can use it. The root ARN does not mean only the root user. IAM stores the named role's unique ID, so if DeployPipeline is deleted and recreated, the trust stops matching and shows an ID beginning with AROA instead of the ARN. If both accounts are in your organization, a StringEquals condition on aws:PrincipalOrgID (o-exampleorgid) ends access if the tooling account leaves it. sts:ExternalId is for third parties that serve many customers; you own both accounts here.
Identity policy on DeployPipeline (account 444455556666)
IAM identity-based policy. Attach to: An inline or customer managed policy on the role DeployPipeline in account 444455556666
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "AssumeDeploymentTargetOnly",
"Effect": "Allow",
"Action": "sts:AssumeRole",
"Resource": "arn:aws:iam::111122223333:role/DeploymentTarget"
}
]
}Statement by statement
- AssumeDeploymentTargetOnly allows one action on one role ARN. Resource "*" would let the pipeline assume any role, in any account, whose trust policy accepts it, including every role that trusts the whole 444455556666 account. Add further targets by ARN. The ARN must match exactly: role names in ARNs are case-sensitive, and a role created with --path /deploy/ is role/deploy/DeploymentTarget.
Before you start
- IAM administrator access in both accounts, or two people who have it. Each half is configured in its own account.
- The role DeployPipeline already exists in 444455556666. IAM checks role principals when you save a trust policy, and an ARN for a role that does not exist is typically rejected with an 'Invalid principal in policy' error.
- AWS CLI v2 with credentials for DeployPipeline, for the checks below.
Deploy it step by step
- Create DeploymentTarget with the trust policy. In 111122223333, create the role from trust-policy.json. The trust policy only controls who can become the role; attach its permissions separately.
Commands (replace <placeholders>) aws iam create-role --role-name DeploymentTarget --assume-role-policy-document file://trust-policy.json --max-session-duration 3600 aws iam attach-role-policy --role-name DeploymentTarget --policy-arn arn:aws:iam::111122223333:policy/<deployment-permissions-policy> - Grant DeployPipeline permission to assume it. In 444455556666, add the caller policy to DeployPipeline. It is the half people forget when the workload account's team does the setup.
Commands (replace <placeholders>) aws iam put-role-policy --role-name DeployPipeline --policy-name AssumeDeploymentTarget --policy-document file://caller-permission.json - Point the pipeline at the target role. Let the CLI or SDK assume the role from a profile. Set credential_source to where the pipeline runs (Environment, Ec2InstanceMetadata or EcsContainer), or use source_profile. A fixed session name makes the workload account's CloudTrail entries easy to attribute.
Commands (replace <placeholders>) aws configure set profile.workload-deploy.role_arn arn:aws:iam::111122223333:role/DeploymentTarget aws configure set profile.workload-deploy.credential_source Ec2InstanceMetadata aws configure set profile.workload-deploy.role_session_name pipeline-deploy - Plan for the one-hour session limit. DurationSeconds runs from 900 seconds up to the role's maximum session duration (1 to 12 hours). Here a longer maximum doesn't help: DeployPipeline is itself a role, so assuming DeploymentTarget from its session is role chaining, which AWS limits to one hour regardless of the role's setting. Requests for more fail. Jobs longer than an hour must assume the role again; profile-based CLI credentials are refreshed for you, a single assume-role call in a script is not.
Verify it without changing anything
These checks only read state, simulate, or use dry runs, so they are safe to run in any account. Exact error wording varies by service and over time.
Validate both documents
Run Access Analyzer validation on each file.
aws accessanalyzer validate-policy --policy-type RESOURCE_POLICY --validate-policy-resource-type AWS::IAM::AssumeRolePolicyDocument --policy-document file://trust-policy.json
aws accessanalyzer validate-policy --policy-type IDENTITY_POLICY --policy-document file://caller-permission.jsonExpected: No findings, or only SUGGESTION findings; fix any ERROR or SECURITY_WARNING. Passing shows the documents are well formed, not that the assumption will succeed.
Simulate the caller side
In 444455556666, test DeployPipeline's own policies against the target and against a role it should not reach.
aws iam simulate-principal-policy --policy-source-arn arn:aws:iam::444455556666:role/DeployPipeline --action-names sts:AssumeRole --resource-arns arn:aws:iam::111122223333:role/DeploymentTarget --query 'EvaluationResults[].EvalDecision'
aws iam simulate-principal-policy --policy-source-arn arn:aws:iam::444455556666:role/DeployPipeline --action-names sts:AssumeRole --resource-arns arn:aws:iam::111122223333:role/<some-other-role> --query 'EvaluationResults[].EvalDecision'Expected: allowed for DeploymentTarget, implicitDeny for the other role. The simulator cannot evaluate a role's trust policy, so 'allowed' says nothing about the workload account's half.
Assume the role for real
From the pipeline's environment, confirm who you are, assume the role, then check the profile from step 3.
aws sts get-caller-identity
aws sts assume-role --role-arn arn:aws:iam::111122223333:role/DeploymentTarget --role-session-name deploy-check --query AssumedRoleUser.Arn
aws sts get-caller-identity --profile workload-deployExpected: First, an assumed-role ARN for DeployPipeline in 444455556666; any other role means the pipeline runs as something else. assume-role returns arn:aws:sts::111122223333:assumed-role/DeploymentTarget/deploy-check. The profile call reports Account 111122223333 and DeploymentTarget with session name pipeline-deploy.
Confirm the chaining limit before you depend on it
Request a two-hour session as the pipeline role. The failure is harmless and shows what a long job will hit.
aws sts assume-role --role-arn arn:aws:iam::111122223333:role/DeploymentTarget --role-session-name duration-check --duration-seconds 7200Expected: A validation error saying DurationSeconds exceeds the allowed maximum; wording varies. If it succeeds, the caller is not a role session and the role allows longer sessions.
Read the trust policy back in the workload account
In 111122223333, look at what IAM actually stored.
aws iam get-role --role-name DeploymentTarget --query 'Role.[AssumeRolePolicyDocument,MaxSessionDuration,RoleLastUsed]'Expected: The trust policy naming arn:aws:iam::444455556666:role/DeployPipeline, MaxSessionDuration 3600 and, after a successful assumption, a RoleLastUsed date, which can lag. An AROA ID instead of the ARN means the source role was recreated: save the policy again with the ARN.
Common mistakes and troubleshooting
Not reading which side denied the call
In AWS's documented formats, 'because no identity-based policy allows the sts:AssumeRole action' points at DeployPipeline's policies, and 'because no role trust policy allows the sts:AssumeRole action' points at DeploymentTarget. 'explicit deny' points at a Deny, a boundary or an organization policy. AWS names only one policy type even when several deny, and some messages carry no context. A wrong path or role-name case fails the same way, so copy ARNs from get-role output.
Guardrails on either side
An SCP on the tooling account, or a permissions boundary on DeployPipeline that does not allow sts:AssumeRole, blocks the call even though the identity policy allows it. AWS STS supports resource control policies, so an RCP covering the workload account can block it too.
Looking for denied attempts in the wrong account
CloudTrail does not record denied cross-account AssumeRole calls in the target account. Look in 444455556666's CloudTrail; successful calls appear in both.
Limitations and production considerations
- Anyone who can act as DeployPipeline can become DeploymentTarget, so protecting the pipeline's build definitions and runners protects the workload account.
- After assumption, the session's permissions come only from DeploymentTarget's policies and any session policy passed on the call.
- Trusting a role ARN ties access to that role's lifecycle. Recreating the source role breaks access silently: safe, but it looks like an outage.
Official AWS documentation
Account IDs, bucket and organization names are AWS documentation placeholders. Test in a non-production account before use (see the methodology).