├── aws-profiles example ├── account1 └── main ├── setup.sh ├── profile-additions.source_me ├── Using MFA and Roles in AWS.md ├── README.md └── aws-token-or-role-refresh.sh /aws-profiles example/account1: -------------------------------------------------------------------------------- 1 | [account1] 2 | # This is an example of an IAM role credentials profile file called account1, using an IAM role name "account1role". This uses the credentials from the "main" profile to get role session credentials. 3 | 4 | region = us-east-1 # optional 5 | account_id = 0987654321 6 | role_name = myadminrole # the IAM role name to assume. 7 | master_profile = main # refers to the profile name for the IAM user used to assume this role -------------------------------------------------------------------------------- /aws-profiles example/main: -------------------------------------------------------------------------------- 1 | [mainuser] 2 | # This is an example of an IAM user credentials profile called mainuser. These credentials are enough to request STS credentials (using MFA) that then have rights to request role credentials without using MFA. 3 | 4 | region = us-east-1 # optional 5 | aws_access_key_id = AKIAJHEGCHEXAMPLE 6 | aws_secret_access_key = fSU2a3BdmxVX5cX0+HFw6IBcNaEXAMPLEKEY 7 | mfa_id = arn:aws:iam::12342567890:mfa/mainuser 8 | account_id = 1234256789 9 | mfa_type = virtual # optional. Possible values: "virtual" (m, "yubikey". defaults to prompting interactively 10 | -------------------------------------------------------------------------------- /setup.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -ueo pipefail 4 | set -x 5 | 6 | REPLY='' 7 | while [[ -z $REPLY ]]; do 8 | read -p "Where do you want to store the scripts? (ex. ~/Documents/aws-scripts) " -r 9 | if [[ -z $REPLY ]]; then 10 | echo "You need to enter something." 11 | else 12 | PARENTDIR="$( unset CDPATH && cd "${REPLY/#\~/$HOME}" && pwd -P )" 13 | fi 14 | done 15 | 16 | if ! [ -d "${PARENTDIR}" ]; then 17 | echo "'${PARENTDIR}' doesn't exist. Creating it..." 18 | mkdir -p "${PARENTDIR}" 19 | fi 20 | 21 | cd "${PARENTDIR}" 22 | git clone https://github.com/seren/aws-token-refresh.git 23 | cd aws-token-refresh 24 | DIR="${PARENTDIR}/aws-token-refresh" 25 | 26 | mkdir -p "${HOME}/.aws/aws-profiles" 27 | chmod 700 "${HOME}/.aws/aws-profiles" 28 | 29 | cp profile-additions.source_me profile-additions.source_me.bak 30 | # shellcheck disable=2016 31 | echo 'AWS_SCRIPTS_DIR="${HOME}/'"${DIR}" > profile-additions.source_me 32 | sed '1d' profile-additions.source_me.bak >> profile-additions.source_me 33 | rm profile-additions.source_me.bak 34 | 35 | echo "Adding '. "'"'"${DIR}"'"'"/profile-additions.source_me' to ${HOME}/.bash_login" 36 | echo "If you don't use bash, add it to the your shell's login file." 37 | echo '. "'"${DIR}"'/profile-additions.source_me"' >> "${HOME}/.bash_login" 38 | 39 | open . 40 | -------------------------------------------------------------------------------- /profile-additions.source_me: -------------------------------------------------------------------------------- 1 | AWS_SCRIPTS_DIR="${HOME}/PATH-TO-WHERE-YOU-DOWNLOADED/aws-token-refresh" 2 | 3 | AWS_CONF_DIR="${HOME}/.aws" 4 | 5 | clear_aws_env () 6 | { 7 | unset AWS_SECURITY_TOKEN_EXPIRATION 8 | unset AWS_SESSION_TOKEN 9 | unset ADMIN_AWS_SECURITY_TOKEN 10 | unset AWS_MFA_SN 11 | unset ADMIN_AWS_MFA_SN 12 | unset ADMIN_AWS_KEY 13 | unset ADMIN_AWS_SECRET 14 | unset ADMIN_AWS_KEY_ROOT 15 | unset ADMIN_AWS_SECRET_ROOT 16 | unset ADMIN_AWS_ACCT 17 | unset AWS_ACCESS_KEY_ID 18 | unset AWS_SECRET_ACCESS_KEY 19 | unset AWS_SECURITY_TOKEN 20 | } 21 | 22 | token () 23 | { 24 | # Note: If no argument given, just lists the available profile names 25 | "${AWS_SCRIPTS_DIR}/aws-token-or-role-refresh.sh" token "$@" 26 | } 27 | 28 | 29 | awsenv () 30 | { 31 | # If no argument given, use the last generated environment variables 32 | # Useful for populating credentials in multiple terminal windows quickly 33 | if [ "$#" -eq 0 ]; then 34 | clear_aws_env 35 | # shellcheck disable=SC1090 36 | . "${AWS_CONF_DIR}/env-files/current-env.txt" && \ 37 | ENVNAME="${GLOBUS_ACCOUNT_NAME}" 38 | echo "AWS environment variables populated for '${ENVNAME}'" 39 | return 40 | fi 41 | 42 | ENVNAME="${1}" 43 | if [ ! -f "${AWS_CONF_DIR}/aws-profiles/${ENVNAME}" ]; then 44 | echo "No such environment" 45 | return 1 46 | fi 47 | 48 | clear_aws_env 49 | # shellcheck disable=SC1090 50 | "${AWS_SCRIPTS_DIR}/aws-token-or-role-refresh.sh" awsenv "${ENVNAME}" && \ 51 | . "${AWS_CONF_DIR}/env-files/current-env.txt" && \ 52 | echo "AWS environment variables populated for '${ENVNAME}'" 53 | } 54 | 55 | list_aws_profiles() 56 | { 57 | echo "Avaliable profiles are:" 58 | find -L "${AWS_CONF_DIR}/aws-profiles" -type f ! -name '.*' | sed 's#'"${AWS_CONF_DIR}/aws-profiles"'/\(.*\)#\1#' | paste -sd ' ' - 59 | } 60 | -------------------------------------------------------------------------------- /Using MFA and Roles in AWS.md: -------------------------------------------------------------------------------- 1 | # Using MFA and Roles in AWS 2 | 3 | 4 | **TL/DR:** For security, user credentials require an MFA token and can only be used to request temporary (session) credentials. Using these temporary credentials, users are allowed perform some actions and to assume roles in other accounts. These assumed roles are what grant the final set of credentials and permissions users need to perform their duties (including full administration). 5 | 6 | The following examples show how to retrieve and use temporary session credentials, and how to retrieve and use role (cross-account in our example) session credentials, with and without MFA. The examples use shell code, but the concepts hold true for other languages. 7 | 8 | 9 | ## Using session credentials with MFA (no role assumption) 10 | 11 | Our user accounts have policies applied which restrict the actions that can be performed without MFA. 12 | 13 | ~~~bash 14 | # This will fail: 15 | 16 | export AWS_ACCESS_KEY_ID="AKIAJFFBSWF5EXAMPLE" 17 | export AWS_SECRET_ACCESS_KEY="LcUFzdfyTzNAo+vuuHBxUpGwrNTJrrEXAMPLEKEY" 18 | 19 | aws --region us-east-1 ec2 describe-instances 20 | ~~~ 21 | ~~~ 22 | An error occurred (UnauthorizedOperation) when calling the DescribeInstances operation: You are not authorized to perform this operation. 23 | ~~~ 24 | 25 | You can use normal credentials, with an mfa token, to get temporary session credentials. These have the MFA attribute set, so the policy will allow additional API calls that would otherwise be denied. 26 | 27 | First, get the temporary session credentials: 28 | 29 | ~~~bash 30 | MFA_TOKEN=$(mfa aws-myorg-users-jones -q) 31 | MFA_DEVICE="arn:aws:iam::694237938522:mfa/jones" 32 | 33 | # Get session credentials from STS 34 | JSON=$(aws sts get-session-token --serial-number ${MFA_DEVICE} --token-code ${MFA_TOKEN}) 35 | 36 | echo $JSON 37 | { 38 | "Credentials": { 39 | "SecretAccessKey": "GG+xvoLF0HKCV8DGhk1lQCss6eu4vEXAMPLEKEY", 40 | "SessionToken": "FQoDeXdzEBw...", 41 | "Expiration": "2017-11-23T06:36:57Z", 42 | "AccessKeyId": "ASIAIGMQ2ZKW4EXAMPLE" 43 | } 44 | } 45 | 46 | # Export the new session credentials 47 | export AWS_ACCESS_KEY_ID=`echo ${JSON} | jq '.Credentials.AccessKeyId' | tr -d '"'` 48 | export AWS_SECRET_ACCESS_KEY=`echo ${JSON} | jq '.Credentials.SecretAccessKey' | tr -d '"'` 49 | export AWS_SESSION_TOKEN=`echo ${JSON} | jq '.Credentials.SessionToken' | tr -d '"'` 50 | 51 | ~~~ 52 | 53 | Then use the temporary session credentials to make the API calls: 54 | 55 | ~~~bash 56 | # With the new session credentials, this will succeed: 57 | aws --region us-east-1 ec2 describe-instances 58 | 59 | { 60 | "Reservations": [ 61 | { 62 | "Instances": [ 63 | { 64 | ... 65 | ~~~ 66 | 67 | 68 | Note: There are three components to session credentials: 69 | 70 | ~~~bash 71 | export AWS_ACCESS_KEY_ID=AKIAI44QH8DHBEXAMPLE 72 | export AWS_SECRET_ACCESS_KEY=wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY 73 | export AWS_SESSION_TOKEN=AQoDYXdzEJr... 74 | ~~~ 75 | 76 | 77 | ## Assuming roles (cross-account or not) 78 | 79 | There are two ways to assume a role, depending on the account restrictions: 80 | 81 | 1. If MFA is required for the regular IAM credentials to call `sts assume-role`: `aws sts assume-role --role-arn ${ROLE_ARN} --role-session-name "${LOGNAME}-${DATESTAMP}" --serial-number ${MFA_DEVICE} --token-code ${MFA_TOKEN}` 82 | 1. If the regular IAM credentials can call `sts assume-role` without MFA: `aws sts assume-role --role-arn ${ROLE_ARN} --role-session-name "${LOGNAME}-${DATESTAMP}"` 83 | 84 | If you are already using session credentials that were obtained using MFA, you can use the second option since the MFA attribute will carry over (you don't have to use MFA a second time). 85 | 86 | ~~~bash 87 | # Assuming our current environment credentials (eg. AWS_*) are allowed to call assume-role 88 | ROLE_ARN="arn:aws:iam::303064072661:role/myorg_admin" 89 | DATESTAMP=$(date +"%Y-%m-%d-%H-%M-%S") 90 | 91 | JSON=$(aws sts assume-role --role-arn ${ROLE_ARN} --role-session-name "${LOGNAME}-${DATESTAMP}") 92 | 93 | echo $JSON 94 | { 95 | "AssumedRoleUser": { 96 | "AssumedRoleId": "AROAJ2O5CEKIUVIUOXXXX:jones-2017-11-22-14-47-09", 97 | "Arn": "arn:aws:sts::303064072661:assumed-role/myorg_admin/jones-2017-11-22-14-47-09" 98 | }, 99 | "Credentials": { 100 | "SecretAccessKey": "JeXc31hsWEvT47JDR7HtHBjkiBjsvEXAMPLEKEY", 101 | "SessionToken": "FQoDYXdzECAaD...", 102 | "Expiration": "2017-11-22T23:47:26Z", 103 | "AccessKeyId": "ASIAJJR44GIKEXAMPLE" 104 | } 105 | } 106 | 107 | # Export the session credentials for the role 108 | export AWS_ACCESS_KEY_ID=`echo ${JSON} | jq '.Credentials.AccessKeyId' | tr -d '"'` 109 | export AWS_SECRET_ACCESS_KEY=`echo ${JSON} | jq '.Credentials.SecretAccessKey' | tr -d '"'` 110 | export AWS_SESSION_TOKEN=`echo ${JSON} | jq '.Credentials.SessionToken' | tr -d '"'` 111 | 112 | ~~~ 113 | 114 | *Author: Seren Thompson* 115 | 116 | *Edition: 2019-01-23* -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## Overview 2 | 3 | This script retrieves temporary IAM user credentials (with MFA) or role credentials from AWS STS, and generates or updates standard `credentials` and `config` files for use with AWS CLI, Boto, or anything else that uses AWS profiles. 4 | 5 | 6 | 7 | ## Requirements 8 | 9 | - Bash 10 | - AWS CLI tools (to make STS calls) 11 | - https://docs.aws.amazon.com/cli/latest/userguide/installing.html 12 | - An MFA (multi-factor authentication) token generator such as: 13 | - [Google Authenticator](https://www.google.com/search?q=google+authenticator) 14 | - [Authy](https://authy.com/download/) 15 | - Something else (there are [many options](https://www.google.com/search?q=2-factor+authentication+download) including CLI-based tools. Keywords to search for include: TOTP, MFA, and 2FA) 16 | 17 | 18 | 19 | ## Usage 20 | - To get credentials: `token ` 21 | - To export credentials into environment variables: `awsenv ` 22 | 23 | 24 | 25 | ## Getting started 26 | 27 | **Warning:** The `config` file is recreated each time. Any customizations should be done in the `credentials` file or the profile files. 28 | 29 | ### File and shell changes 30 | 31 | Run the `setup.sh` script or perform the following four steps manually: 32 | 33 | 1. Clone the git repo into a local directory (we'll be using `~/aws-token-refresh` in these examples): 34 | 35 | ~~~ 36 | git clone https://github.com/seren/aws-token-refresh.git ~/aws-token-refresh 37 | ~~~ 38 | 39 | 2. Create a directory for credentials (most tools expect them to be in `~/.aws`). Create a `aws-profiles` directory within: 40 | 41 | ~~~ 42 | mkdir -p ${HOME}/.aws/aws-profiles 43 | chmod 700 ${HOME}/.aws/aws-profiles 44 | ~~~ 45 | 46 | 3. In `profile-additions.sh`, update the `AWS_SCRIPTS_DIR` value file to the directory containing the scripts (ex. `${HOME}/aws-token-refresh`). 47 | 48 | 4. In your shell's login profile (usually `.bash_profile`, if you use bash), source `profile-additions.source_me`: 49 | 50 | ~~~ 51 | echo 'source "${HOME}/aws-token-refresh/profile-additions.source_me"' >> ${HOME}/.profile 52 | ~~~ 53 | 54 | 55 | ### Create the IAM profiles 56 | 57 | Create profile files in `~/.aws/aws-profiles/`. These files are what are used to generate the `credentials` file (for the aws cli tools) and `config` file (formated slightly differently for boto). They should have at least the following information: 58 | 59 | Note: You can also create profiles for non-IAM accounts or accounts that don't use MFA. They'll be added to the `credentials` and `config` files. 60 | 61 | - **For an IAM user profile:** 62 | 63 | ~~~ 64 | [mainuser] 65 | # An optional comment. Blah 66 | aws_access_key_id = AKIAJHEGCHEXAMPLE 67 | aws_secret_access_key = fSU2a3BdmxVX5cX0+HFw6IBcNaEXAMPLEKEY 68 | mfa_id = arn:aws:iam::12342567890:mfa/mainuser 69 | account_id = 12342567890 70 | region = us-east-1 71 | mfa_type = virtual 72 | ~~~ 73 | 74 | - `[profilename]` - The nickname you want to give this profile. It needs to match the filename. 75 | - `# Blah` - You may place comment lines within the profile 76 | - `aws_access_key_id` - Key for IAM user (starts with `AKAI`) 77 | - `aws_secret_access_key` - Key secret for IAM user 78 | - `mfa_id` - The ARN of the virtual MFA device from the IAM user credentials page 79 | - `account_id` - Optional, but useful to avoid confusion when using multiple profile files 80 | - `region` - Optional. Useful if you always use the same region, or want to use different nicknames for different regions 81 | - `mfa_type` - Optional. If not specified, user is prompted for the MFA token. Possible values: 82 | - `virtual` (an MFA-generating app, specified as `MFA_PROGRAM` in `aws-token-or-role-refresh.sh`) 83 | - `yubikey` (hardware token) 84 | 85 | - **For an IAM role profile:** 86 | 87 | ~~~ 88 | [profilename] 89 | # An optional comment. Blah 90 | role_name = myadminrole 91 | account_id = 12342567890 92 | master_profile = mainuser 93 | region = us-east-1 94 | ~~~ 95 | 96 | - `[profilename]` - The nickname you want to give this profile. It needs to match the filename. 97 | - `# Blah` - You may place comment lines within the profile 98 | - `role_name` - The name of the IAM role to assume. NOTE: The presence of this is also used to determine whether a profile is a "user" profile or a "role" profile. 99 | - `account_id` - The account ID that the role is in. Used, along with the `role_name`, to form the role ARN 100 | - `master_profile` - The name of the user profile which has permissions to assume this role. 101 | - `region` - Optional. Useful if you always use the same region, or want to use different nicknames for different regions 102 | 103 | 104 | 105 | ## Notes 106 | 107 | - The script only modifies individual profile sections of the `credentials` file, so you can add extra profiles to the `credentials` file manually without them being overwritten. 108 | 109 | - The `config` file is recreated from the `credentials` file during each run, so any customization should be done in the `credentials` file. 110 | 111 | - The `[profilename]` must match the profile filename, but doesn't need to match the IAM role name. 112 | 113 | 114 | 115 | ## AWS Web Console Tips 116 | 117 | If you've just started using roles, the AWS Web Console has a nice feature where you can switch between them. It can remember up to 5 recently switched-to roles. To add a role, choose the `Switch Role` option in the user menu in the upper-right menu bar of the web console page, or enter a url like so: 118 | 119 | [https://signin.aws.amazon.com/switchrole?account=1234567890&roleName=myadminrole&displayName=account1-admin](https://signin.aws.amazon.com/switchrole?account=1234567890&roleName=myadminrole&displayName=account1-admin) 120 | 121 | For more than 5 roles, you may want to check out browser extensions/add-ons such as [aws-extend-switch-roles](https://github.com/tilfin/aws-extend-switch-roles) 122 | 123 | -------------------------------------------------------------------------------- /aws-token-or-role-refresh.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | # Wrapper for aws cli to handle MFA session tokens 4 | 5 | set -euo pipefail 6 | 7 | ################# 8 | # Vars 9 | 10 | TOKEN_PREEXPIRATION_HOURS=2 # Hours before a token expires to try to get a fresh one instead of reusing it (tokens expire after 12 hours by default) 11 | # Note: This doesn't apply to role tokens which expire after one hour max. 12 | 13 | # How much feedback to display: 14 | # LOG_LEVEL=ERROR 15 | # LOG_LEVEL=WARN 16 | LOG_LEVEL=INFO 17 | # LOG_LEVEL=DEBUG 18 | 19 | # Paths 20 | AWS_CLI="$(which aws)" 21 | AWS_DIR="${HOME}/.aws" 22 | CREDENTIALS_FILE="${AWS_DIR}/credentials" 23 | BOTO_CONFIG_FILE="${AWS_DIR}/config" 24 | CREDENTIALS_SOURCE_DIR="${AWS_DIR}/aws-profiles" 25 | 26 | # If you have a local MFA token generator that accepts profile names, you can specify it here 27 | MFA_PROGRAM="" 28 | 29 | PLATFORM=$(uname) 30 | 31 | 32 | ########## 33 | # set up logging and debugging output 34 | XTRACE="${XTRACE:-0}" 35 | 36 | if [ "$XTRACE" == "1" ] || [ "$XTRACE" == "true" ] ; then 37 | set -x 38 | PS4="$(tput setaf 2)COMMAND: $(tput sgr0)" 39 | fi 40 | 41 | ## Bash < 4 doesn't have associative arrays, so we can't use this 42 | # declare -A log_levels 43 | # log_levels=([DEBUG]=0 [INFO]=1 [WARN]=2 [ERROR]=3) 44 | function log_levels () 45 | { 46 | case $1 in 47 | DEBUG) return 0 ;; 48 | INFO) return 1 ;; 49 | WARN) return 2 ;; 50 | ERROR) return 3 ;; 51 | *) echo "Invalid log level: $1" && exit 1;; 52 | esac 53 | } 54 | 55 | function loggable_timestamp { 56 | date "+%F %T" 57 | } 58 | 59 | function check_log_level { 60 | local global_log_level_name=${LOG_LEVEL:-INFO} 61 | # local global_log_level=${log_levels[$global_log_level_name]} 62 | # local event_log_level=${log_levels[$1]} 63 | log_levels $global_log_level_name 64 | local global_log_level=$? 65 | log_levels "$1" 66 | local event_log_level=$? 67 | [ $global_log_level -le $event_log_level ] && return 0 || return 1 68 | } 69 | 70 | function print_log_msg { 71 | if [ "$#" -lt 3 ]; then echo "$@" ; exit ; fi 72 | local level="$1" 73 | local color_code="$2" 74 | local message="${*:3}" 75 | 76 | check_log_level "$level"|| return 0 77 | 78 | tput setaf "$color_code" 79 | printf "%s %-7s: " "$(loggable_timestamp)" "$level" 80 | tput sgr0 81 | echo "$message" 82 | } 83 | 84 | function log_debug { 85 | if [ "$XTRACE" == "1" ]; then { set +x; } 2>/dev/null ; fi 86 | print_log_msg DEBUG 4 "$@" 87 | if [ "$XTRACE" == "1" ]; then { set -x; } 2>/dev/null ; fi 88 | } 89 | 90 | function log_info { 91 | if [ "$XTRACE" == "1" ]; then { set +x; } 2>/dev/null ; fi 92 | print_log_msg INFO 4 "$@" 93 | if [ "$XTRACE" == "1" ]; then { set -x; } 2>/dev/null ; fi 94 | } 95 | 96 | function log_warning { 97 | if [ "$XTRACE" == "1" ]; then { set +x; } 2>/dev/null ; fi 98 | print_log_msg WARNING 3 "$@" 99 | if [ "$XTRACE" == "1" ]; then { set -x; } 2>/dev/null ; fi 100 | } 101 | 102 | function log_error { 103 | if [ "$XTRACE" == "1" ]; then { set +x; } 2>/dev/null ; fi 104 | print_log_msg ERROR 1 "$@" 105 | if [ "$XTRACE" == "1" ]; then { set -x; } 2>/dev/null ; fi 106 | } 107 | 108 | ########## 109 | 110 | 111 | ###################################### 112 | # Script functions 113 | ###################################### 114 | # Lists all the available profiles (eg. profile source files) 115 | func_list_profiles () { 116 | echo "Avaliable profiles are:" 117 | ls "${CREDENTIALS_SOURCE_DIR}" 118 | return 0 119 | } 120 | 121 | # Pulls a key value from a profile section 122 | func_get_value_from_profile_source () { 123 | key_name=${1} 124 | sourcefile=${2} 125 | RETVAL=$(sed -n 's/^'"${key_name}"' = \(.*\)/\1/p' "${sourcefile}") 126 | if [ -z "${RETVAL}" ]; then 127 | log_debug "WARNING: Couldn't find ${key_name} in ${sourcefile}" 128 | fi 129 | return $? 130 | } 131 | 132 | # The whole song and dance to get an updated token for an IAM user using their non-MFA profile, and then insert the MFA-associated token into that profile 133 | func_update_user_token () { 134 | local PROFILE_NAME=$1 135 | # Remove the existing profile to remove old session tokens 136 | func_remove_profile_from_credentials_file "${PROFILE_NAME}" 137 | # Add the regular profile so we can request session tokens 138 | func_append_profile_to_credentials_file "${PROFILE_NAME}" 139 | func_update_boto_config 140 | # Now we can request a new token 141 | func_get_new_token "${PROFILE_NAME}" 142 | # Update the regular profile with the session token version 143 | func_remove_profile_from_credentials_file "${PROFILE_NAME}" 144 | func_append_sts_profile_to_credentials_file "${PROFILE_NAME}" 145 | # Remove consecutive blank lines 146 | sed -i.bak '/./,/^$/!d' "${CREDENTIALS_FILE}" 147 | rm "${CREDENTIALS_FILE}.bak" 148 | func_update_boto_config 149 | log_info "Updated credentials and config files" 150 | } 151 | 152 | # Alter the profile section titles for the boto version 153 | func_update_boto_config () { 154 | sed 's/^\[/[profile /' "${CREDENTIALS_FILE}" > "${BOTO_CONFIG_FILE}" 155 | } 156 | 157 | # Assigns session cred values to variables from a json blob (assuming the json isn't all on one line since we're using sed instead of jq) 158 | func_extract_session_credentials () { 159 | log_debug "Json session info is:" 160 | log_debug "$JSON_SESSION_INFO" 161 | 162 | # Extract values from STS response 163 | AWS_ACCESS_KEY_ID="$(echo "${JSON_SESSION_INFO}" | sed -n 's/.* "AccessKeyId": "\([^"]*\).*/\1/p')" 164 | AWS_SECRET_ACCESS_KEY="$(echo "${JSON_SESSION_INFO}" | sed -n 's/.*"SecretAccessKey": "\([^"]*\).*/\1/p')" 165 | AWS_SESSION_TOKEN="$(echo "${JSON_SESSION_INFO}" | sed -n 's/.*"SessionToken": "\([^"]*\).*/\1/p')" 166 | AWS_SESSION_TOKEN_EXPIRATION="$(echo "${JSON_SESSION_INFO}" | sed -n 's/.*"Expiration": "\([^"]*\).*/\1/p')" 167 | log_debug "Set 'AWS_ACCESS_KEY_ID' to '${AWS_ACCESS_KEY_ID}'" 168 | log_debug "Set 'AWS_SECRET_ACCESS_KEY' to '${AWS_SECRET_ACCESS_KEY:0:25}'" 169 | log_debug "Set 'AWS_SESSION_TOKEN' to '${AWS_SESSION_TOKEN:0:100}'" 170 | log_debug "Set 'AWS_SESSION_TOKEN_EXPIRATION' to '${AWS_SESSION_TOKEN_EXPIRATION}'" 171 | } 172 | 173 | # Gets a new session token using normal credentials and MFA 174 | func_get_new_token () { 175 | local PROFILE_NAME=$1 176 | if [ -z "${PROFILE_NAME-}" ]; then 177 | log_error "This fuction needs a profile name as an argument" 178 | exit 1 179 | fi 180 | 181 | func_get_value_from_profile_source "aws_access_key_id" "${CREDENTIALS_SOURCE_DIR}/${PROFILE_NAME}" 182 | if [ -z "${RETVAL-}" ]; then 183 | log_error "There are no IAM credentials in the profile '${PROFILE_NAME}'. Perhaps you meant to assume a role instead of getting credential directly?" 184 | exit 1 185 | fi 186 | 187 | 188 | log_info "Getting new STS session credentials for '${PROFILE_NAME}'" 189 | 190 | # Get a fresh sts session token 191 | func_get_value_from_profile_source "aws_access_key_id" "${CREDENTIALS_SOURCE_DIR}/${PROFILE_NAME}" 192 | MASTER_KEY=${RETVAL} 193 | func_get_value_from_profile_source "mfa_id" "${CREDENTIALS_SOURCE_DIR}/${PROFILE_NAME}" 194 | MFA_ID=${RETVAL} 195 | func_get_value_from_profile_source "mfa_type" "${CREDENTIALS_SOURCE_DIR}/${PROFILE_NAME}" 196 | MFA_TYPE="${RETVAL:-}" 197 | 198 | if [ -z "${MFA_ID}" ]; then 199 | log_info "Trying without MFA since 'mfa_id' was not found in the configuration file: ${CREDENTIALS_SOURCE_DIR}/${PROFILE_NAME}" 200 | log_debug "Calling STS with access_key ${MASTER_KEY}" 201 | log_debug "${AWS_CLI} --profile ${PROFILE_NAME} sts get-session-token" 202 | JSON_SESSION_INFO=$(${AWS_CLI} --profile "${PROFILE_NAME}" sts get-session-token) 203 | else 204 | 205 | # If an MFA_ID was specified in the profile config, get an MFA token 206 | case "${MFA_TYPE:-}" in 207 | yubikey) 208 | # Get an mfa token from an attached yubikey 209 | # For yubikey gotchas, see https://scalesec.com/blog/why-your-yubikey-wont-work-with-aws-cli/ 210 | # Disable abort-on-error to handle the case of a missing yubikey 211 | set +e 212 | MFA_TOKEN="$(ykman oath accounts code ${PROFILE_NAME} | tr -s ' ' | cut -d ' ' -f 2)" 213 | set -e 214 | ;; 215 | virtual) 216 | # Get an mfa token from an mfa app, if one's configured 217 | if [ -z "${MFA_PROGRAM}" ]; then 218 | echo "'virtual' mfa type specified in ${PROFILE_NAME}, but no MFA_PROGRAM configured." 219 | else 220 | log_debug "${MFA_PROGRAM} PROFILE_NAME}" 221 | set +e 222 | MFA_TOKEN="$($MFA_PROGRAM "$PROFILE_NAME")" 223 | set -e 224 | fi 225 | ;; 226 | esac 227 | 228 | # If we still don't have a token, ask the user interactively 229 | if [ -z "${MFA_TOKEN:-}" ]; then 230 | read -p "Enter the MFA code for '${PROFILE_NAME}': " -r MFA_TOKEN # add -s to prevent echoing of input 231 | if [ -z "${MFA_TOKEN:-}" ]; then 232 | echo "I need an MFA code to continue." 233 | exit 1 234 | fi 235 | fi 236 | 237 | log_debug "Calling STS with mfa token ${MFA_TOKEN}, access_key ${MASTER_KEY}, and serial-number ${MFA_ID}" 238 | log_debug "${AWS_CLI} --profile ${PROFILE_NAME} sts get-session-token --serial-number ${MFA_ID} --token-code ${MFA_TOKEN}" 239 | JSON_SESSION_INFO=$(${AWS_CLI} --profile "${PROFILE_NAME}" sts get-session-token --serial-number "${MFA_ID}" --token-code "${MFA_TOKEN}") 240 | fi 241 | 242 | func_extract_session_credentials 243 | return 0 244 | } 245 | 246 | # The song and dance of getting role credentials using the master profile 247 | func_update_role_credentials () { 248 | ROLE_PROFILE="$1" 249 | func_get_value_from_profile_source "master_profile" "${CREDENTIALS_SOURCE_DIR}/${PROFILE_NAME}" 250 | if [ -n "${RETVAL:-}" ]; then 251 | MASTER_PROFILE=${RETVAL} 252 | else 253 | log_error "The profile '${ROLE_PROFILE} is a role, but I can't assume it because there's no master profile specified." 254 | log_error "Specify it in the role profile file (${CREDENTIALS_SOURCE_DIR}/${PROFILE_NAME})," 255 | log_error "in the form: 'master_profile = xxxxx'" 256 | exit 1 257 | fi 258 | # Remove the existing profile to remove old session tokens 259 | func_remove_profile_from_credentials_file "${ROLE_PROFILE}" 260 | # Add the regular profile so we can request session tokens 261 | func_append_profile_to_credentials_file "${ROLE_PROFILE}" 262 | # Now we can request a new token 263 | func_get_role_token "${ROLE_PROFILE}" "${MASTER_PROFILE}" 264 | # Update the regular profile with the session token version 265 | func_remove_profile_from_credentials_file "${ROLE_PROFILE}" 266 | func_append_sts_profile_to_credentials_file "${ROLE_PROFILE}" 267 | # Remove consecutive blank lines 268 | sed -i.bak '/./,/^$/!d' "${CREDENTIALS_FILE}" 269 | rm "${CREDENTIALS_FILE}.bak" 270 | # Alter the profile section titles for the boto version 271 | echo "### This is automatically generated from the credentials file ###" > "${BOTO_CONFIG_FILE}" 272 | sed 's/^\[/[profile /' "${CREDENTIALS_FILE}" >> "${BOTO_CONFIG_FILE}" 273 | log_info "Updated credentials and config files" 274 | } 275 | 276 | # Gets a new role session token using the master-account user credentials (probably an IAM token) 277 | func_get_role_token () { 278 | # Sanity check 279 | if [ -z "${2-}" ]; then 280 | log_error "This fuction needs two arguments" 281 | exit 1 282 | fi 283 | 284 | ROLE_PROFILE="$1" 285 | MASTER_PROFILE="$2" 286 | 287 | log_info "Getting new STS credentials for role '${ROLE_PROFILE}'" 288 | 289 | func_get_value_from_profile_source "account_id" "${CREDENTIALS_SOURCE_DIR}/${ROLE_PROFILE}" 290 | ROLE_ACCOUNT_ID=${RETVAL} 291 | func_get_value_from_profile_source "role_name" "${CREDENTIALS_SOURCE_DIR}/${ROLE_PROFILE}" 292 | ROLE_NAME=${RETVAL} 293 | 294 | ROLE_ARN="arn:aws:iam::${ROLE_ACCOUNT_ID}:role/${ROLE_NAME}" 295 | 296 | # Get a fresh sts session token 297 | log_debug "Calling STS using profile '${MASTER_PROFILE}' to assume role '${ROLE_ARN}'" 298 | set +e # It's ok for the next line to fail; we can recover 299 | result=$(${AWS_CLI} --profile "${MASTER_PROFILE}" sts assume-role --role-arn "${ROLE_ARN}" --role-session-name "${LOGNAME}" 2>&1) 300 | ret=$? 301 | set -e 302 | if [ $ret == 0 ]; then 303 | JSON_SESSION_INFO="$result" 304 | else 305 | log_debug "${result}" 306 | log_info "The master profile's temporary IAM credentials are expired. Renewing them..." 307 | log_info "" 308 | func_update_user_token "${MASTER_PROFILE}" 309 | log_info "" 310 | log_info "Renewed the master profile's credentials. Trying to get role credentials again." 311 | JSON_SESSION_INFO=$(${AWS_CLI} --profile "${MASTER_PROFILE}" sts assume-role --role-arn "${ROLE_ARN}" --role-session-name "${LOGNAME}") 312 | fi 313 | func_extract_session_credentials 314 | return 0 315 | } 316 | 317 | # Needed to clean out old profile info before added refreshed info 318 | func_remove_profile_from_credentials_file () { 319 | local PROFILE_NAME=$1 320 | sed -e '/./{H;$!d;}' -e 'x;/\['"${PROFILE_NAME}"'\]/d;' "${CREDENTIALS_FILE}" | sed '/./,$!d' > "${CREDENTIALS_FILE}.bak" 321 | cat "${CREDENTIALS_FILE}.bak" > "${CREDENTIALS_FILE}" 322 | rm "${CREDENTIALS_FILE}.bak" 323 | log_debug "Removed profile '${PROFILE_NAME}' from ${CREDENTIALS_FILE}" 324 | return 0 325 | } 326 | 327 | # Adds a reguler profile to to the credentials file, plus session tokens 328 | func_append_sts_profile_to_credentials_file () { 329 | local PROFILE_NAME=$1 330 | func_append_profile_without_creds_to_credentials_file "${PROFILE_NAME}" 331 | cat >> "${CREDENTIALS_FILE}" <<-EOF 332 | aws_access_key_id = ${AWS_ACCESS_KEY_ID} 333 | aws_secret_access_key = ${AWS_SECRET_ACCESS_KEY} 334 | aws_session_token = ${AWS_SESSION_TOKEN} 335 | aws_session_token_expiration = ${AWS_SESSION_TOKEN_EXPIRATION} 336 | EOF 337 | log_debug "Appended STS tokens for ${PROFILE_NAME}" 338 | return 0 339 | } 340 | 341 | # Appends a profile (including AWS credentials) to profile 342 | func_append_profile_to_credentials_file () { 343 | local PROFILE_NAME=$1 344 | local PROFILE_SOURCE_FILE="${CREDENTIALS_SOURCE_DIR}/${PROFILE_NAME}" 345 | func_check_if_profile_source_valid "${PROFILE_SOURCE_FILE}" 346 | # shellcheck disable=2129 347 | echo '' >> "${CREDENTIALS_FILE}" 348 | cat "${PROFILE_SOURCE_FILE}" >> "${CREDENTIALS_FILE}" 349 | echo "### This profile section was auto-generated from '${PROFILE_SOURCE_FILE}' ###" >> "${CREDENTIALS_FILE}" 350 | log_debug "Appended unaltered ${PROFILE_SOURCE_FILE}" 351 | func_check_no_dups 352 | } 353 | 354 | # Appends a profile (without AWS credentials) to profile 355 | func_append_profile_without_creds_to_credentials_file () { 356 | local PROFILE_NAME=$1 357 | local PROFILE_SOURCE_FILE="${CREDENTIALS_SOURCE_DIR}/${PROFILE_NAME}" 358 | func_check_if_profile_source_valid "${PROFILE_SOURCE_FILE}" 359 | # shellcheck disable=2129 360 | echo '' >> "${CREDENTIALS_FILE}" 361 | sed -e '/aws_access_key_id/d' -e '/aws_secret_access_key/d' "${PROFILE_SOURCE_FILE}" >> "${CREDENTIALS_FILE}" 362 | echo "### This profile section was auto-generated from '${PROFILE_SOURCE_FILE}' ###" >> "${CREDENTIALS_FILE}" 363 | log_debug "Appended unaltered ${PROFILE_SOURCE_FILE}" 364 | func_check_no_dups 365 | } 366 | 367 | # Simple helper for sanity checking that the profile source file exists and is sane 368 | func_check_if_profile_source_valid () 369 | { 370 | src_filepath="$1" 371 | src_filename=$(basename "${src_filepath}") 372 | if ! [ -f "${src_filepath}" ]; then 373 | log_error "The credentials source (${src_filepath}) file doesn't exist" 374 | func_list_profiles 375 | exit 1 376 | fi 377 | if ! grep -q '^\['"${src_filename}"'\]$' "${src_filepath}"; then 378 | log_error "The profile name '${src_filename}' was not found in the file '${src_filepath}'" 379 | exit 1 380 | fi 381 | } 382 | 383 | # Check the type of profile file given 384 | func_is_role_profile () { 385 | func_get_value_from_profile_source "role_name" "${CREDENTIALS_SOURCE_DIR}/${PROFILE_NAME}" 386 | if [ -z "${RETVAL:-}" ]; then 387 | log_debug "No 'role_name' found in ${CREDENTIALS_SOURCE_DIR}/${PROFILE_NAME}. Assuming this profile is a user profile." 388 | return 1 389 | else 390 | log_debug "'role_name' found in ${CREDENTIALS_SOURCE_DIR}/${PROFILE_NAME}. Assuming this profile is a role profile." 391 | return 0 392 | fi 393 | } 394 | 395 | # Convert the contents of a profile section to a sourceable environment variable export file. 396 | func_export_to_env_file () { 397 | PN=$1 # profile name. Example: dev 398 | ENVFILE="$2.txt" 399 | YAMLFILE="$2.yaml" 400 | 401 | # shell sourceable version 402 | ( 403 | echo "export GLOBUS_ACCOUNT_NAME=${PN}" 404 | echo "export AWS_PROFILE=${PN}" 405 | sed -e '/./{H;$!d;}' -e 'x;/\['"${PN}"'\]/!d;' "${CREDENTIALS_FILE}" | sed -n 's/^region = \(.*\)/export AWS_DEFAULT_REGION=\1/p' 406 | sed -e '/./{H;$!d;}' -e 'x;/\['"${PN}"'\]/!d;' "${CREDENTIALS_FILE}" | sed -n 's/^account_id = \(.*\)/export AWS_ACCT=\1/p' 407 | sed -e '/./{H;$!d;}' -e 'x;/\['"${PN}"'\]/!d;' "${CREDENTIALS_FILE}" | sed -n 's/^aws_access_key_id = \(.*\)/export AWS_ACCESS_KEY_ID=\1/p' 408 | sed -e '/./{H;$!d;}' -e 'x;/\['"${PN}"'\]/!d;' "${CREDENTIALS_FILE}" | sed -n 's/^aws_secret_access_key = \(.*\)/export AWS_SECRET_ACCESS_KEY=\1/p' 409 | sed -e '/./{H;$!d;}' -e 'x;/\['"${PN}"'\]/!d;' "${CREDENTIALS_FILE}" | sed -n 's/^aws_session_token = \(.*\)/export AWS_SESSION_TOKEN=\1/p' 410 | sed -e '/./{H;$!d;}' -e 'x;/\['"${PN}"'\]/!d;' "${CREDENTIALS_FILE}" | sed -n 's/^mfa_id = \(.*\)/export AWS_MFA_SN=\1/p' 411 | ) > "${ENVFILE}" 412 | 413 | # convert to YMAL for pyCharm 414 | sed 's/export \([^=]*\)=\(.*\)/\1: "\2"/' "${ENVFILE}" > "${YAMLFILE}" 415 | 416 | # For admin.py. Ugh. 417 | echo 'export OPS_ADMIN_EXTERNAL_MFA=true' >> "${ENVFILE}" 418 | # shellcheck disable=2002 419 | cat "${CREDENTIALS_FILE}" | sed -e '/./{H;$!d;}' -e 'x;/\['"${PN}"'\]/!d;' | sed -n 's/^account_id = \(.*\)/export ADMIN_AWS_ACCT=\1/p' >> "${ENVFILE}" 420 | 421 | chmod 600 "${ENVFILE}" "${YAMLFILE}" 422 | } 423 | 424 | # Make sure the credentials file contains no duplicate entries 425 | func_check_no_dups () { 426 | dups="$(grep '\[' "${CREDENTIALS_FILE}" | sort | uniq -d | xargs )" 427 | if [ -n "${dups}" ]; then 428 | log_error "Duplicate profile names found in credentials file (${CREDENTIALS_FILE}):" 429 | log_error " ${dups}" 430 | log_error "Please fix this and, if necessary, your source files (${CREDENTIALS_SOURCE_DIR}/)." 431 | exit 1 432 | fi 433 | } 434 | 435 | 436 | 437 | ################# 438 | # Main 439 | 440 | # Sanity checks 441 | if [ -z "${2-}" ]; then 442 | log_error "You need to pass an action type (token or awsenv) and a profile name as arguments" 443 | log_error "" 444 | log_error "Example:" 445 | log_error " $0 token my-account-admin-role" 446 | log_error "" 447 | func_list_profiles 448 | exit 1 449 | fi 450 | 451 | ACTION_NAME="$1" 452 | PROFILE_NAME="$2" 453 | 454 | if ! [ -d "${CREDENTIALS_SOURCE_DIR}" ]; then 455 | log_error "The credentials source (${CREDENTIALS_SOURCE_DIR}) dir doesn't exist" 456 | exit 1 457 | fi 458 | 459 | PROFILE_SOURCE_FILE="${CREDENTIALS_SOURCE_DIR}/${PROFILE_NAME}" 460 | func_check_if_profile_source_valid "${PROFILE_SOURCE_FILE}" 461 | 462 | # Create a new credentials file if it's missing 463 | if ! [ -f "${CREDENTIALS_FILE}" ]; then 464 | log_info "${CREDENTIALS_FILE} doesn't exist. Creating a new one" 465 | for i in "${CREDENTIALS_SOURCE_DIR}"/*; do 466 | { cat "${i}"; echo; echo; } >> "${CREDENTIALS_FILE}" 467 | done 468 | log_info "${CREDENTIALS_FILE} initialized." 469 | func_check_no_dups 470 | fi 471 | 472 | if func_is_role_profile; then 473 | func_update_role_credentials "${PROFILE_NAME}" 474 | else 475 | AWS_SESSION_TOKEN_EXPIRATION=$(sed -e '/./{H;$!d;}' -e 'x;/\['"${PROFILE_NAME}"'\]/!d;' "${CREDENTIALS_FILE}" | sed -En 's/aws_session_token_expiration = (.*)/\1/p') 476 | 477 | # If a session token doesn't exist, get a new one 478 | if [ -z "${AWS_SESSION_TOKEN_EXPIRATION}" ]; then 479 | log_info "No session found in ${CREDENTIALS_FILE}. Creating a new one." 480 | func_update_user_token "${PROFILE_NAME}" 481 | 482 | else 483 | # If a token already exists, check if it's expired or close to expiring 484 | log_info "Checking if cached token is near or past expiration" 485 | log_debug "Found '${AWS_SESSION_TOKEN_EXPIRATION}' in ${CREDENTIALS_FILE} for profile ${PROFILE_NAME}" 486 | CURRENT_TIME_EPOCH="$(date "+%s")" 487 | if [ "$PLATFORM" == "Darwin" ]; then 488 | AWS_SESSION_TOKEN_EXPIRATION_EPOCH="$(date -j -f "%Y-%m-%dT%H:%M:%S+00:00" "${AWS_SESSION_TOKEN_EXPIRATION}" "+%s")" 489 | AWS_SESSION_TOKEN_REFRESH_EPOCH="$(date -j -f "%Y-%m-%dT%H:%M:%S+00:00" -v "-${TOKEN_PREEXPIRATION_HOURS}H" "${AWS_SESSION_TOKEN_EXPIRATION}" "+%s")" 490 | else 491 | AWS_SESSION_TOKEN_EXPIRATION_EPOCH="$(date -d "${AWS_SESSION_TOKEN_EXPIRATION}" "+%s")" 492 | AWS_SESSION_TOKEN_REFRESH_EPOCH="$(date -d "${AWS_SESSION_TOKEN_EXPIRATION} -${TOKEN_PREEXPIRATION_HOURS} hours" "+%s")" 493 | fi 494 | # If the session token has expired or is close to expiring, get a new one 495 | if [ "$AWS_SESSION_TOKEN_REFRESH_EPOCH" -lt "$CURRENT_TIME_EPOCH" ]; then 496 | log_info "The session token is no longer fresh (expires in $(( ( $AWS_SESSION_TOKEN_EXPIRATION_EPOCH - $CURRENT_TIME_EPOCH ) / 60 )) minutes). Getting a fresh one" 497 | func_update_user_token "${PROFILE_NAME}" 498 | else 499 | # otherwise tell the user how long they have before it will be refreshed 500 | REFRESH_SEC=$((($AWS_SESSION_TOKEN_REFRESH_EPOCH - $CURRENT_TIME_EPOCH) % 60 )) 501 | REFRESH_MIN=$((($AWS_SESSION_TOKEN_REFRESH_EPOCH - $CURRENT_TIME_EPOCH) / 60 % 60)) 502 | REFRESH_HR=$((($AWS_SESSION_TOKEN_REFRESH_EPOCH - $CURRENT_TIME_EPOCH) / 3600)) 503 | EXPIRE_SEC=$((($AWS_SESSION_TOKEN_EXPIRATION_EPOCH - $CURRENT_TIME_EPOCH) % 60)) 504 | EXPIRE_MIN=$((($AWS_SESSION_TOKEN_EXPIRATION_EPOCH - $CURRENT_TIME_EPOCH) / 60 % 60)) 505 | EXPIRE_HR=$((($AWS_SESSION_TOKEN_EXPIRATION_EPOCH - $CURRENT_TIME_EPOCH) / 3600)) 506 | log_info "Everything looks good. Reusing existing session token (fresh for ${REFRESH_HR}:${REFRESH_MIN}:${REFRESH_SEC}, expires in ${EXPIRE_HR}:${EXPIRE_MIN}:${EXPIRE_SEC})" 507 | fi 508 | fi 509 | fi 510 | 511 | if [ "${ACTION_NAME}" == "awsenv" ]; then 512 | # Save the profile credentials to a file that can be exported by the shell function calling this 513 | mkdir -p "${AWS_DIR}/env-files" 514 | func_export_to_env_file "${PROFILE_NAME}" "${AWS_DIR}/env-files/current-env" 515 | func_export_to_env_file "${PROFILE_NAME}" "${AWS_DIR}/env-files/${PROFILE_NAME}-env" 516 | fi 517 | 518 | exit 0 519 | --------------------------------------------------------------------------------