Automating Multi Factor Authentication for the AWS command line

Gert-Jan van de Streek

Gert-Jan van de Streek

Published: 26 November, 2020

Our security policy forces all employees to use Multi Factor Authentication (MFA) whenever possible. It's easy enough to adhere to this requirement for most serious cloud services. Turning it on for access to the service website is mostly a no-brainer. Using Multi Factor for access to the command line however, needs a bit more attention. We noticed developers turn on Multi Factor for the AWS console for example, but keep using static keys for the command line. Ouch, time to fix that.

socialgj

I hear people say 'security first' more than often. Security's weakest link is the one in the driver seat, in this case, the developer. That's why I advocate investing time to pick or develop the proper tools supporting the security policies. Make things easy for the developer/user.

Enough talk, show me the code:
We choose to force MFA as soon as you change directory to a project containing cloud deployment scripts or code. We already use direnv to force the use of a named profile for AWS to make sure the developer is always on the right account. We can simply add some code to that to force the availability of a valid session token.


ACCOUNT=123456789100
KEY_BASE_USER_ID=$(keybase status -j | jq -r '.Username')
if [ -z "$AVISI_ID" ]; then
  WHOAMI=$(whoami)
else
  WHOAMI=$AVISI_ID
fi
 
STS=$(keybase fs read "/keybase/private/${KEY_BASE_USER_ID}/.${ACCOUNT}.json")
 
RENEW=1
if [ ! -z "${STS}" ]; then
  TOKEN_EXPIRES=$(echo "${STS}" | jq -r '.Credentials.Expiration')
  DATE_NOW=$(date +%s)
  DATE_TOKEN=$(date -d "${TOKEN_EXPIRES}" +%s)
  if [ "$DATE_TOKEN" -ge "$DATE_NOW" ]; then
    echo Found a valid MFA token for account "${ACCOUNT}"
    RENEW=0
  fi
fi
 
if [ ${RENEW} -eq 1 ]; then
  echo -n "Enter MFA code for ${WHOAMI}@${ACCOUNT}: "
  read -r TOKENCODE
  STS=$(aws sts get-session-token --serial-number "arn:aws:iam::${ACCOUNT}:mfa/${WHOAMI}" --token-code "${TOKENCODE}")
  echo "${STS}" | keybase fs write "/keybase/private/${KEY_BASE_USER_ID}/.${ACCOUNT}.json"
fi
 
export AWS_ACCESS_KEY_ID=$(echo "${STS}" | jq -r '.Credentials.AccessKeyId')
export AWS_SECRET_ACCESS_KEY=$(echo "${STS}" | jq -r '.Credentials.SecretAccessKey')
export AWS_SESSION_TOKEN=$(echo "${STS}" | jq -r '.Credentials.SessionToken')

The key functionality of this script is on:

  • line 9: We hope to find an existing session token file here.
  • lines 12-20: If there was an existing session, we figure out if it is still valid.
  • lines 22-27: If we need to fetch a new session token, ask for an MFA code and request a new session code.

This script assumes the AWS SDK to be installed as well as the jq utility. Next to that, we use Keybase to encrypt / decrypt that session file.

Some considerations about this script:

Keybase

Not everybody might appreciate the use of Keybase. Our team was ok with it, so we left it at that. If you don't want that, or you are not using Keybase, you can cache the session token somewhere else (possibly encrypting/decrypting it through Keybase or another pgp setup).

Session time

The above script assumes that it's perfectly ok to have a pretty long open session time. If you want to make things more secure, you can alter the script to use a smaller window. Pass `--duration-seconds` to `get-session-token`

Force MFA for API calls

You can also require that a user be authenticated using an MFA to perform particular API actions by using the aws:MultiFactorAuthPresent or aws:MultiFactorAuthAge conditions in an IAM policy. When you have educated your developers, this is the only conclusive way to ensure that that static keys are no longer used.

Different workflow

You might argue that it is better for a developer to authenticate on his own request, instead of being forced by a .envrc script. With .envrc, you have to grab your phone as soon as you change directory, maybe you were just looking for something or need time to make code changes first. It depends on your workflow, investigate what works best, maybe integrate the script in a Makefile!

Related blogs

Did you enjoy reading?

Share this blog with your audience!