AWS MFA in Bash

Best practice when working with AWS (along with most things that require authentication) is to use some form of multi-factor authentication (MFA). After enabling MFA for your account, interactions with AWS using the CLI needs to provide the corresponding additional information. The CLI itself doesn’t provide particular support for this need, and so often additional helpers such as awsmfa are used. While such a tool is very effective and as the one referenced is written in Python it is unlikely to represent significant overhead beyond the AWS CLL/boto, I’ve had cases in the past for which it was less than ideal.

The particular case where a solution such as awsmfa did not readily help was where I wanted to rely on the relevant ticket information to be present in the environment which could then be accessed by the standard AWS credentials lookup chain. As forking an additional process does not lend itself to modifying the current environment, adopting an approach which can more directly modify that environment would be preferred. The shell/Bash can coordinate calls to the AWS CLI to support this functionality across a range of flows.

Supported Flows

Different organizations manage their AWS IAM differently, so far I’ve used variations of this script to support. I’ve typically only had to use one of these at a time so in the past I’ve modified the script in ways that may or may not preserve backwards compatibility, so some flows may need some tightening.

Basic Single User MFA

The most straightforward scenario is supporting MFA for a given IAM user, analogous to logging directly into the desired account using the AWS console. This can be supported by a basic population of the associated environment variables.

Multiple Specified Profiles

Multiple accounts can be accessed by associating the ticket information in a profile for each account and then referencing the profile while accessing AWS. In this scenario the environment doesn’t need to be modified and therefore this is likely to not be any value in this approach compared to awscli. A wrinkle when using this approach is that the named profiles risk coupling the calls that are being made with the environment in which they are executed thereby inviting additional complexity of one form or other.

Assumed Roles

A more elaborate AWS configuration may involve a network of accounts where most of the resources themselves are housed in accounts which are not that which has been logged into and is managed through cross account access which requires assuming a role in such resource accounts.

aws.sh

As referenced earlier modifying an environment is best done without forking a process and therefore modifying the environment of the current shell is best done within the current shell process. This can be accomplished through invoking functions which provide the desired functionality as opposed to calling a separate process, and this file is therefore intended to be sourced rather than run.

Variables

As many of these variables reflect the current environment they will be left mutable so as not to lead to incidentally limiting use of the shell.

AWS_CONFIG_FILE

The ARN for the MFA token should be defined in a local configuration file so the location of that file will be indicated.

declare AWS_CONFIG_FILE="${AWS_CONFIG_FILE:-${HOME}/.aws/config}"

AWS_PROFILE

Some of the use cases for this script can benefit from referencing an AWS_PROFILE, so the variable is defined here with a default of default.

declare AWS_PROFILE="${AWS_PROFILE:-default}"

AWS_ROLE_ARN

If a role needs to be assumed its ARN should be stored in this variable.

declare AWS_ROLE_ARN

my_aws_access_key_id

Store a local access key id for later use.

declare my_aws_access_key_id

Functions

lookup_mfa_serial

The serial number for the MFA device will often be defined in a profile’s configuration but is less likely to be explicitly provided. An awk script which parses the configuration file while keeping track of the current profile and printing the mfa_serial if the profile in the context is the desired one serves to extract the relevant value.

This adopts the pattern of inline awk with associated escaping hoops.

TODO: The profile matching seems like it would be more idiomatic if there were two rules where one set thisP and the other unset it rather than the one that sets it to the test result. Also thisP could stand a rename.

aws::lookup_mfa_serial() {
  AWS_MFA_SERIAL=$(awk -s "BEGIN { thisP=0; } /\[.*]/ { thisP=(\$0==\"[${AWS_PROFILE}]\") } /mfa_serial/ { if (thisP) { print \$3 } }" "${AWS_CONFIG_FILE}")
}

load_credentials_from