One of the biggest issues with the current version of the AWS-SDK-JS is that role credentials aren't initialised as nicely as one might want to. I myself am definitely not someone who writes code every day and doesn't want to understand the issues at hand, but when teams come to me and ask me for advice: I want to be able to give them clear, concrete and concise answers.
One of the many use-cases is cross-account lambda/resource access. In the code below I have created an example which allows you to assume a role from the execution role, write to an s3-bucket, assume another role from the previous assumed role and with another file to another s3 bucket.
Pre-requisits
- Some role with policy/privileges to Assume
- Policy / rights to allow to assume said role in your application or lambda (e.g. executionRole/instanceRole
Click here to Jump to the code instead.
Enable Allow Assume Role - Policy
{
"Version": "2012-10-17",
"Statement": [{
"Effect": "Allow",
"Action": "sts:AssumeRole",
"Resource": "arn:aws:iam::1234567890:role/SomeExampleRole"
}]
}
You should specify in both the role you assume and the role to be assumed, who does so and who is allowed. AWS uses deny by default, so without specifying an explicit allow, you will get a deny.
Conceptual Role example - YAML snippet
Description: Some Example role that gives some other role acccess to do deploy and request of certificates
AWSTemplateFormatVersion: '2010-09-09'
Metadata:
Version: 0.0.1
Author: Some Author Other Than Me!
Last-Modified-by: Well Not me
Note: Some Note
Version-changes: Added ACM support
Company: Some Company
Company-Email: Some@Company.Email
Email: Some@E.Mail
Phone: +9876543210
Parameters:
SomeSharedSecret:
Description: Please provide a random guid for ExternalID (shared secret) https://guidgenerator.com/online-guid-generator.aspx
Type: String
MinLength: 8
MaxLength: 36
AllowedPattern: ^[a-z0-9\\-]*$
Environment:
Default: "dev"
Type: String
Description: Environment type (prod/pre-prod/test/dev)
AllowedValues:
- "dev"
- "test"
- "preprod"
- "prod"
Version:
Default: "0.0.1"
Type: String
Description: This templates version
AllowedValues:
- "0.0.1"
Resources:
ExampleRole:
Type: AWS::IAM::Role
Properties:
AssumeRolePolicyDocument:
Version: '2012-10-17'
Statement:
- Effect: Allow
Principal:
AWS:
- arn:aws:iam::01234567890:role/SomeExecutionRole
Action: sts:AssumeRole
Condition:
StringEquals:
'sts:ExternalId': !Ref 'SomeSharedSecret'
Path: "/"
ManagedPolicyArns:
- !Ref ExampleRolePolicy
RoleName: !Sub "SomeExampleRoleName"
ExampleRolePolicy:
Type: AWS::IAM::ManagedPolicy
Properties:
ManagedPolicyName: !Sub "SomeExampleRolePolicyName-v${Version}"
PolicyDocument:
Version: "2012-10-17"
Statement:
- Effect: Allow
Action:
- s3:ListBucket
Resource:
- !Sub "arn:aws:s3:::some-example-bucket-${Environment}"
- Effect: Allow
Action:
- s3:GetObject
- s3:PutObject
- s3:DeleteObject
Resource:
- !Sub "arn:aws:s3:::some-example-bucket-${Environment}/*"
- Effect: Allow
Action:
- s3:GetBucketLocation
- s3:ListAllMyBuckets
Resource:
- "arn:aws:s3:::*"
- Effect: Allow
Action:
- iam:ListServerCertificates
- iam:UploadServerCertificate
- iam:UpdateServerCertificate
- iam:DeleteServerCertificate
Resource:
- "*"
- Effect: Allow
Action:
- acm:DescribeCertificate
- acm:ListCertificates
- acm:ImportCertificate
- acm:DeleteCertificate
Resource:
- "*"
- Effect: Allow
Action:
- route53:ListHostedZones
- route53:ListHostedZonesByName
- route53:ListResourceRecordSets
- route53:GetChange
- route53:GetHostedZone
- route53:GetHostedZoneCount
- route53:ChangeResourceRecordSets
Resource:
"*"
Conceptual Policy Example - YAML snippet
Resources:
SomeLambdaExectutionPolicy:
Type: AWS::IAM::ManagedPolicy
Properties:
ManagedPolicyName: !Sub "SomeExampleExecutionRolePolicyName-v${Version}"
PolicyDocument:
Version: "2012-10-17"
Statement:
- Effect: Allow
Action:
- sts:assumeRole
Resource:
- !Sub "arn:aws:iam::01234567890:role/SomeExampleRole"
Use managed policies over inline policies whenever possible. The advantage of managed policies that they have up to 7 revisions
Code Example - Snippet: getSTS.js
Below code is a compilation of some snippets which one can implement illustrating how to assume a role using sts.assumerole in node.js (e.g., to jump roles and/or assume roles and use them inside the AWS-"client"). You specify the sts-temporary-credentials as arguments when creating the new handler.
import AWS from 'aws-sdk';
const getSTS = async () => {
const sts = new AWS.STS({ region: process.env.REGION });
const params = {
RoleArn: 'arn:aws:iam::1234567890:role/someRole',
RoleSessionName: 'CrossAccountCredentials',
ExternalId: '1234567-1234-1234-1234-123456789012',
DurationSeconds: 3600,
};
const assumeRoleStep1 = await sts.assumeRole(params).promise();
console.log('Changed Credentials');
const accessparams = {
accessKeyId: assumeRoleStep1.Credentials.AccessKeyId,
secretAccessKey: assumeRoleStep1.Credentials.SecretAccessKey,
sessionToken: assumeRoleStep1.Credentials.SessionToken,
};
const s3 = await new AWS.S3(accessparams);
s3.putObject({
Body: `Angeliques test ${new Date().toLocaleString()}`,
Bucket: 'bucket1',
Key: 'helloworld.txt',
}, (err, data) => {
if (err) console.log(err, err.stack); // an error occurred
else console.log(data); // successful response
});
const sts2 = new AWS.STS(accessparams);
const params2 = {
RoleArn: 'arn:aws:iam::1234567890:role/someRole2',
RoleSessionName: 'CrossAccountCredentials2',
ExternalId: '1234567-1234-1234-1234-123456789012',
};
const assumeRoleStep2 = await sts2.assumeRole(params2).promise();
console.log(assumeRoleStep2);
const accessparams2 = {
accessKeyId: assumeRoleStep2.Credentials.AccessKeyId,
secretAccessKey: assumeRoleStep2.Credentials.SecretAccessKey,
sessionToken: assumeRoleStep2.Credentials.SessionToken,
};
const s32 = await new AWS.S3(accessparams2);
s32.putObject({
Body: `Angeliques test ${new Date().toLocaleString()}`,
Bucket: 'bucket2',
ServerSideEncryption: 'AES256',
Key: 'helloworld.txt',
}, (err, data) => {
if (err) console.log(err, err.stack); // an error occurred
else console.log(data); // successful response
});
return assumeRoleStep1;
};
module.exports = getSTS;
This code is not complete, prettified or in single style. It requires additional implementation and revision! It is written using ES6 and will require transpiling (babel etc) or modern implementation. Use this as snippet-example only. Not as-is!
Step-by-step
Create a new STS service client using the already known credentials from the Lambda execution role.
const sts = new AWS.STS({ region: process.env.REGION });
AWS.Config.Credentials are pulled from the environment variables. These credentials are provided via the metadata service and STS. The chain-provider will ensure there will always be correct credentials available and always ensure they are "renewed" using retry & callback mechanisms.
Do not change your AWS.config unless you really need to. Tons of examples will tell you to do the following:
AWS.config.update({region: 'some-region',accessKeyId: 'some-key',secretAccessKey: 'some-secret-key',sessionToken: 'some-session-token'});
. This does work but has the side-effect that this will also result in your logging and everything else to use those credentials as it will update the global AWS object. (This global, does not behave as you would think and has very unpredictable behaviour). Normally this is not what you want to achieve in 99% of all cases. You just want to create new service clients with specific credentials that you can re-use and/or destroy when leaving scope.
The credentials are stored in clear-text in memory. You could store these in a different way. This example does not cover this.
Define the parameters of the role you want to assume
const params = {
RoleArn: 'arn:aws:iam::1234567890:role/someRole',
RoleSessionName: 'CrossAccountCredentials',
ExternalId: '1234567-1234-1234-1234-123456789012',
DurationSeconds: 3600,
};
Create a promise object and store the AWS request object.
const assumeRoleStep1 = await sts.assumeRole(params).promise();
I use async/await (ecma2015). You could use regular callback sequences etc, but both code and execution become so much more concise with await/async. You could rewrite the above code as .promise().then().catch()
or try{}catch(err){}
etc. Current example does not catch any errors!
Avoid unnecessary nesting and/or callback hell whenever you can. The use of async and await makes life much simpler, by making async functions seem as if they are synchronous code. All of these functions execute in a nonblocking fashion, having the benefit of nonblocking async functions, with the simplicity and readability of synchronous code. Click here for a Google primer on async/await-functions.
Create an array which contains the credentials received from STS to pass to the constructer of the service client. In this case S3 or STS.
const accessparams = {
accessKeyId: assumeRoleStep1.Credentials.AccessKeyId,
secretAccessKey: assumeRoleStep1.Credentials.SecretAccessKey,
sessionToken: assumeRoleStep1.Credentials.SessionToken,
};
Now create the service-client and start using the role/temporary security credentials:
const s3 = await new AWS.S3(accessparams);
const sts2 = new AWS.STS(accessparams);
I could have used let instead of const when creating the "handles" for sts and s3. If you do not re-assign variables, const is deemed better than let. Never use var! (scoping)