node.jsamazon-web-servicesaws-lambdasts

AWS Use STS to generate dynamic credentials for Lambda to invoke another Lambda


Struggling with a problem here. Node.js. I need to generate temporary credentials using STS from a dynamically generated temporary, highly specific reduced scope role. I then need to use the STS credentials to invoke another Lambda so that control can be passed to the new Lambda, which runs under the reduced temporary scope. SaaS factory style but where we need to limit the Lambda scope itself rather than using the credentials in individual service calls to things like S3 and DynamoDB. i.e. The Lambda to which control is passed, is invoked with the STS credentials, which automatically limit scope of the code the new Lambda executes so that any calls the developer might make are already restricted.

Using code pretty much as below:

let policy = require('./clientPolicy.json');
let sts = new AWS.STS();
let stsParams = {
    RoleArn: process.env.clientWrapperRole,         //primary execution role
    RoleSessionName: `cws-${clientWrapperSession}`,
    Policy: policyString,                           //generated policy
    DurationSeconds: 900
}
let stsResult = await sts.assumeRole(stsParams).promise();
let clientWrapperCred = stsResult.Credentials;
let clientLambdaParams = {
    FunctionName: process.env.clientWrapper, /* required */
    InvocationType: 'RequestResponse',
    LogType: 'Tail',
    Payload: JSON.stringify(clientLambdaPayload)
};

let clientLambda = new AWS.Lambda({ credentials: clientWrapperCred });
let clientResult = await clientLambda.invoke(clientLambdaParams).promise();

Logging shows that the STS call functions correctly, generating credentials against a role that has wide scope, including the ability to call Lambda, which has a service trust to lambda.amazonaws.com and also the role principal of the calling Lambda. The client policy includes scope equivalent to Lambda basic auths, plus allows invoke against the new Lambda.

clientLambda declaration occurs ok but execution at let clientResult... hangs for the maximum Lambda timeout before returning an error response: {"code":"ERR_INVALID_ARG_TYPE"}

If I don't use the STS credentials, but just invoke the Lambda like this:

let clientLambda = new AWS.Lambda();
let clientResult = await clientLambda.invoke(clientLambdaParams).promise();

...then everything works OK.

Anyone got any ideas? Normally I'd just scope down the execution role attached to the Lambda to be called, but in this case I need to dynamically adjust the scope depending on the payload being passed like a SaaS factory.


Solution

  • A quick look at the SDK docs suggests that the Credentials returned from assumeRole is a map while the credentials option provided to the AWS.Lambda() client constructor is of type AWS.Credentials with different property names.

    So, try creating clientWrapperCred like this:

    let clientWrapperCred = {
      accessKeyId: stsResult.Credentials.AccessKeyId,
      secretAccessKey: stsResult.Credentials.SecretAccessKey,
      sessionToken: stsResult.Credentials.SessionToken
    }
    

    I've verified this works correctly using the following code:

    const AWS = require("aws-sdk");
    
    get_assumed_credentials = async () => {
      const sts = new AWS.STS();
    
      const stsParams = {
        RoleArn: ASSUMED_ROLE_ARN,
        RoleSessionName: 'cws-1234',
        DurationSeconds: 900,
      };
    
      const stsResult = await sts.assumeRole(stsParams).promise();
    
      const clientWrapperCred = {
        accessKeyId: stsResult.Credentials.AccessKeyId,
        secretAccessKey: stsResult.Credentials.SecretAccessKey,
        sessionToken: stsResult.Credentials.SessionToken,
      };
    
      return clientWrapperCred;
    };
    
    exports.handler = async (event, context) => {
      try {
        const creds = await get_assumed_credentials();
    
        const lambda = new AWS.Lambda(creds);
    
        const lambda_params = {
          FunctionName: INVOKED_LAMBDA_FUNC,
          Payload: JSON.stringify({ id: 42, source: "hitchhiker" }),
          InvocationType: "RequestResponse",
        };
    
        const lambda_rsp = await lambda.invoke(lambda_params).promise();
        console.log("Lambda rsp:\n" + lambda_rsp);
        console.log("Lambda json:\n" + JSON.stringify(lambda_rsp));
      } catch (err) {
        console.log(err, err.stack);
        throw err;
      }
    };
    

    The Lambda function's execution role with EXECUTION_ROLE_ARN was as follows:

        {
            "Version": "2012-10-17",
            "Statement": [
                {
                    "Sid": "VisualEditor0",
                    "Effect": "Allow",
                    "Action": "sts:AssumeRole",
                    "Resource": "ASSUMED_ROLE_ARN"
                }
            ]
        }
    

    The assumed role with ASSUMED_ROLE_ARN was as follows:

        {
            "Version": "2012-10-17",
            "Statement": [
                {
                    "Sid": "VisualEditor0",
                    "Effect": "Allow",
                    "Action": "lambda:InvokeFunction",
                    "Resource": "INVOKED_LAMBDA_FUNC"
                }
            ]
        }
    

    And the INVOKED_LAMBDA_FUNC had an AWS Managed policy of AWSLambdaBasicExecutionRole (for logging purposed) and the function simply prints its received event payload ({ id: 42, source: "hitchhiker" }) and returns a hello world response.