A Brief Introduction to AWS Assume Role

Cemal Ünal
Picus Security Engineering
4 min readJan 29, 2022

We extensively use AWS services in our daily tasks by using AWS SDKs (boto3, AWS SDK for Go, etc.). All programmatic access to the AWS resources made using SDKs takes place via an API call. We need to have the required credentials to perform these calls since AWS APIs require authentication. There are various ways to obtain these credentials and some of them are the following:

  • Using the root account credentials. (Not recommended by AWS)
  • Creating an IAM user and using its credentials. (This way is less secure than the assume role method.)
  • Temporary credentials using assume role functionality.

By following one of the security best practices in AWS Documentation — “Use roles to delegate permissions” we should use Security Token Service (STS) assume role feature to receive temporary credentials. This way is more secure than using IAM user credentials since the IAM user has long-term access key security credentials. Also, assume role credentials expire after a period of time (12 hours maximum). This reduces the risk of compromising the credentials.

How Does it Work?

Let’s assume that we have two roles Role A and Role B in our AWS account and we want to assume Role B using Role A.

We need to edit the inline policy of Role A and add sts:AssumeRole action. This will allow required permission to Role A to assume Role B.

{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Principal": {
"AWS": "arn:aws:iam::account-id:role/Role-B"
},
"Action": "sts:AssumeRole"
}
]
}

After that, we need to edit the trust relationship of the Role B and allow access to Role A to assume it:

{
"Version": "2012-10-17",
"Statement": {
"Effect": "Allow",
"Action": "sts:AssumeRole",
"Resource": "arn:aws:iam::account-id:role/Role-A"
}
}

Also, we might require a unique identifier when assuming a role using the external ID parameter. By doing that, we are adding an extra layer of security, and only someone with this id will be able to assume the role. For example, let’s update the trust relationship of Role B with the following content and add the Condition field:

{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Principal": {
"AWS": "arn:aws:iam::account-id:role/Role-A"
},
"Action": "sts:AssumeRole",
"Condition": {
"StringEquals": {
"sts:ExternalId": "RandomExternalId253544"
}
}
}
]
}

From now on, Role A will need to pass RandomExternalId253544 parameter to assume Role B

We can also assume a role that resides in a different AWS account. It can be achieved by updating the Principalfield in the trust relationship:

"Principal": {
"AWS": "arn:aws:iam::different-account-id:role/Role-A"
}

Assume Role in Action

Assuming role via AWS CLI

To assume a role using the AWS CLI aws sts assume-role like following:

aws sts assume-role \
--role-arn "arn:aws:iam::account-id:role/role-name" \
--role-session-name "assume_role_session_name" \
--external-id "RandomExternalId253544"

Output:

{
"Credentials": {
"AccessKeyId": "<AccessKeyId>",
"SecretAccessKey": "<SecretAccessKey>",
"SessionToken": "<SessionToken>",
"Expiration": "2022-01-14T13:45:22+00:00"
},
"AssumedRoleUser": {
"AssumedRoleId": "omitted",
"Arn": "omitted"
}
}

Then you can export the following environment variables in your terminal:

export AWS_ACCESS_KEY_ID="<AccessKeyId>"
export AWS_SECRET_ACCESS_KEY="<SecretAccessKey>"
export AWS_SESSION_TOKEN="<SessionToken>"

Important: Please note that after you set the credentials of the assumed role, you lose your current credentials temporarily. For example, let’s say your current credentials allow you to create an object in an S3 bucket. When you assume a role that does not allow you to create an object on that bucket, you lose your permission to create that object.

Assuming role via Programmatic Way (Go)

Create a new AWS session:

sess, err := session.NewSession(&aws.Config{
Region: aws.String("region"),
})
if err != nil {
fmt.Printf("failed to create aws session, %v\n", err)
return
}

Call the related function to assume the role:

assumeRoleInput := sts.AssumeRoleInput{
RoleArn: aws.String(roleArn),
RoleSessionName: aws.String(sessionName),
DurationSeconds: aws.Int64(60 * 60 * 1),
}

if externalId != "nil" {
fmt.Println("Trying to fetch external id to assume new role")
assumeRoleInput.ExternalId = aws.String(externalId)
}

svc := sts.New(sess)
result, err := svc.AssumeRole(&assumeRoleInput)
if err != nil {
fmt.Printf("failed to assume role, %v\n", err)
return
}

And we can set the related environment variables using the credentials in the assume role response

if err := os.Setenv("AWS_ACCESS_KEY_ID", *result.Credentials.AccessKeyId); err != nil {
fmt.Printf("Couldn't set AWS_ACCESS_KEY_ID environment variable. Err: %v", err)
}

if err := os.Setenv("AWS_SECRET_ACCESS_KEY", *result.Credentials.SecretAccessKey); err != nil {
fmt.Printf("Couldn't set AWS_SECRET_ACCESS_KEY environment variable. Err: %v", err)
}

if err := os.Setenv("AWS_SESSION_TOKEN", *result.Credentials.SessionToken); err != nil {
fmt.Printf("Couldn't set AWS_SESSION_TOKEN environment variable. Err: %v", err)
}

The final code snippet is like the following:

import (
"fmt"
"github.com/aws/aws-sdk-go/aws"
"github.com/aws/aws-sdk-go/aws/session"
"github.com/aws/aws-sdk-go/service/sts"
"os"
)
func assumeRole(roleArn string, sessionName string, externalId string) {
sess, err := session.NewSession(&aws.Config{
Region: aws.String("region"),
})
if err != nil {
fmt.Printf("failed to create aws session, %v\n", err)
return
}

svc := sts.New(sess)
assumeRoleInput := sts.AssumeRoleInput{
RoleArn: aws.String(roleArn),
RoleSessionName: aws.String(sessionName),
DurationSeconds: aws.Int64(60 * 60 * 1),
}

if externalId != "nil" {
fmt.Println("Trying to fetch external id to assume new role")
assumeRoleInput.ExternalId = aws.String(externalId)
}

result, err := svc.AssumeRole(&assumeRoleInput)
if err != nil {
fmt.Printf("failed to assume role, %v\n", err)
return
}

if err := os.Setenv("AWS_ACCESS_KEY_ID", *result.Credentials.AccessKeyId); err != nil {
fmt.Printf("Couldn't set AWS_ACCESS_KEY_ID environment variable. Err: %v", err)
}

if err := os.Setenv("AWS_SECRET_ACCESS_KEY", *result.Credentials.SecretAccessKey); err != nil {
fmt.Printf("Couldn't set AWS_SECRET_ACCESS_KEY environment variable. Err: %v", err)
}

if err := os.Setenv("AWS_SESSION_TOKEN", *result.Credentials.SessionToken); err != nil {
fmt.Printf("Couldn't set AWS_SESSION_TOKEN environment variable. Err: %v", err)
}
}

func main() {
roleArn := "arn:aws:iam::account-id:role/role-name"
sessionName := "assume_role_session_name"
externalId := "RandomExternalId253544"
assumeRole(roleArn, sessionName, externalId)
}

For security concerns, we should store the value of externalId somewhere else instead of writing plaintext in the code. Let’s keep it this way for the sake of simplicity of this article.

Thanks for reading. If you have questions or comments regarding this article, please feel free to leave a comment below.

--

--

Cemal Ünal
Picus Security Engineering

Cloud Software Engineer @ Picus Security | AWS Certified DevOps Engineer Professional