Getting and Using Session Tokens with MFA in the AWS CLI

Securing your AWS IAM user with multi factor authentication (MFA) is a good idea. However, when working on the command line interface (CLI), the need to enter changing token codes creates some overhead. This article shows how to use MFA in the CLI.

Enabling MFA

This documentation shows how to enable MFA. With these settings, logins to the web console via the browser will only work with a code from your MFA device.

To also force IAM users to having to enter an MFA token code when using the CLI, set the IAM policy as shown here.

With this setup, any command given to the AWS CLI will fail without a proper token code:

$ aws eks list-clusters
An error occurred (AccessDeniedException) when calling the ListClusters operation: User: arn:aws:iam::000000000012:user/yourusername is not authorized to perform: eks:ListClusters on resource: arn:aws:eks:eu-central-1:000000000012:cluster/* with an explicit deny

Requesting and Setting Credentials Manually

Here is how to get a token and use it in every request. This process is also shown here on YouTube.

First, find the amazon resource name (ARN) of your MFA device. Go to the user table in IAM service and select your user. The tab “Security credentials” shows the ARN at “Assigned MFA device”.

AWS Web Console Assigned MFA Device

With that, a token can be requested from the Security Token Service (STS), using one of the codes from your MFA device:

$ aws sts get-session-token --serial-number arn:aws:iam::000000000012:mfa/yourusername --token-code 123456
{
    "Credentials": {
        "AccessKeyId": "A...2",
        "SecretAccessKey": "x...F",
        "SessionToken": "IQ....DZS",
        "Expiration": "2022-02-27T22:10:04+00:00"
    }
}

To make following commands in the AWS CLI use this token, it can be exported in the environment variables like this:

$ export AWS_ACCESS_KEY_ID=A...2
$ export AWS_SECRET_ACCESS_KEY=x...F
$ export AWS_SESSION_TOKEN=IQ...DZS

Here is a good article about environment variables in Linux.

A request made within the lifetime of that token will now succeed:

$ aws eks list-clusters
{
    "clusters": [
       "my-cluster-name-here"
    ]
}

Requesting and Setting Credentials By Script

To ease the process demonstrated above, I wrote two scripts.

The first script, aws-unset-mfa-access-token.sh, removes the three variables from the environment:

#!/bin/bash

if [ ${BASH_SOURCE[0]} == ${0} ]
then
	SOURCED=false
else
	SOURCED=true
fi

while getopts ":h" option; do
   case $option in
      h) # Display help
         echo "This will unset the following environment variables:"
 		 echo
		 echo "	AWS_ACCESS_KEY_ID"
		 echo "	AWS_SECRET_ACCESS_KEY"
		 echo "	AWS_SESSION_TOKEN"
		 echo
		 echo "To be able to change environment variables, this script has to be executed with 'source' like this:"
		 echo "source ./aws-unset-mfa-access-token.sh"
		 if [ $SOURCED = true ]
		 then
			return
		 else
			exit
		 fi
		 ;;
   esac
done

if [ $SOURCED = false ]
then
	echo "You are not running this script as source."
	echo "To make these changes permanent, call this script source'd like this:"
	echo "source ./aws-unset-mfa-access-token.sh"
	exit
fi

echo "Removing AWS_ACCESS_KEY_ID from environment variables ..."
unset AWS_ACCESS_KEY_ID
echo "Removing AWS_SECRET_ACCESS_KEY from environment variables ..."
unset AWS_SECRET_ACCESS_KEY
echo "Removing AWS_SESSION_TOKEN from environment variables ..."
unset AWS_SESSION_TOKEN

The second script, aws-set-mfa-access-token.sh, requests new credentials from the STS as shown above and sets them in the environment variables:

#!/bin/bash

if [ ${BASH_SOURCE[0]} == ${0} ]
then
	SOURCED=false
else
	SOURCED=true
fi

while getopts ":h" option; do
   case $option in
      h) # Display help
		 echo "This will call AWS STS with your current credentials, get a temporary access token and set this token with the following environment variables:"
		 echo
		 echo "	AWS_ACCESS_KEY_ID"
		 echo "	AWS_SECRET_ACCESS_KEY"
		 echo "	AWS_SESSION_TOKEN"
		 echo
		 echo "Usage:"
		 echo
		 echo "aws-set-mfa-access-token MFA_SERIAL_NUMBER TOKEN_CODE"
		 echo
		 echo "Example:"
		 echo
		 echo "aws-set-mfa-access-token arn:aws:iam::000000000042:mfa/youruser 424242"
		 echo
		 echo "This will cause every request to be send with this access token."
		 echo
		 echo "To be able to change environment variables, this script has to be executed with 'source' like this:"
		 echo "source ./aws-set-mfa-access-token.sh"
		 if [ $SOURCED = true ]
		 then
			return
		 else
			exit
		 fi
		 ;;
   esac
done

if [ $SOURCED = false ]
then
	echo "You are not running this script as source."
	echo "To make these changes permanent, call this script source'd like this:"
	echo "source ./aws-set-mfa-access-token.sh"
	exit
fi

if [ $# -lt 2 ]
then
	echo "Please provide two arguments:"
	echo "1. MFA_SERIAL_NUMBER"
	echo "2. TOKEN_CODE"
	return
fi

echo "Checking if environment variables are set ..."
echo
if [ -z ${AWS_ACCESS_KEY_ID+x} ]; then 
	echo "AWS_ACCESS_KEY_ID is unset";
	AWS_ACCESS_KEY_ID_ALREADY_SET=false
else 
	echo "AWS_ACCESS_KEY_ID is set to '$AWS_ACCESS_KEY_ID'"; 
	AWS_ACCESS_KEY_ID_ALREADY_SET=true
fi

if [ -z ${AWS_SECRET_ACCESS_KEY+x} ]; then 
	echo "AWS_SECRET_ACCESS_KEY is unset"; 
	AWS_SECRET_ACCESS_KEY_ALREADY_SET=false
else 
	echo "AWS_SECRET_ACCESS_KEY is set to '$AWS_SECRET_ACCESS_KEY'"; 
	AWS_SECRET_ACCESS_KEY_ALREADY_SET=true
fi

if [ -z ${AWS_SESSION_TOKEN+x} ]; then 
	echo "AWS_SESSION_TOKEN is unset"; 
	AWS_SESSION_TOKEN_ALREADY_SET=false
else 
	echo "AWS_SESSION_TOKEN is set to '$AWS_SESSION_TOKEN'"; 
	AWS_SESSION_TOKEN_ALREADY_SET=true
fi

echo

if [ $AWS_ACCESS_KEY_ID_ALREADY_SET = true ] || [ $AWS_SECRET_ACCESS_KEY_ALREADY_SET = true ] || [ $AWS_SESSION_TOKEN_ALREADY_SET = true ]; then
	echo "The already existing variables will be overriden. Continue?"
	read -p "Continue? (Y/N): " confirm && [[ $confirm == [yY] || $confirm == [yY][eE][sS] ]] || return
else
	echo "No existing variables set. Will continue ..."
fi


echo "Getting new MFA access token ..."
CREDJSON="$(aws sts get-session-token --serial-number $1 --token-code $2)"

ACCESSKEY="$(echo $CREDJSON | jq '.Credentials.AccessKeyId' | sed 's/"//g')"
SECRETACESSKEY="$(echo $CREDJSON | jq '.Credentials.SecretAccessKey' | sed 's/"//g')"
SESSIONTOKEN="$(echo $CREDJSON | jq '.Credentials.SessionToken' | sed 's/"//g')"

echo
echo "AccessKeyId:"
echo $ACCESSKEY
echo "SECRETACESSKEY:"
echo $SECRETACESSKEY
echo "SESSIONTOKEN:"
echo $SESSIONTOKEN

echo
echo "Setting values as environment variables ..."
export AWS_ACCESS_KEY_ID=$ACCESSKEY
export AWS_SECRET_ACCESS_KEY=$SECRETACESSKEY
export AWS_SESSION_TOKEN=$SESSIONTOKEN
echo "Done."

Both scripts come with a help page and some user friendliness such as input validation and informative output. They can be downloaded at this GitHub repository.

Note: AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY and AWS_SESSION_TOKEN could also be added to the credentials file stored in the .aws folder. However, Amazon recommends using the environment variables. Also, there is no easy way adding the values to the credentials file via the AWS CLI.

(Image Public Domain, https://pxhere.com/en/photo/730639)