Mastering Multiple AWS Accounts with AWS CLI: Navigating Role Delegation, MFA, and Automated Login Scripts
Tags: aws, security, mfa, cli
Published: 2023-11-15 09:03:43

In my role as a platform engineer, I often work with multiple AWS accounts. This can get tricky, especially when setting up Terraform or writing scripts for different account resources. I've learned some helpful tips and tricks along the way, which may be helpful to you. 

For multiple accounts access [role delegation](https://docs.aws.amazon.com/IAM/latest/UserGuide/tutorial_cross-account-with-roles.html) is usually used. This is especially helpful, because it is easy to manage, it adds required granularity of access to the different resources and allows user to have only one pair of secrets on computer. It is also [easy to configure for AWS CLI](https://docs.aws.amazon.com/cli/latest/userguide/cli-configure-role.html) and it is supported by Terraform. 

It is a bit more difficult to configure when [Multi-Factor Authentication (MFA) is enabled](https://aws.amazon.com/iam/features/mfa)  for the additional security. MFA is great for security - it makes sure this is you who are trying to use credentials, not anyone else uses stolen creds. On practice, it means you need to enter some One-Time Password (OTP) code generated for you by some program (like [Google Authenicator](https://play.google.com/store/apps/details?id=com.google.android.apps.authenticator2&pcampaignid=web_share)) or by separate hardware (like [Yubikey](https://www.yubico.com) or [Protectimus](https://www.protectimus.com)). There are other solutions on a market, but I have experience with these only. 

For OTP I usually use Google Authenicator but using it with some short-lived session is very annoying - you have to type code from phone every time. For these cases I use Yubikey to generate OTP codes, or I use [1Password](https://support.1password.com/one-time-passwords/) when I can't use Yubikey. 1Password is a bit less secure option for this case - because it can be used if someone get access to your computer and have your password, while with GA or hardware token attacker must have access to them too. 

- [How to configure Yubikey as AWS MFA](https://aws.amazon.com/blogs/security/use-yubikey-security-key-sign-into-aws-management-console/)
- [How to configure 1Password for AWS MFA](https://support.1password.com/one-time-passwords/)

Once you have it configured, let's start from configuring AWS CLI and login script. Login script will generate temporary credentials which may be used to access all other account. The flow of it is simple: 
- Retrieve new set of temporary credentials using [sts get-session-token](https://docs.aws.amazon.com/cli/latest/reference/sts/get-session-token.html) AWS CLI command using credentials from account where user is created
- Set temporary credentials to `~/.aws/credentials` profile
- use this profile as `source_profile` with multiple AWS profiles

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/7aetuhm997674uyfxooh.png)

First of all, you need to add profile where your user was created, and credentials to it:

`~/.aws/config`
```ini
[profile account-where-user-congured]
region = us-east-1
```

`~/.aws/credentials`
```ini
[account-where-user-configured]
aws_access_key_id        = *******************
aws_secret_access_key    = *********************
```

Add empty profile where generated credentials will be stored.
`~/.aws/credentials`
```ini
[account-where-user-configured]
aws_access_key_id        = *******************
aws_secret_access_key    = *********************

[root-mfa]
```

and, of course, add other accounts where user have access with assumed role:
`~/.aws/config`
```ini
[profile account-where-user-configured]
region = us-east-1
output = json

[profile some-account-you-can-assume-role-in]
region = us-east-1
source_profile = root-mfa
role_arn = arn:aws:iam::0123456789:role/my-awesome-role

[profile some-other-account-you-can-assume-role-in]
region = us-east-1
source_profile = root-mfa
role_arn = arn:aws:iam::0123456789:role/my-awesome-role
....
```

Now, let's make script to retrieve temporary credentials (based on script from here [Script to generate AWS STS token · GitHub](https://gist.github.com/ogavrisevs/2debdcb96d3002a9cbf2)). 

`awslogin.sh`
```bash
#!/bin/bash
#
# Sample for getting temp session token from AWS STS
#
# aws --profile youriamuser sts get-session-token --duration 3600 \
# --serial-number arn:aws:iam::012345678901:mfa/user --token-code 012345
#
# Based on : https://github.com/EvidentSecurity/MFAonCLI/blob/master/aws-temp-token.sh
#

set -e
AWS_CLI=`which aws`

if [ $? -ne 0 ]; then
  echo "AWS CLI is not installed; exiting"
  exit 1
else
  echo "Using AWS CLI found at $AWS_CLI"
fi

AWS_USER_PROFILE=account-where-user-configured
AWS_2AUTH_PROFILE=root-mfa
ARN_OF_MFA=arn:aws:iam::account-where-user-configured-id:mfa/yubikey-mfa-arn-name
DURATION=129600

read AWS_ACCESS_KEY_ID AWS_SECRET_ACCESS_KEY AWS_SESSION_TOKEN <<< \
$( aws --profile $AWS_USER_PROFILE sts get-session-token \
  --duration $DURATION  \
  --serial-number $ARN_OF_MFA \
  --token-code $(ykman oath code -s $ARN_OF_MFA) \
  --output text  | awk '{ print $2, $4, $5 }')

if [ -z "$AWS_ACCESS_KEY_ID" ]
then
  exit 1
fi

`aws --profile $AWS_2AUTH_PROFILE configure set aws_access_key_id "$AWS_ACCESS_KEY_ID"`
`aws --profile $AWS_2AUTH_PROFILE configure set aws_secret_access_key "$AWS_SECRET_ACCESS_KEY"`
`aws --profile $AWS_2AUTH_PROFILE configure set aws_session_token "$AWS_SESSION_TOKEN"`
```

Note `$(ykman oath code -s $ARN_OF_MFA)` line - this is where [ykman](https://developers.yubico.com/yubikey-manager/) will ask you to touch Yubikey. That pretty much it. Now you can login just once and use different AWS profiles without need to re-login. 

```sh
⋊>./awslogin.sh 
Using AWS CLI found at /home/linuxbrew/.linuxbrew/bin/aws
Touch your YubiKey...
⋊>aws s3 ls --profile some-account-you-can-assume-role-in
# some buckets will be shown
# you can simultaneously use it for all other accounts you can assume role in
# without need to switch to it or use env variables
⋊>aws s3 ls --profile some-other-account-you-can-assume-role-in
# some buckets will be shown
```

Configuration with 1Password OTP generation is similar — but instead of `ykman` [op](https://1password.com/downloads/command-line/) should be used to retrieve a OTP code. If we are going to use 1Password for auth anyways, it is reasonable to get rid of the local secret for `account-where-user-configured` stored in plain text in `~/.aws/credentials`. 

Set up credentials in 1Password and remove `[profile account-where-user-configured]` from `~/.aws/credentials` and `~/.aws/config`.
Credentials for accounts where user is set up will be taken from 1Password and used in script instead of ones in `~/.aws/credentials`. We also will use OTP generated by 1Password

```sh
#!/bin/bash
#
# Sample for getting temp session token from AWS STS
#
# aws --profile youriamuser sts get-session-token --duration 3600 \
# --serial-number arn:aws:iam::012345678901:mfa/user --token-code 012345
#
# Based on : https://github.com/EvidentSecurity/MFAonCLI/blob/master/aws-temp-token.sh
#

set -e
AWS_CLI=`which aws`

if [ $? -ne 0 ]; then
  echo "AWS CLI is not installed; exiting"
  exit 1
else
  echo "Using AWS CLI found at $AWS_CLI"
fi

AWS_USER_PROFILE=account-where-user-configured
AWS_2AUTH_PROFILE=root-mfa
ARN_OF_MFA=arn:aws:iam::account-where-user-configured-id:mfa/yubikey-mfa-arn-name
DURATION=129600

read AWS_ACCESS_KEY_ID AWS_SECRET_ACCESS_KEY AWS_SESSION_TOKEN <<< \
$(env AWS_ACCESS_KEY_ID=$(op read "op://Private/<your-account-secret>/access key id") \
  AWS_SECRET_ACCESS_KEY=$(op read "op://Private/<your-account-secret>/secret access key") \
  aws sts get-session-token \
  --duration $DURATION  \
  --serial-number $ARN_OF_MFA \
  --token-code $(--token-code $(op read "op://Private/<your-account-secret>/one-time password")) \
  --output text  | awk '{ print $2, $4, $5 }')

if [ -z "$AWS_ACCESS_KEY_ID" ]
then
  exit 1
fi

`aws --profile $AWS_2AUTH_PROFILE configure set aws_access_key_id "$AWS_ACCESS_KEY_ID"`
`aws --profile $AWS_2AUTH_PROFILE configure set aws_secret_access_key "$AWS_SECRET_ACCESS_KEY"`
`aws --profile $AWS_2AUTH_PROFILE configure set aws_session_token "$AWS_SESSION_TOKEN"`
```

TIP: You can use it with Terraform/Terragrun by setting environment variable temporary, just for the command:
```sh
env AWS_PROFILE=your-profile-name terraform init
```

TIP: You can export generated credentials to environment variable (for cases when aws profiles is not supported) by the [following command]((https://stackoverflow.com/questions/40852223/is-there-a-way-to-export-an-aws-cli-profile-to-environment-variables)):
```sh
eval "$(aws configure export-credentials --profile your-profile-name --format env)"
```

TIP: you also can pass credentials to docker container on run without storing them in separate file or export to your environment:
```sh
docker run --env-file <(aws configure export-credentials --profile your-profile-name --format env-no-export) ...
```