Generating Dynamic Programmatic API Access Tokens for AWS CLI on macOS

Aaron Louks
6 min readJan 6, 2023

--

In my previous tutorial, I showed how to Manage a multi-account AWS organization with Okta and IAM Identity Center.

With Okta being the Identity Provider (IdP) for AWS and using IAM Identity Center, we’re able to centralize the fine grain permission control of AWS users from Okta. This also gives us the ability to generate programmatic API access tokens for AWS that are session limited, instead of using indefinite tokens.

Why this is good from a security perspective:

  1. We don’t have to remember to remove programmatic access from every account when revoking access. When access is removed in Okta, the user deactivation is pushed down to IAM Identity Center via SCIM and disables the user, making the session no longer valid.
  2. If an access_key or session_token is leaked, it only has a short lifespan (however long the session is defined as). Also, the session can be invalidated by removing specific user access.
  3. We have an obvious and readable central overview of how access is granted across the organization.

Note: These instructions will technically work with any IdP configured with AWS IAM Identity Center. However, the assumption is that you’re using Okta.

Requirements

This guide is created for macOS clients. You’ll need the following installed:

  1. Basic knowledge of Bash and using the CLI on macOS
  2. Python3 (AWS CLI requires v3.11.1+)
  3. AWS CLI v2.9.12+ — https://aws.amazon.com/cli/

Steps

1. On our AWS landing page for authenticating and choosing accounts, expand the field for the account you want to work with:

2. On the landing page, you’ll be able to see what accounts you have access to and what roles you have available for each account. Take note of the account numbers and role names.

3. We’ll need to configure our various profiles in ~/.aws/config
I’m using the FDX1 account as an example. Add this to your config:

[profile fdx1-Administrator] 
cli_pager = ''
output = json
sso_start_url = <aws_sso_start_url>
sso_region = us-east-1
sso_account_id = <aws_account_id>
sso_role_name = AdministratorAccess

[profile fdx1-ViewOnly]
cli_pager = ''
output = json
sso_start_url = <aws_sso_start_url>
sso_region = us-east-1
sso_account_id = <aws_account_id>
sso_role_name = ViewOnlyAccess

Notes:
The [profile <name>] can be anything, but I think its helpful to keep the naming schemes consistent with {account-permission_level}

cli_pager = option is important so the output is returned to stdout and not captured by less

The sso_start_url should be the value defined in step 31 of the previous tutorial.

Add as many profiles as you need for programmatic access to your accounts. Take note of the profile names so you can reference them later.

4. You’ll need to put the following bash script in a location that is in your $PATH so that its executable from anywhere. You can determine what locations are in your execution path with echo $PATH

I created a ~/bin directory and added that location to my PATH environment variable. I added the following to my .zshrc file, but you’re welcome to do what you prefer in this case, so long as the bash script is executable and in your $PATH.

export PATH="/Users/<user>/bin:$PATH"

Create the script in your preferred bin directory. You can name it whatever you like, I named mine get_aws_creds

vim ~/bin/get_aws_creds

#!/bin/bash

die () {
echo >&2 "$@"
exit 1
}

opts () {
while getopts a:R:p:r: flag
do
case "${flag}" in
a) ACCOUNT=${OPTARG};;
R) REGION=${OPTARG};;
p) PROFILE=${OPTARG};;
r) ROLE=${OPTARG};;
esac
done

if [ -z $ACCOUNT ] ; then
echo "Account number required. Use -a flag" && die;
fi
if [ -z $REGION ] ; then
echo "Region required. Use -R flag" && die;
fi
if [ -z $PROFILE ] ; then
echo "Profile name required. Use -p flag" && die;
fi
if [ -z $ROLE ] ; then
echo "Role name required. Use -r flag" && die;
fi
}

getcreds () {
# Remove old profile from credentials file and remove sso cache
sed -i -e "/$PROFILE/,+4d" ~/.aws/credentials
find -E ~/.aws/sso/cache/ -regex '\/Users\/([a-zA-Z0-9_\-]*)\/\.aws\/sso\/cache\/\/([a-fA-F0-9]{40}).json' -exec rm -f {} \;

aws sso login --profile $PROFILE
TOKEN=`for i in $(find -E ~/.aws/sso/cache/ -regex '\/Users\/([a-zA-Z0-9_\-]*)\/\.aws\/sso\/cache\/\/([a-fA-F0-9]{40}).json'); do grep accessToken $i; done | python3 -c "import sys, json; print(json.load(sys.stdin)['accessToken'])"`

# Get new creds and write to credentials file
aws sso get-role-credentials --profile $PROFILE --account-id $ACCOUNT --role-name $ROLE --access-token $TOKEN --region $REGION | \
python3 -c "
import sys, json, os;
data = json.load(sys.stdin)['roleCredentials'];
access_key_id = data['accessKeyId'];
secret_access_key = data['secretAccessKey'];
session_token = data['sessionToken'];
creds_file = open(os.path.expanduser('~')+'/.aws/credentials', 'a');
creds_file.write('\n[$PROFILE]\n');
creds_file.write('aws_access_key_id='+access_key_id+'\n');
creds_file.write('aws_secret_access_key='+secret_access_key+'\n');
creds_file.write('aws_session_token='+session_token+'\n');
creds_file.close();"
}

opts $@
getcreds $@

Note: I ended up using a mixture of python in my bash script because I didn’t know of a pure bash method for parsing JSON without having to install yet another library. If you’ve got a better solution, let me know in the comments!

5. Make it executable

chmod +x ~/bin/get_aws_creds

6. Now you can execute the script with parameters to specify which account and which role to create credentials for. You would need to define your own profile name, but in this example I’m using the fdx1-Administrator profile:

get_aws_creds -a <aws_account_id> -R us-east-1 -p fdx1-Administrator -r AdministratorAccess

This will open your default browser window and will perform redirects to Okta if you need to authenticate.

After you click Allow, you will have a valid token/session and the credentials will be appended to your credentials file. You can verify it with this command: cat ~/.aws/credentials

7. To simplify that final command, we create a bash alias to something that’s easy for you to remember. I added this to my ~/.zshrc file:

alias fdx1="get_aws_creds -a <aws_account_id> -R us-east-1 -p fdx1-Administrator -r AdministratorAccess"

Don’t forget to source the file! source ~/.zshrc

Now anytime you’re running terraform / cloud formations / kubectl / etc commands against an AWS environment, you’ll be able to use these profiles. The tokens are temporary and will not be valid after they expire.

For example, if you try to use kubectl with a config that references this profile and your session token is expired, you’ll run into a notice like this:

The SSO session associated with this profile has expired or is otherwise invalid. To refresh this SSO session run aws sso login with the corresponding profile.

So you just need to execute the bash alias before executing the kubectl command.

From a user perspective, this process is slightly more inconvenient than having a static aws_access_key because you need to be mindful of executing the bash alias command to get a renewed session_token before you’re able to use scripts that reference the AWS profile name. However, the security benefits of not having a static aws_access_key that could be stolen are worth the extra step.

From an administrator’s perspective, this process reduces the probability of attackers using stolen credentials. It also allows administrators to react faster by disabling an SSO account in case of a breach. And finally, it will prevent/reduce orphaned aws_access_keys when users leave so it ties up loose ends and keeps access tidy across multiple accounts.

Its always a tradeoff between Security and Convenience. ⚖️

--

--

No responses yet