Giving AWS Lambda Cross-Account Access to Resources

By Chris Diggs | April 17, 2018

Scenario:

I have two AWS accounts: Account A and Account B.

In Account A, I have a Lambda function that needs to access resources in Account B.

How do we do this? It turns out, it’s not so hard. In fact, you can define any level of access to the Lambda functions as if they were in the same account.


Basic Walkthrough:

What allows cross-account access is AWS’ STS (Security Token Service). The AWS docs point to how users can use STS to gain temporary access to other AWS accounts. The same concept can be applied to other AWS compute resources - Lambda, EC2, Elastic Beanstalk, etc.

Details:

  1. Since Account A has the Lambda function, we’ll give the Lambda function a role with a Managed Policy that allows sts:AssumeRole.

    • Account A’s Managed Policy:
    {
        "Version": "2012-10-17",
        "Statement": [
            {
                "Effect": "Allow",
                "Action": "sts:AssumeRole",
                "Resource": "arn:aws:iam::XXXXXXXXXXXX:role/account-b-role"
            }
        ]
    }
    • XXXXXXXXXXXX = Account B’s Account Number
    • account-b-role = The role created in Account B (next step)


  2. In Account B, we’ll create a new role (account-b-role) and allow it to be assumed by Account A by creating a Trust Relationship.

    • Account B’s Trust Relationship:
    {
        "Version": "2012-10-17",
        "Statement": [
          {
            "Effect": "Allow",
            "Principal": {
              "AWS": "arn:aws:iam::YYYYYYYYYYYY:root"
            },
            "Action": "sts:AssumeRole",
            "Condition": {}
          }
        ]
    }
    • YYYYYYYYYYYY = Account A’s Account Number
    • You can edit the Trust Relationship directly in the AWS Console after creating the role.


  3. We’ll fine tune the Managed Policy attached to the role in Account B (account-b-role) to limit the access of the Lambda functions.

    • Account B’s Managed Policy:
    {
        "Version": "2012-10-17",
        "Statement": [
            {
                "Action": [
                    "s3:*"
                ],
                "Resource": [
                    "arn:aws:s3:::some-s3-bucket",
                    "arn:aws:s3:::some-s3-bucket/*",
                ],
                "Effect": "Allow"
            }
        ]
    }
    • The example above grants complete S3 access to some-s3-bucket. Make sure to limit the access to only what is required by the Lambda functions.


  4. With the roles/policies set up, the Lambda functions can now assume the role from Account B and access it’s resources, where permitted.

    • Example NodeJS Lambda function:
      'use strict';
      const AWS = require('aws-sdk');
      var s3 = new AWS.S3();
      var s3_params = {
        Bucket: "some-s3-bucket",
        Key: "some-s3-key",
      };
      var sts = new AWS.STS();
      var sts_params = {
        RoleArn: "arn:aws:iam::XXXXXXXXXXXX:role/account-b-role",
        RoleSessionName: "ThisCanBeAnyName"
      };
      var resp = {};
    
      var handler = (event, context, callback) => {
    
        //Assuming the new role will return temporary credentials
        sts.assumeRole(sts_params, function (err, data) {
          if (err) {
            console.log(err, err.stack);
            resp = response('Internal server error!', 501);
            callback(null, resp);
          } else {
            console.log(data);
    
            //Once we've gotten the temp credentials, let's apply them
            AWS.config.credentials = new AWS.TemporaryCredentials({RoleArn: sts_params.RoleArn});
    
            //Let's get the s3 object sitting in Account B
            s3.getObject(s3_params, function(err, result) {
              if (err) {
                console.log(err, err.stack);
                resp = response('Internal server error!', 501);
              } else {
                console.log(result);
                resp = response('Successfully got data from S3!', 200);
              }
              callback(null, resp);
            });
          }
        });
      };
    
      var response = (message, status) => {
        console.log("Message: " + message);
        return {
            statusCode: status,
            headers: {
              "Access-Control-Allow-Origin": "*"
            },
            body: JSON.stringify({
              message: message
            }),
          };
      };
    
      module.exports = {
        handler: handler
      };
    
comments powered by Disqus