Categories
AWS

Using multi-factor authentication (MFA) in AWS

I will go over setting up multiple AWS profiles using MFA.

Create your Access key.

Add account(s)

This will update ~/.aws/credentials. It will create 3 profiles.

aws configure --profile account1-default
aws configure --profile account2-default
aws configure --profile account3-default

Example

aws configure --profile account1-default
AWS Access Key ID [None]: XXXXX       # Updates ~/.aws/credentials
AWS Secret Access Key [None]: XXXXX   # Updates ~/.aws/credentials
Default region name [None]: us-east-1 # Updates ~/.aws/config
Default output format [None]: json    # Updates ~/.aws/config (options: None, text, json)

aws configure --profile account2-default
AWS Access Key ID [None]: XXXXX
AWS Secret Access Key [None]: XXXXX
Default region name [None]: us-east-1
Default output format [None]: json

aws configure --profile account3-default
AWS Access Key ID [None]: XXXXX
AWS Secret Access Key [None]: XXXXX
Default region name [None]: us-east-1
Default output format [None]: json

Get short-lived access token

I get my [TOKEN] from Authy.

# account1
aws sts get-session-token --serial-number arn:aws:iam::[ACCOUNT_ID]:mfa/[USERNAME] --token-code [TOKEN] --profile account1-default

# account2
aws sts get-session-token --serial-number arn:aws:iam::[ACCOUNT_ID]:mfa/[USERNAME] --token-code [TOKEN] --profile account2-default

# account3
aws sts get-session-token --serial-number arn:aws:iam::[ACCOUNT_ID]:mfa/[USERNAME] --token-code [TOKEN] --profile account3-default

Output:

{
"Credentials": {
"AccessKeyId": "XXXXX",
"SecretAccessKey": "XXXXX",
"SessionToken": "XXXXX",
"Expiration": "2021-03-16T19:15:23+00:00"
}
}

Set MFA credentials

Update ~/.aws/credentials.

Replace the arn number and username (e.g. arn:aws:iam::[ACCOUNT_ID]:mfa/[USERNAME]) with your own. Adding aws_arn_mfa will come in handy when using the python script provided at the end of this page.

vi ~/.aws/credentials
[account1-default]
aws_access_key_id = XXXXX
aws_secret_access_key = XXXXX

[account1]
aws_arn_mfa = arn:aws:iam::[ACCOUNT_ID]:mfa/[USERNAME]
aws_access_key_id = XXXXX
aws_secret_access_key = XXXXX
aws_session_token = XXXXX

[account2-default]
aws_access_key_id = XXXXX
aws_secret_access_key = XXXXX

[account2]
aws_arn_mfa = arn:aws:iam::[ACCOUNT_ID]:mfa/[USERNAME]
aws_access_key_id = XXXXX
aws_secret_access_key = XXXXX
aws_session_token = XXXXX

[account3-default]
aws_access_key_id = XXXXX
aws_secret_access_key = XXXXX

[account3]
aws_arn_mfa = arn:aws:iam::[ACCOUNT_ID]:mfa/[USERNAME]
aws_access_key_id = XXXXX
aws_secret_access_key = XXXXX
aws_session_token = XXXXX

Update ~/.aws/config

Add a section for your MFA accounts.

vi  ~/.aws/config
[default]
region = us-east-1
output = json

[profile account1-default]
region = us-east-1
output = json

[profile account1]
region = us-east-1
output = json

[profile account2-default]
region = us-east-1
output = json

[profile account2]
region = us-east-1
output = json

[profile account3-default]
region = us-east-1
output = json

[profile account3]
region = us-east-1
output = json

View

Validate identity.

aws sts get-caller-identity --profile account1
aws sts get-caller-identity --profile account2
aws sts get-caller-identity --profile account3

# Query specific fields
aws sts get-caller-identity --query UserId --output text --profile account1
aws sts get-caller-identity --query Account --output text --profile account1
aws sts get-caller-identity --query Arn --output text --profile account1

List profiles.

aws configure list-profiles

# Example
aws configure list-profiles
default
account1-default
account1
account2-default
account2
account3-default
account3

Test access

We can now use the temp MFA account to access AWS.

aws iam list-users --profile account1
aws iam list-users --profile account2
aws iam list-users --profile account3
aws s3api list-buckets --query "Buckets[].Name" --profile account1
aws s3api list-buckets --query "Buckets[].Name" --profile account2
aws s3api list-buckets --query "Buckets[].Name" --profile account3

Script

Create script.

cat <<EOF > aws_update_mfa.py
#!/usr/bin/env python3
import os
import json
import argparse
import subprocess
import configparser

parser = argparse.ArgumentParser(description='Update your AWS CLI Token')
parser.add_argument('token', help='token from your MFA device')
parser.add_argument('--profile', help='aws profile to store the session token', default=os.getenv('AWS_PROFILE'))
parser.add_argument('--arn', help='AWS ARN from the IAM console (Security credentials -> Assigned MFA device). This is saved to your .aws/credentials file')
parser.add_argument('--credential-path', help='path to the aws credentials file', default=os.path.expanduser('~/.aws/credentials'))

args = parser.parse_args()

if args.profile is None:
parser.error('Expecting --profile or profile set in environment AWS_PROFILE. e.g. "stage"')

config = configparser.ConfigParser()
config.read(args.credential_path)

if args.profile not in config.sections():
parser.error('Invalid profile. Section not found in ~/.aws/credentails')

if args.arn is None:
if 'aws_arn_mfa' not in config[args.profile]:
parser.error('ARN is not provided. Specify via --arn')

args.arn = config[args.profile]['aws_arn_mfa']
else:
# Update the arn with user supplied one
config[args.profile]['aws_arn_mfa'] = args.arn

# Generate the session token from the default profile based on the environment. We do not want to overwrite these profiles as we wouldn't
# be able to generate another token
result = subprocess.run(['aws', 'sts', 'get-session-token', '--profile', args.profile + '-default', '--serial-number', args.arn, '--token-code', args.token], stdout=subprocess.PIPE, stderr=subprocess.PIPE)
if result.returncode != 0:
parser.error(result.stderr.decode('utf-8').strip('\n'))

credentials = json.loads(result.stdout.decode('utf-8'))['Credentials']

config[args.profile]['aws_access_key_id'] = credentials['AccessKeyId']
config[args.profile]['aws_secret_access_key'] = credentials['SecretAccessKey']
config[args.profile]['aws_session_token'] = credentials['SessionToken']

# Save the changes back to the file
with open(args.credential_path, 'w') as configFile:
config.write(configFile)

print('Saved {} credentials to {}'.format(args.profile, args.credential_path)) EOF

Make it executable.

chmod +x aws_update_mfa.py

Move script to ~/.aws.

mv aws_update_mfa.py ~/.aws

Alias.

vi ~/.zshrc
alias awsaccount1="~/.aws/aws_update_mfa.py --profile account1"
alias awsaccount2="~/.aws/aws_update_mfa.py --profile account2"
alias awsaccount3="~/.aws/aws_update_mfa.py --profile account3"

Source.

source ~/.zshrc

Usage.

I added the arn (aws_arn_mfa) within my credentials file so I do not need to use the –arg argument (one less step).

usage: aws_update_mfa.py [-h] [--profile PROFILE] [--arn ARN] [--credential-path CREDENTIAL_PATH] token

Update MFA credentials.

awsaccount1 [TOKEN]
awsaccount2 [TOKEN]
awsaccount3 [TOKEN]

By Tommy Elmesewdy

DevOps Engineer