Problem Statement
I am a frequent AWS CLI user, and this has never been a challenge until my organisation decided to enforce AWS MFA for all users - a recommended practice. This significantly affected how I use the AWS CLI.
- How to enable MFA: AWS Documentation on Enabling a Virtual MFA Device
- How to use MFA with the CLI: AWS Knowledge Center Guide
Look at the hustle you have to go through when using the CLI. To add to that, imagine if you had 4 AWS environments. Will you be copying these temporary credentials into the .aws/credentials
file all the time, or updating the environment variables each time?
Solution
In the DevOps spirit, I had to find a way to automate this. My solution has two parts:
- A Python script that handles the authentication and updates
.aws/credentials
with the new session keys and token. - Creating shell aliases to run the Python script seamlessly.
Part 1: The Python Script
The script uses argparse
to define required command-line arguments: your MFA device ARN, the MFA token from your virtual device, and the AWS CLI profile you wish to authenticate.
import argparse
import boto3
import re
import os
import fileinput
description = "Authenticate and update AWS credentials with MFA tokens"
parser = argparse.ArgumentParser(description=description)
parser.add_argument('--mfa_serial_number', type=str, dest='mfa_serial_number', help='Enter your mfa device arn')
parser.add_argument('--profile_name', dest='profile_name', type=str, help='use default if you dont have any unique profiles')
parser.add_argument('--mfa_token_code', dest='mfa_token_code', type=str, help='MFA code from your virtual device')
# Parse the command-line arguments
args = parser.parse_args()
# Assign arguments to variables
mfa_serial_number = args.mfa_serial_number
profile_name = args.profile_name
mfa_token_code = args.mfa_token_code
# Check that all inputs are present
if not all([mfa_serial_number, mfa_token_code, profile_name]):
parser.error('Required: --mfa_token_code, --mfa_serial_number and --profile_name are required.')
The script then uses the boto3
STS client to authenticate and retrieve temporary session credentials. The session duration is configurable.
##################### Authenticate MFA ##########################
session = boto3.Session(profile_name=profile_name) # enter profile
sts_client = session.client('sts')
response = sts_client.get_session_token(
DurationSeconds=3600, # Value changes between 900 (15 mins) - 129600 (36 hours)
SerialNumber=mfa_serial_number,
TokenCode=mfa_token_code
)
access_key = response['Credentials']['AccessKeyId']
secret_key = response['Credentials']['SecretAccessKey']
session_token = response['Credentials']['SessionToken']
Finally, the script updates the .aws/credentials
file in-place. It searches for a specific profile block (e.g., [mfa]
) and replaces the subsequent three lines with the new credentials.
Prerequisite: You must pre-create the profile block in ~/.aws/credentials
:
[mfa]
wertyio
asdfgh
zxcvbn
#################### Update .aws/credential file with the temporary mfa credentials #####################
# Define the pattern to search for
pattern = re.compile(r'\[mfa\]')
# Define the replacement content for the three lines
replacement_lines = [f'aws_access_key_id = {access_key}\n', f'aws_secret_access_key = {secret_key}\n', f'aws_session_token = {session_token}\n']
home = os.path.expanduser("~")
# Open the file in inplace mode using fileinput
with fileinput.input(f'{home}/.aws/credentials', inplace=True) as file:
# Iterate over the lines in the file
for line in file:
# If the line matches the pattern, print it and then modify the next three lines
if pattern.match(line):
print(line, end='')
# Print the new replacement lines
for replacement_line in replacement_lines:
print(replacement_line, end='')
# Skip the next three lines in the original file
next(file)
next(file)
next(file)
# Otherwise, just print the line
else:
print(line, end='')
Part 2: Creating Aliases
Aliases are shortcuts in your shell to execute longer commands. This is a personal preference; feel free to modify them.
First, create an alias to source your Python virtual environment (if you use one).
# Add to ~/.zshrc or ~/.bashrc
alias source_env='source /Users/devops/Desktop/python_lab/env/bin/activate'
Next, create aliases for each of your AWS environments. The number of aliases depends on the number of environments you have (e.g., test
, staging
, prod
).
# Add to ~/.zshrc or ~/.bashrc
alias mfaTest='source_env && python /Users/devops/Desktop/python_lab/enableMFA/newCred.py --mfa_serial_number $TEST_DEVICE --profile_name test --mfa_token_code '
alias mfaStaging='source_env && python /Users/devops/Desktop/python_lab/enableMFA/newCred.py --mfa_serial_number $STAGING_DEVICE --profile_name staging --mfa_token_code '
alias mfaProd='source_env && python /Users/devops/Desktop/python_lab/enableMFA/newCred.py --mfa_serial_number $PROD_DEVICE --profile_name prod --mfa_token_code '
Important: The mfa_serial_number
argument is passed from an environment variable. You must define these variables in your shell configuration file:
# Add to ~/.zshrc or ~/.bashrc
export TEST_DEVICE='arn:aws:iam::123456789012:mfa/my-device'
export STAGING_DEVICE='arn:aws:iam::210987654321:mfa/my-device'
export PROD_DEVICE='arn:aws:iam::098765432109:mfa/my-device'
Usage: To authenticate, call the alias followed by the current MFA code from your authenticator app.
mfaTest 123456
After running, you can verify your credentials by calling any AWS CLI command using the --profile mfa
flag.
Alternative Approach
If you are an admin and have IAM permissions across your AWS accounts, you can create an IAM role with cross-account permissions. This allows you to assume a role directly for the account you need, which can often be a more scalable and secure solution than managing long-term credentials and MFA tokens for the CLI.
Top comments (0)