Cross-account AWS resource access with AWS CDK


So here is the case: you have S3 buckets, DynamoDB tables, relational tables on several AWS accounts and want to share the data with other AWS accounts. To create a data lake for example. And you are not using the AWS Lake Formation, which provides cross account usage out of the box.

Or, you are in the middle of a migration from accounts.

Or, you want to have communication over multiple accounts, for example an API Gateway which needs to be used by different lambdas over multiple accounts.

Or, you want to deploy with code pipeline to multiple environments which exists in multiple accounts.

Where to start

Although AWS describes this topic quit nicely, I want to demonstrate how to do it with CDK.

For this example we will have two accounts, the original, source Account ID is 11111 and the new, target Account ID is 22222.There are actually two ways of using resources in cross accounts, namely by identity-based policy and resource-based policy. What is the difference then?

Well, the first (and for us most important) difference is, not all resources do support a resource-based policy. For example, DynamoDB does not, as can be found in these tables right here.

Resource based vs.

With an identity-based policy, you will kind of create a “proxy role” to get to the other account and resources. For a resource-based policy, a policy will be directly attached to the resource itself, where you can attach the account IDs you want to give access to. For an identity-based policy the new account will need to assume a role temporarily, which then only gives permissions for that specific role instead of the original permissions, while a resource-based policy will give both permissions at the same time.

Given that not all resources support identity-based policy, we will provide a solution for both below.


Resource-based policy cross account usage

We are going to add some code in our existing CDK script for the source account (11111):

const bucket = new s3.Bucket(this, 'SourceBucket', {
    bucketName: 'source-bucket'

bucket.addToResourcePolicy(new iam.PolicyStatement({
    actions: ['s3:Get*', 's3:List*'],
    resources: [bucket.arnForObjects('*')],
    principals: [new iam.AccountPrincipal('22222')]

Here we are defining the resource-based policy for the new target account, 22222 for our original bucket, so that it will have direct access. The service in the target account just has to reference the bucket (by arn, most of the times) and it will work! At least, the actions that you gave permissions for. 🙂

The target CDK script could contain the bucket if needed like so:

const bucket = s3.Bucket.fromBucketName(this, 'SourceBucket', 'source-bucket');

But more likely is it, that you will have to use it in your application, for example with the Java S3Client (v2):

var s3Client = S3Client.builder().build();
var getObjectRequest = GetObjectRequest
var response = s3Client.getObject(getObjectRequest);

A bit more advanced approach which you will probably need when you have > 10 accounts that need to use the S3 bucket, is by using AWS Organizations. Because if you will have to add all those accounts separately, you can miss the overview quite fast!

The CDK script will change slightly, but still is easy configurable as long as you are managing your accounts correctly by Organizations:

bucket.addToResourcePolicy(new iam.PolicyStatement({
   actions: ['s3:Get*', 's3:List*'],
   resources: [bucket.arnForObjects('*')],
   principals: [new iam.OrganizationPrincipal('organizationId')]

All accounts under this organizationId will get the access you just defined.

Identity-based cross account usage

If you want to enable cross account usage for DynamoDB for example, we are going to need to use the identity-based policy option. This will involve some more steps than for the resource-based policy.

First, we need to ensure that the original account will allow the new account to perform the action of assuming a role. This is done by creating a role in the 11111 account with a policy to allow this actions. Because we trust the new account fully, we will use 22222:root as principal. You could always put this to a specific user if you want to.

//create role to assume the new principal account to
const role = new iam.Role(this, 'CrossAcountRole', {
    assumedBy: new iam.ArnPrincipal('arn:aws:iam::22222:root'),
    roleName: 'cross-account-role'

//add statement to allow assumerole action for this account
const assumeStatement = new iam.PolicyStatement();

//add under the principal policy

//add statement to allow reading and putting into dynamodb table in original account
const resourceStatement = new iam.PolicyStatement();
resourceStatement.addActions('dynamodb:DescribeTable', 'dynamodb:GetItem', 'dynamodb:PutItem');

//add as new policy

This will create a role with an arn. Keep this arn for the next step.

After this, we can go on to the CDK part of the new account. Here, we need to allow the task, lambda or any computing service to let it assume a role to the original account; the changing to the proxy role. This is done by adding a policy to the related role of the service. Here we need the arn of the role we just created.

//for an ecs task
new ecs.TaskDefinition(this, 'TaskDefinition', {...})
    new iam.PolicyStatement({
        resources: ['arn:aws:iam::111111:role/cross-account-role'],
        actions: ['sts:AssumeRole'],
//for a labmda function
new lambda.Function(this, 'Function', {...})
    new iam.PolicyStatement({
        resources: ['arn:aws:iam::111111:role/cross-account-role'],
        actions: ['sts:AssumeRole'],

In CDK that’s it!

Now, we need to get the credentials correctly set up in the application itself (we are using a Java application as example, which uses AWS SDK v1); meaning that we get temporary credentials (the proxy role) we can use to hook into the original account, instead of the actual account, which will normally happen when you are creating a resource client like for DynamoDB.

When looking into the STSAssumeRoleSessionCredentialsProvider class, it becomes clear we can use this to keep a temporary role for the original account we want. In order to use this, we also need the ARN of the role defined in the original account.

var stsAssumeRoleSessionCredentialsProvider = StsAssumeRoleCredentialsProvider
         .roleSessionName("Name for session role")

And then we need to ensure our client(s) use these credentials.

var amazonDynamoDBClient = DynamoDbClient

After that, we will get a connection between the two accounts. We can still also define a separate client, which connects to a resource in its own account, by not providing the credentials.

That was quite easy! Now your target account has access to resources of the original account, as long as the role exists and the resources are available.