Assumed Roles with the AWS SDK

Filed under AWS on January 22, 2020

Something I haven’t noticed before, mostly because I haven’t needed it until recently, is using assumed roles to authorise applications with the Amazon SDK. I was met with the following error when I tried to use a profile defined with a role_arn and source_profile instead of an access ID and secret

encountered error: NoCredentialProviders: no valid providers in chain. Deprecated.
	For verbose messaging see aws.Config.CredentialsChainVerboseErrors

Took me a little bit to find the solution via Duck duck go, but it turns out you can’t use the AWS_PROFILE environment variable here.

Using roles in the CLI

To create a role based profile in your credentials file, you will need to have at least one profile that has permissions to assume that role.

[default]
aws_access_key_id = AAAAAAAAAAAAAAAAAAAA
aws_secret_access_key = LoLYouThoughtYouWereGettingMyKey

[rolecred]
role_arn = arn:aws:iam::11111111111:role/some-role
source_profile = default

Here we can see that in order to assume a role using a profile, you need to profile a role ARN, which can also be a cross account role, and a source for the initial credentials.

Side note: I found this digging pretty hard through the docs looking for something else. If you can repost the above snippet somewhere to make search engines find it more readily it would probably be for the best.

Back to it.

I’m not sure how many applications will need to be able to use every profile in the credentials file, so I’ll be using the assumption that we only need to be able to assume one role moving forward. If you need a golang ini parser to load up the roles manually, I’ve been using gopkg.in/ini.v1, which is pretty simple to use and hasn’t given me any issues yet.

Simple token service

STS is Amazon’s way of providing applications temporary access to resources in the AWS ecosystem. I think the quintessential example I’ve seen in most of the training material is generating presigned S3 URLs so that you can give a link with an expiry date to a client, but it can be used to generate federation tokens, get caller IDs and of course assume another role as well.

We’re going to be assuming a role, obviously, so your code is going to look a bit like this

package main

import (
	"fmt"
	"github.com/aws/aws-sdk-go/aws"
	"github.com/aws/aws-sdk-go/aws/credentials"
	"github.com/aws/aws-sdk-go/aws/session"
	"github.com/aws/aws-sdk-go/service/sts"
	"math/rand"
)

func getAssumedSession(roleArn, region string) (*session.Session, error) {
	// Assuming AWS_PROFILE is set
	baseSess := session.Must(session.NewSession(&aws.Config{
		Region: aws.String(region),
	}))
	stsSvc := sts.New(baseSess)
	sessionName := fmt.Sprintf("some-session-%d", rand.Int())
	assumedRole, err := stsSvc.AssumeRole(&sts.AssumeRoleInput{
		RoleArn:         aws.String(roleArn),
		RoleSessionName: aws.String(sessionName),
	})
	if err != nil {
		return nil, err
	}
	return session.NewSession(&aws.Config{
		Credentials:   credentials.NewStaticCredentials(
			*assumedRole.Credentials.AccessKeyId,
			*assumedRole.Credentials.SecretAccessKey,
			*assumedRole.Credentials.SessionToken),
		Region:        aws.String(region),
	})
}

And that’s it! You should be able to use the returned session object like any other session object in the SDK.

This post was inspired by a tool I’m working on in my own time to generate an SSH tunnel through bastion servers and into databases sitting in private subnets by using ec2-connect to generate itself a key. It needs to be able to flick through profiles, so naturally this was a problem I ran into very quickly. Hopefully I can save someone a bit of digging on Google or stack overflow with this one, or at least have that function handy if I need it.