Software Engineering

Cross-account lambdas and renewing security tokens

In a recent project I encountered a need for an AWS Lambda function to receive messages from a Kinesis stream, and forward them on to an IoT pipeline in another ‘target’ AWS account.

In a recent project I encountered a need for an AWS Lambda function to receive messages from a Kinesis stream, and forward them on to an IoT pipeline in another ‘target’ AWS account.

The basic steps are:

1. Create a role to perform the actions

The basic steps are:Create a role in the ‘target’ AWS account, with the permissions to perform the actions you desire (such as publishing to MQTT). I’ve called this ‘ExternalAccountRole’, so it’s ARN will look something like: arn:aws:iam::TargetAccountId:role/ExternalAccountRole. This is important as we’ll need to reference it elsewhere.

2. Permit the lambda to assume the role

To do this, we need to configure permissions in both the ‘target’ and the ‘source’ AWS account.

Firstly, in the source AWS account, for the execution role of the Lambda we add IAM permissions to permit assuming the ‘ExternalAccountRole’ we just created.

    "Version": "2012-10-17",
    "Statement": [
            "Effect": "Allow",
            "Action": "sts:AssumeRole",
            "Resource": "arn:aws:iam::TargetAccountId:role/ExternalAccountRole"

Then, back in the target AWS account, we attach a trust relationship to ‘ExternalAccountRole’ for the “sts:AssumeRole” action, with an AWS Principal. It will need to reference the ARN of the role of the Lambda in the source AWS account.

  "Version": "2012-10-17",
  "Statement": [
      "Sid": "",
      "Effect": "Allow",
      "Principal": {
        "AWS": "arn:aws:iam::SourceAccountId:role/service-role/LambdaRoleName"
      "Action": "sts:AssumeRole"
3. Update the Lambda code to assume the role

Ok, so how do you then go about assuming the role in the lambda function? I found some examples doing this sort of thing:

const sts = new AWS.STS();
const data = await sts.assumeRole({
    RoleArn: process.env.EXTERNAL_ROLE_ARN,
    RoleSessionName: 'awssdk'

    accessKeyId: data.Credentials.AccessKeyId,
    secretAccessKey: data.Credentials.SecretAccessKey,
    sessionToken: data.Credentials.SessionToken

Great! Now, if you go and fetch this role every time your lambda executes then you’re done.

However, what if you want to perform this in the ‘cold start’ steps of the lambda, outside of the main handler, in order to ‘cache’ these credentials?

Guess what. It works great. For an hour. And then your assumed credentials expire! Ok. The Credentials object has an expiry, so we could check that, and then go ask for it again right? But… we’re now using the credentials of the assumed role. And the assumed role isn’t allowed to assume it’s own role again!

Fortunately, turns out this has all been figured out. And the TLDR is you want the AWS.ChainableTemporaryCredentials object.

Your code then looks something like this:

const AWS = require("aws-sdk");

const assumeRoleInExternalAwsAccount = async () => {
  AWS.config.region = "us-west-2";
  const credentials = new AWS.ChainableTemporaryCredentials({
    params: { RoleArn: process.env.EXTERNAL_ROLE_ARN },
    masterCredentials: new AWS.EnvironmentCredentials("AWS"),
  AWS.config.update({ credentials });

// this runs once per cold start
// so we begin an async fetch of role credentials here
// but deliberately don't *await* it here
const assumedRolePromise = assumeRoleInExternalAwsAccount();

exports.handler = async (event) => {
  // ensure we have a token - wait for initialization
  await assumedRolePromise;
  // do some processing, in our case, publishing to the MQTT endpoint in the external account...
   const iotData = new AWS.IotData({ endpoint: process.env.IOT_ENDPOINT });
  await iotData.publish({ 'someTopic', { somePayload: true }).promise()

And hopefully, there you have it!

Leave a Reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.