diff --git a/README.md b/README.md
index 09564fc..b9fc9e7 100644
--- a/README.md
+++ b/README.md
@@ -3,9 +3,9 @@
This repository contains non-proprietary (MIT license) utility scripts for use with AWS.
-* **aws-iam-rotate-keys.sh** - rotates AWS access keys stored in the user's `~/.aws/credentials` file. If you have set the policy for a user to have maximum of two concurrent keys, this script will first make sure there is just one existing key by allowing user to delete an existing key that is not in use. It then proceeds to create the new keys, test that they work, replace the keys in the user's `~/.aws/credentials` file, and finally remove the old key that was replaced. This is an interactive script, and as such it does not take arguments. The script was written for macOS, but portability for Linux has been added. Multiple profiles are supported, as is MFA when used in conjunction with `awscli-mfa.sh` script. The script also displays the key ages, and the actual IAM user name associated with each credential profile.
For more details, read my blog post about this script [here](https://random.ac/cess/2017/10/28/aws-cli-key-rotation-script-v2/).
+* **[awscli-mfa/](https://github.com/605data/aws_scripts/tree/master/awscli-mfa)** - A set of scripts that make the use MFA sessions with AWS CLI easy (or, at least feasible :-).
-* **awscli-mfa.sh** - Makes it easy to use MFA sessions with AWS CLI. Multiple profiles are supported. This is an interactive script (since it prompts for the current MFA one time pass code), and as such it does not take arguments. The script was written for macOS, but portability for Linux has been added.
For more details, read my blog post about this script [here](https://random.ac/cess/2017/10/29/easy-mfa-and-profile-switching-in-aws-cli/).
+* **aws-iam-rotate-keys.sh** - rotates AWS access keys stored in the user's `~/.aws/credentials` file. If you have set the policy for a user to have maximum of two concurrent keys, this script will first make sure there is just one existing key by allowing user to delete an existing key that is not in use. It then proceeds to create the new keys, test that they work, replace the keys in the user's `~/.aws/credentials` file, and finally remove the old key that was replaced. This is an interactive script, and as such it does not take arguments. The script was written for macOS, but portability for Linux has been added. Multiple profiles are supported, as is MFA when used in conjunction with `awscli-mfa.sh` script. The script also displays the key ages, and the actual IAM user name associated with each credential profile.
For more details, read my blog post about this script [here](https://random.ac/cess/2017/10/28/aws-cli-key-rotation-script-v2/).
* **get-key-ages.py** - List the ages of all AWS IAM API keys in the account (this assumes properly configured `~/.aws/config`, and obviously sufficient access level to this information. Currently the output is tab delimited, and to the standard output, from where it can be cut-and-pasted to, say, Excel. In other words a quick-and-dirty utility script for a key age report.
diff --git a/aws-iam-rotate-keys.sh b/aws-iam-rotate-keys.sh
index e7ffc73..b195f37 100755
--- a/aws-iam-rotate-keys.sh
+++ b/aws-iam-rotate-keys.sh
@@ -1,15 +1,19 @@
#!/usr/bin/env bash
+DEBUG="false"
+# uncomment below to enable the debug output
+#DEBUG="true"
+
## PREREQUISITES CHECK
# `exists` for commands
exists() {
- command -v "$1" >/dev/null 2>&1
+ command -v "$1" >/dev/null 2>&1
}
# is AWS CLI installed?
if ! exists aws ; then
- printf "\n******************************************************************************************************************************\n\
+ printf "\n******************************************************************************************************************************\n\
This script requires the AWS CLI. See the details here: http://docs.aws.amazon.com/cli/latest/userguide/cli-install-macos.html\n\
******************************************************************************************************************************\n\n"
exit 1
@@ -17,522 +21,546 @@ fi
# check for ~/.aws directory, and ~/.aws/{config|credentials} files
if [ ! -d ~/.aws ]; then
- echo
- echo -e "'~/.aws' directory not present.\nMake sure it exists, and that you have at least one profile configured\nusing the 'config' and 'credentials' files within that directory."
- echo
- exit 1
+ echo
+ echo -e "'~/.aws' directory not present.\nMake sure it exists, and that you have at least one profile configured\nusing the 'config' and 'credentials' files within that directory."
+ echo
+ exit 1
fi
if [[ ! -f ~/.aws/config && ! -f ~/.aws/credentials ]]; then
- echo
- echo -e "'~/.aws/config' and '~/.aws/credentials' files not present.\nMake sure they exist. See http://docs.aws.amazon.com/cli/latest/userguide/cli-config-files.html for details on how to set them up."
- echo
- exit 1
+ echo
+ echo -e "'~/.aws/config' and '~/.aws/credentials' files not present.\nMake sure they exist. See http://docs.aws.amazon.com/cli/latest/userguide/cli-config-files.html for details on how to set them up."
+ echo
+ exit 1
elif [ ! -f ~/.aws/config ]; then
- echo
- echo -e "'~/.aws/config' file not present.\nMake sure it and '~/.aws/credentials' files exists. See http://docs.aws.amazon.com/cli/latest/userguide/cli-config-files.html for details on how to set them up."
- echo
- exit 1
+ echo
+ echo -e "'~/.aws/config' file not present.\nMake sure it and '~/.aws/credentials' files exists. See http://docs.aws.amazon.com/cli/latest/userguide/cli-config-files.html for details on how to set them up."
+ echo
+ exit 1
elif [ ! -f ~/.aws/credentials ]; then
- echo
- echo -e "'~/.aws/credentials' file not present.\nMake sure it and '~/.aws/config' files exists. See http://docs.aws.amazon.com/cli/latest/userguide/cli-config-files.html for details on how to set them up."
- echo
- exit 1
+ echo
+ echo -e "'~/.aws/credentials' file not present.\nMake sure it and '~/.aws/config' files exists. See http://docs.aws.amazon.com/cli/latest/userguide/cli-config-files.html for details on how to set them up."
+ echo
+ exit 1
fi
CREDFILE=~/.aws/credentials
# check that at least one profile is configured
ONEPROFILE="false"
while IFS='' read -r line || [[ -n "$line" ]]; do
- [[ "$line" =~ ^\[(.*)\].* ]] &&
- profile_ident=${BASH_REMATCH[1]}
+ [[ "$line" =~ ^\[(.*)\].* ]] &&
+ profile_ident=${BASH_REMATCH[1]}
- if [ $profile_ident != "" ]; then
- ONEPROFILE="true"
- fi
+ if [ $profile_ident != "" ]; then
+ ONEPROFILE="true"
+ fi
done < $CREDFILE
if [[ "$ONEPROFILE" = "false" ]]; then
- echo
- echo -e "NO CONFIGURED AWS PROFILES FOUND.\nPlease make sure you have '~/.aws/config' (profile configurations),\nand '~/.aws/credentials' (profile credentials) files, and at least\none configured profile. For more info, see AWS CLI documentation at:\nhttp://docs.aws.amazon.com/cli/latest/userguide/cli-config-files.html"
- echo
+ echo
+ echo -e "NO CONFIGURED AWS PROFILES FOUND.\nPlease make sure you have '~/.aws/config' (profile configurations),\nand '~/.aws/credentials' (profile credentials) files, and at least\none configured profile. For more info, see AWS CLI documentation at:\nhttp://docs.aws.amazon.com/cli/latest/userguide/cli-config-files.html"
+ echo
+
+else
+
+ # Check OS for some supported platforms
+ OS="`uname`"
+ case $OS in
+ 'Linux')
+ OS='Linux'
+ ;;
+ 'Darwin')
+ OS='macOS'
+ ;;
+ *)
+ OS='unknown'
+ echo
+ echo "** NOTE: THIS SCRIPT HAS NOT BEEN TESTED ON YOUR CURRENT PLATFORM."
+ echo
+ ;;
+ esac
+
+ ## PREREQS PASSED; PROCEED..
+
+ declare -a cred_profiles
+ declare -a cred_allprofiles
+ declare -a cred_profile_arn
+ declare -a cred_profile_user
+ declare -a cred_profile_keys
+ declare -a key_status
+ cred_profilecounter=0
+
+ TODAY=`date "+%Y-%m-%d"`
+
+ echo -n "Please wait"
+
+ # get profiles, keys (and their ages) for selection
+ while IFS='' read -r line || [[ -n "$line" ]]; do
+ [[ "$line" =~ ^\[(.*)\].* ]] &&
+ profile_ident=${BASH_REMATCH[1]}
+
+ # only process if profile identifier is present,
+ # and if it's not a mfasession profile
+ if [ "$profile_ident" != "" ] &&
+ ! [[ "$profile_ident" =~ -mfasession$ ]]; then
+
+ cred_profiles[$cred_profilecounter]=$profile_ident
+
+ # get user ARN; this should be always available if the access_key_id is valid
+ user_arn="$(aws sts get-caller-identity --profile "$profile_ident" --output text --query 'Arn' 2>&1)"
+ if [[ "$user_arn" =~ ^arn:aws ]]; then
+ cred_profile_arn[$cred_profilecounter]=$user_arn
+ elif [[ "$user_arn" =~ InvalidClientTokenId ]]; then
+ cred_profile_arn[$cred_profilecounter]="INVALID"
+ else
+ cred_profile_arn[$cred_profilecounter]=""
+ fi
+
+ # get the actual username (may be different from the arbitrary profile ident)
+ if [[ "${cred_profile_arn[$cred_profilecounter]}" =~ ^arn:aws ]]; then
+ [[ "$user_arn" =~ ([^/]+)$ ]] &&
+ cred_profile_user[$cred_profilecounter]="${BASH_REMATCH[1]}"
+ elif [ "${cred_profile_arn[$cred_profilecounter]}" = "INVALID" ]; then
+ cred_profile_user[$cred_profilecounter]="CHECK CREDENTIALS!"
+ else
+ cred_profile_user[$cred_profilecounter]=""
+ fi
+
+ # get access keys & their ages for the profile
+ key_status_accumulator=""
+
+ if [ ${cred_profile_arn[$cred_profilecounter]} != "INVALID" ]; then
+
+ key_status_array_input=`aws iam list-access-keys --profile "$profile_ident" --output json --query AccessKeyMetadata[*].[Status,CreateDate,AccessKeyId] 2>&1`
+
+ if [[ "${key_status_array_input}" =~ .*explicit[[:space:]]deny.* ]]; then
+ key_status_array[0]="Denied"
+ key_status_array[1]=""
+ key_status_array[2]=`aws --profile "$profile_ident" configure get aws_access_key_id`
+ cred_profile_arn[$cred_profilecounter]="DENIED"
+ else
+ key_status_array=(`echo "${key_status_array_input}" | grep -A2 ctive | awk -F\" '{print $2}'`)
+ fi
+
+ # get the actual username (may be different from the arbitrary profile ident)
+ s_no=0
+ for s in ${key_status_array[@]}; do
+ if [[ "$s" == "Active" || "$s" == "Denied" || "$s" == "Inactive" ]]; then
+
+ if [ "$s" == "Active" ]; then
+ statusword=" Active"
+ elif [ "$s" == "Denied" ]; then
+ statusword="INSUFFICIENT PRIVILEGES TO PROCESS THE KEY"
+ else
+ statusword="Inactive"
+ fi
+
+ let "s_no++"
+ kcd=`echo ${key_status_array[$s_no]} | sed 's/T/ /' | awk '{print $1}'`
+ let keypos=${s_no}+1
+ if [ "$s" != "Denied" ]; then
+ if [ "$OS" = "macOS" ]; then
+ key_status_accumulator=" ${statusword} key ${key_status_array[$keypos]} is $(((`date -jf %Y-%m-%d $TODAY +%s` - `date -jf %Y-%m-%d $kcd +%s`)/86400)) days old\n${key_status_accumulator}"
+ else
+ key_status_accumulator=" ${statusword} key ${key_status_array[$keypos]} is $(((`date -d "$TODAY" "+%s"` - `date -d "$kcd" "+%s"`)/86400)) days old\n${key_status_accumulator}"
+ fi
+ else
+ key_status_accumulator=" ${statusword} ${key_status_array[$keypos]}\n Restrictive policy in effect.\n"
+ fi
+ else
+ let "s_no++"
+ fi
+ done
+ fi
+
+ cred_profile_keys[$cred_profilecounter]=$key_status_accumulator
+
+ ## DEBUG (enable with DEBUG="true" on top of the file)
+ if [ "$DEBUG" == "true" ]; then
+ echo "PROFILE IDENT: $profile_ident"
+ echo "USER ARN: ${cred_profile_arn[$cred_profilecounter]}"
+ echo "USER NAME: ${cred_profile_user[$cred_profilecounter]}"
+ echo "MFA ARN: ${mfa_arns[$cred_profilecounter]}"
+ ## END DEBUG
+ else
+ echo -n "."
+ fi
+
+ # erase variables & increase iterator for the next iteration
+ user_arn=""
+ profile_ident=""
+ profile_username=""
+
+ cred_profilecounter=$(($cred_profilecounter+1))
+
+ fi
+ done < $CREDFILE
+
+ echo
+ echo
+ # create profile selections for key rotation
+ echo "CONFIGURED AWS PROFILES AND THEIR ASSOCIATED KEYS:"
+ echo
+ SELECTR=0
+ ITER=1
+ for i in "${cred_profiles[@]}"
+ do
+ if [ "${cred_profile_arn[$SELECTR]}" = "INVALID" ]; then
+ echo "X: $i (${cred_profile_user[$SELECTR]})"
+ else
+ echo "${ITER}: $i (${cred_profile_user[$SELECTR]})"
+ printf "${cred_profile_keys[$SELECTR]}"
+ fi
+ echo
+ let ITER=${ITER}+1
+ let SELECTR=${SELECTR}+1
+ done
+
+ # prompt for profile selection
+ printf "SELECT THE PROFILE WHOSE KEYS YOU WANT TO ROTATE (or press [ENTER] to abort): "
+ read -r selprofile
+
+ # process the selection
+ if [ "$selprofile" != "" ]; then
+ # capture the numeric part of the selection
+ [[ $selprofile =~ ^([[:digit:]]+) ]] &&
+ selprofile_check="${BASH_REMATCH[1]}"
+
+ if [ "$selprofile_check" != "" ]; then
+
+ # if the numeric selection was found,
+ # translate it to the array index and validate
+ let actual_selprofile=${selprofile_check}-1
+
+ profilecount=${#cred_profiles[@]}
+ if [[ $actual_selprofile -ge $profilecount ||
+ $actual_selprofile -lt 0 ]]; then
+ # a selection outside of the existing range was specified
+ echo
+ echo "There is no profile '${selprofile}'."
+ echo
+ exit 1
+ fi
+
+ # a base profile was selected
+ if [[ $selprofile =~ ^[[:digit:]]+$ ]]; then
+ if [ "${cred_profile_arn[$actual_selprofile]}" = "INVALID" ]; then
+ echo
+ echo "PROFILE \"${cred_profiles[$actual_selprofile]}\" HAS INVALID ACCESS KEYS. Cannot proceed."
+ echo
+ exit 1
+ elif [ "${cred_profile_arn[$actual_selprofile]}" = "DENIED" ]; then
+ echo
+ echo "PROFILE \"${cred_profiles[$actual_selprofile]}\" HAS INSUFFICIENT PRIVILEGES (restrictive policy in effect). Cannot proceed."
+ echo
+ exit 1
+ else
+ echo
+ echo "SELECTED PROFILE: ${cred_profiles[$actual_selprofile]}"
+ final_selection="${cred_profiles[$actual_selprofile]}"
+ final_selection_name="${cred_profile_user[$actual_selprofile]}"
+ echo "SELECTED USER: $final_selection_name"
+ fi
+ else
+ # non-acceptable characters were present in the selection
+ echo
+ echo "There is no profile '${selprofile}'."
+ echo
+ exit 1
+ fi
+ else
+ # no numeric part in selection
+ echo
+ echo "There is no profile '${selprofile}'."
+ echo
+ exit 1
+ fi
+ else
+ # empty selection; exit
+ echo
+ echo "Aborting. No changes were made."
+ echo
+ exit 1
+ fi
+fi
+
+# does mfasession profile exist for the chosen profile?
+mfaprofile_exists="false"
+ITERATR=0
+while IFS='' read -r line || [[ -n "$line" ]]; do
+ [[ "$line" =~ ^\[(.*)\].* ]] &&
+ all_profile_ident=${BASH_REMATCH[1]}
+ if [[ $all_profile_ident != "" &&
+ "$all_profile_ident" =~ ${final_selection}-mfasession ]]; then
+ mfaprofile_exists="true"
+ break
+ let "ITERATR++"
+ fi
+ all_profile_ident=""
+done < $CREDFILE
+# mfasession exists -- use it?
+use_mfaprofile="false"
+if [ "$mfaprofile_exists" = "true" ]; then
+
+ echo
+ echo "-----------------------------------------------------------------"
+ echo
+ echo -e "An MFA profile ('${final_selection}-mfasession') exists\nfor the profile whose keys you have chosen to rotate."
+ echo
+ echo -e "Do you want to use it to execute the key rotation for\nthe profile you selected? If MFA is being enforced\nfor the profile selected for rotation, it may not be\nauthorized to carry out its own key rotation and the MFA\nprofile may need to be used instead."
+ echo
+ echo -e "Note that the MFA profile session MUST BE ACTIVE for it\nto work. Select Abort below if you need to activate\nthe MFA session before proceeding."
+ echo
+
+ while true; do
+ read -p "Use the MFA profile to authorize the key rotation? Yes/No/Abort " ync
+ case $ync in
+ [Yy]* ) use_mfaprofile="true"; break;;
+ [Nn]* ) break;;
+ [Aa]* ) echo; exit;;
+ * ) echo "Please answer (y)es, (n)o, or (a)bort.";;
+ esac
+ done
+fi
+
+if [ "$use_mfaprofile" = "false" ]; then
+ selected_authprofile=$final_selection
else
+ selected_authprofile=${final_selection}-mfasession
+
+ echo "Please wait..."
+
+ # check for expired MFA session
+ EXISTING_KEYS_CREATEDATES=$(aws iam list-access-keys --query 'AccessKeyMetadata[].CreateDate' --output text --profile "$selected_authprofile" 2>&1)
+ if [[ "$EXISTING_KEYS_CREATEDATES" =~ ExpiredToken ]]; then
+ echo -e "\nYour MFA token has expired. Refresh your MFA session\nfor the profile '${final_selection}', and try again.\n\nAborting.\n"
+ exit 1
+ fi
- # Check OS for some supported platforms
- OS="`uname`"
- case $OS in
- 'Linux')
- OS='Linux'
- ;;
- 'Darwin')
- OS='macOS'
- ;;
- *)
- OS='unknown'
- echo
- echo "** NOTE: THIS SCRIPT HAS NOT BEEN TESTED ON YOUR CURRENT PLATFORM."
- echo
- ;;
- esac
-
- ## PREREQS PASSED; PROCEED..
-
- declare -a cred_profiles
- declare -a cred_allprofiles
- declare -a cred_profile_arn
- declare -a cred_profile_user
- declare -a cred_profile_keys
- declare -a key_status
- cred_profilecounter=0
-
- TODAY=`date "+%Y-%m-%d"`
-
- echo "Please wait..."
-
- # get profiles, keys (and their ages) for selection
- while IFS='' read -r line || [[ -n "$line" ]]; do
- [[ "$line" =~ ^\[(.*)\].* ]] &&
- profile_ident=${BASH_REMATCH[1]}
-
- # only process if profile identifier is present,
- # and if it's not a mfasession profile
- if [ "$profile_ident" != "" ] &&
- ! [[ "$profile_ident" =~ -mfasession$ ]]; then
-
- cred_profiles[$cred_profilecounter]=$profile_ident
-
- # get user ARN; this should be always available if the access_key_id is valid
- user_arn="$(aws sts get-caller-identity --profile "$profile_ident" --output text --query 'Arn' 2>&1)"
- if [[ "$user_arn" =~ ^arn:aws ]]; then
- cred_profile_arn[$cred_profilecounter]=$user_arn
- elif [[ "$user_arn" =~ InvalidClientTokenId ]]; then
- cred_profile_arn[$cred_profilecounter]="INVALID"
- else
- cred_profile_arn[$cred_profilecounter]=""
- fi
-
- # get the actual username (may be different from the arbitrary profile ident)
- if [[ "${cred_profile_arn[$cred_profilecounter]}" =~ ^arn:aws ]]; then
- [[ "$user_arn" =~ ([^/]+)$ ]] &&
- cred_profile_user[$cred_profilecounter]="${BASH_REMATCH[1]}"
- elif [ "${cred_profile_arn[$cred_profilecounter]}" = "INVALID" ]; then
- cred_profile_user[$cred_profilecounter]="CHECK CREDENTIALS!"
- else
- cred_profile_user[$cred_profilecounter]=""
- fi
-
- # get access keys & their ages for the profile
- key_status_accumulator=""
-
- if [ ${cred_profile_arn[$cred_profilecounter]} != "INVALID" ]; then
-
- key_status_array=(`aws iam list-access-keys --profile "$profile_ident" --output json --query AccessKeyMetadata[*].[Status,CreateDate,AccessKeyId] | grep -A2 ctive | awk -F\" '{print $2}'`)
-
- s_no=0
- for s in ${key_status_array[@]}; do
- if [[ "$s" == "Active" || "$s" == "Inactive" ]]; then
-
- if [ "$s" == "Active" ]; then
- statusword=" Active"
- else
- statusword="Inactive"
- fi
-
- let "s_no++"
- kcd=`echo ${key_status_array[$s_no]} | sed 's/T/ /' | awk '{print $1}'`
- let keypos=${s_no}+1
- if [ "$OS" = "macOS" ]; then
- key_status_accumulator=" ${statusword} key ${key_status_array[$keypos]} is $(((`date -jf %Y-%m-%d $TODAY +%s` - `date -jf %Y-%m-%d $kcd +%s`)/86400)) days old\n${key_status_accumulator}"
- else
- key_status_accumulator=" ${statusword} key ${key_status_array[$keypos]} is $(((`date -d "$TODAY" "+%s"` - `date -d "$kcd" "+%s"`)/86400)) days old\n${key_status_accumulator}"
- fi
- else
- let "s_no++"
- fi
- done
-
- fi
- cred_profile_keys[$cred_profilecounter]=$key_status_accumulator
-
-## DEBUG
-# echo "PROFILE IDENT: $profile_ident"
-# echo "USER ARN: ${cred_profile_arn[$cred_profilecounter]}"
-# echo "USER NAME: ${cred_profile_user[$cred_profilecounter]}"
-# echo "MFA ARN: ${mfa_arns[$cred_profilecounter]}"
-
- # erase variables & increase iterator for the next iteration
- user_arn=""
- profile_ident=""
- profile_username=""
-
- cred_profilecounter=$(($cred_profilecounter+1))
-
- fi
- done < $CREDFILE
-
- # create profile selections for key rotation
- echo "CONFIGURED AWS PROFILES AND THEIR ASSOCIATED KEYS:"
- echo
- SELECTR=0
- ITER=1
- for i in "${cred_profiles[@]}"
- do
- if [ "${cred_profile_arn[$SELECTR]}" = "INVALID" ]; then
- echo "X: $i (${cred_profile_user[$SELECTR]})"
- else
- echo "${ITER}: $i (${cred_profile_user[$SELECTR]})"
- printf "${cred_profile_keys[$SELECTR]}"
- fi
- echo
- let ITER=${ITER}+1
- let SELECTR=${SELECTR}+1
- done
-
- # prompt for profile selection
- printf "SELECT THE PROFILE WHOSE KEYS YOU WANT TO ROTATE (or press [ENTER] to abort): "
- read -r selprofile
-
- # process the selection
- if [ "$selprofile" != "" ]; then
- #capture the numeric part of the selection
- [[ $selprofile =~ ^([[:digit:]]+) ]] &&
- selprofile_check="${BASH_REMATCH[1]}"
- if [ "$selprofile_check" != "" ]; then
-
- # if the numeric selection was found,
- # translate it to the array index and validate
- let actual_selprofile=${selprofile_check}-1
-
- profilecount=${#cred_profiles[@]}
- if [[ $actual_selprofile -ge $profilecount ||
- $actual_selprofile -lt 0 ]]; then
- # a selection outside of the existing range was specified
- echo
- echo "There is no profile '${selprofile}'."
- echo
- exit 1
- fi
-
- # a base profile was selected
- if [[ $selprofile =~ ^[[:digit:]]+$ ]]; then
-
- if [ "${cred_profile_arn[$actual_selprofile]}" = "INVALID" ]; then
- echo
- echo "PROFILE \"${cred_profiles[$actual_selprofile]}\" HAS INVALID ACCESS KEYS. Cannot proceed."
- echo
- exit 1
- else
- echo
- echo "SELECTED PROFILE: ${cred_profiles[$actual_selprofile]}"
- final_selection="${cred_profiles[$actual_selprofile]}"
- final_selection_name="${cred_profile_user[$actual_selprofile]}"
- echo "SELECTED USER: $final_selection_name"
- fi
- else
- # non-acceptable characters were present in the selection
- echo
- echo "There is no profile '${selprofile}'."
- echo
- exit 1
- fi
-
- else
- # no numeric part in selection
- echo
- echo "There is no profile '${selprofile}'."
- echo
- exit 1
- fi
-
- else
- # empty selection; exit
- echo
- echo "Aborting. No changes were made."
- echo
- exit 1
- fi
-
- fi
-
- # does mfasession profile exist for the chosen profile?
- mfaprofile_exists=false
- ITERATR=0
- while IFS='' read -r line || [[ -n "$line" ]]; do
- [[ "$line" =~ ^\[(.*)\].* ]] &&
- all_profile_ident=${BASH_REMATCH[1]}
- if [[ $all_profile_ident != "" &&
- "$all_profile_ident" =~ ${final_selection}-mfasession ]]; then
- mfaprofile_exists=true
- break
- let "ITERATR++"
- fi
- all_profile_ident=""
- done < $CREDFILE
-
- # mfasession exists -- use it?
- use_mfaprofile=false
- if [ "$mfaprofile_exists" = "true" ]; then
-
- echo
- echo "-----------------------------------------------------------------"
- echo
- echo -e "An MFA profile ('${final_selection}-mfasession') exists\nfor the profile whose keys you have chosen to rotate."
- echo
- echo -e "Do you want to use it to execute the key rotation for\nthe profile you selected? If MFA is being enforced\nfor the profile selected for rotation, it may not be\nauthorized to carry out its own key rotation and the MFA\nprofile may need to be used instead."
- echo
- echo -e "Note that the MFA profile session MUST BE ACTIVE for it\nto work. Select Abort below if you need to activate\nthe MFA session before proceeding."
- echo
-
- while true; do
- read -p "Use the MFA profile to authorize the key rotation? Yes/No/Abort " ync
- case $ync in
- [Yy]* ) use_mfaprofile=true; break;;
- [Nn]* ) break;;
- [Aa]* ) echo; exit;;
- * ) echo "Please answer (y)es, (n)o, or (a)bort.";;
- esac
- done
- fi
-
- if [ "$use_mfaprofile" = "false" ]; then
- selected_authprofile=$final_selection
- else
- selected_authprofile=${final_selection}-mfasession
-
- echo "Please wait..."
-
- # check for expired MFA session
- EXISTING_KEYS_CREATEDATES=$(aws iam list-access-keys --query 'AccessKeyMetadata[].CreateDate' --output text --profile "$selected_authprofile" 2>&1)
- if [[ "$EXISTING_KEYS_CREATEDATES" =~ ExpiredToken ]]; then
- echo -e "\nYour MFA token has expired. Refresh your MFA session\nfor the profile '${final_selection}', and try again.\n\nAborting.\n"
- exit 1
- fi
-
- fi
+fi
### BEGIN KEY ROTATION SEQUENCE
- echo
- echo "Verifying that AWS CLI has configured credentials ..."
- ORIGINAL_ACCESS_KEY_ID=$(aws configure get aws_access_key_id --profile "$final_selection")
- ORIGINAL_SECRET_ACCESS_KEY=$(aws configure get aws_secret_access_key --profile "$final_selection")
- if [ -z "$ORIGINAL_ACCESS_KEY_ID" ]; then
- >&2 echo "ERROR: No aws_access_key_id/aws_secret_access_key configured for AWS CLI. Run 'aws configure' with your current keys."
- exit 1
- fi
-
- EXISTING_KEYS_CREATEDATES=0
- EXISTING_KEYS_CREATEDATES=($(aws iam list-access-keys --query 'AccessKeyMetadata[].CreateDate' --output text --profile "$selected_authprofile"))
- NUM_EXISTING_KEYS=${#EXISTING_KEYS_CREATEDATES[@]}
- if [ ${NUM_EXISTING_KEYS} -lt 2 ]; then
- echo "You have only one existing key. Now proceeding with new key creation."
-
- else
- echo "You have two keys (maximum number). We must make space ..."
-
- IFS=$'\n' sorted_createdates=($(sort <<<"${EXISTING_KEYS_CREATEDATES[*]}"))
- unset IFS
-
- echo "Now acquiring data for the older key ..."
- OLDER_KEY_CREATEDATE="${sorted_createdates[0]}"
- OLDER_KEY_ID=$(aws iam list-access-keys --query "AccessKeyMetadata[?CreateDate=='${OLDER_KEY_CREATEDATE}'].AccessKeyId" --output text --profile "$selected_authprofile")
- OLDER_KEY_STATUS=$(aws iam list-access-keys --query "AccessKeyMetadata[?CreateDate=='${OLDER_KEY_CREATEDATE}'].Status" --output text --profile "$selected_authprofile")
-
- echo "Now acquiring data for the newer key ..."
- NEWER_KEY_CREATEDATE="${sorted_createdates[1]}"
- NEWER_KEY_ID=$(aws iam list-access-keys --query "AccessKeyMetadata[?CreateDate=='${NEWER_KEY_CREATEDATE}'].AccessKeyId" --output text --profile "$selected_authprofile")
- NEWER_KEY_STATUS=$(aws iam list-access-keys --query "AccessKeyMetadata[?CreateDate=='${NEWER_KEY_CREATEDATE}'].Status" --output text --profile "$selected_authprofile")
-
- key_in_use=""
- allow_older_key_delete=false
- allow_newer_key_delete=false
- if [ ${OLDER_KEY_STATUS} = "Active" ] &&
- [ ${NEWER_KEY_STATUS} = "Active" ] &&
- [ "${NEWER_KEY_ID}" = "${ORIGINAL_ACCESS_KEY_ID}" ]; then
- # both keys are active, newer key is in use
- key_in_use="newer"
- allow_older_key_delete=true
- key_id_can_delete=$OLDER_KEY_ID
- key_id_remaining=$NEWER_KEY_ID
- elif [ ${OLDER_KEY_STATUS} = "Active" ] &&
- [ ${NEWER_KEY_STATUS} = "Active" ] &&
- [ "${OLDER_KEY_ID}" = "${ORIGINAL_ACCESS_KEY_ID}" ]; then
- # both keys are active, older key is in use
- key_in_use="older"
- allow_newer_key_delete=true
- key_id_can_delete=$NEWER_KEY_ID
- key_id_remaining=$OLDER_KEY_ID
- elif [ ${OLDER_KEY_STATUS} = "Inactive" ] &&
- [ ${NEWER_KEY_STATUS} = "Active" ]; then
- # newer key is active and in use
- key_in_use="newer"
- allow_older_key_delete=true
- key_id_can_delete=$OLDER_KEY_ID
- key_id_remaining=$NEWER_KEY_ID
- elif [ ${OLDER_KEY_STATUS} = "Active" ] &&
- [ ${NEWER_KEY_STATUS} = "Inactive" ]; then
- # older key is active and in use
- key_in_use="older"
- allow_newer_key_delete=true
- key_id_can_delete=$NEWER_KEY_ID
- else
- echo "You don't have keys I can delete to make space for the new key. Please delete a key manually and then try again."
- echo "Aborting."
- exit 1
- fi
-
- fi
-
- if [ "${allow_older_key_delete}" = "true" ] ||
- [ "${allow_newer_key_delete}" = "true" ]; then
- echo "To proceed you must delete one of your two existing keys; they are listed below:"
- echo
- echo "OLDER EXISTING KEY (${OLDER_KEY_STATUS}, created on ${OLDER_KEY_CREATEDATE}):"
- echo -n "Key Access ID: ${OLDER_KEY_ID} "
- if [ "${allow_older_key_delete}" = "true" ]; then
- echo "(this key can be deleted)"
- elif [ "${key_in_use}" = "older" ]; then
- echo "(this key is currently your active key)"
- fi
- echo
- echo "NEWER EXISTING KEY (${NEWER_KEY_STATUS}, created on ${NEWER_KEY_CREATEDATE}):"
- echo -n "Key Access ID: ${NEWER_KEY_ID} "
- if [ "${allow_newer_key_delete}" = "true" ]; then
- echo "(this key can be deleted)"
- elif [ "${key_in_use}" = "newer" ]; then
- echo "(this key is currently your active key)"
- fi
- echo
- echo
- echo "Enter below the Access Key ID of the key to delete, or leave empty to cancel, then press enter."
- read key_in
-
- if [ "${key_in}" = "${key_id_can_delete}" ]; then
- echo "Now deleting the key ${key_id_can_delete}"
- aws iam delete-access-key --access-key-id "${key_id_can_delete}" --profile "$selected_authprofile"
- if [ $? -ne 0 ]; then
- echo
- echo "Could not delete the access keyID ${key_id_can_delete}. Cannot proceed."
- if [ "$use_mfaprofile" = "true" ]; then
- echo -e "\nNOTE: If you see access denied/not authorized error above,\nyour MFA session may have expired.\n"
- else
- echo -e "\nNOTE: If you see access denied/not authorized error above, you may need\nto use MFA session to authorize the key rotation.\n"
- fi
- echo "Aborting."
- exit 1
- fi
- elif [ "${key_in}" = "" ]; then
- echo Aborting.
- exit 1
- else
- echo "The input did not match the Access Key ID of the key that can be deleted. Run the script again to retry."
- echo "Aborting."
- exit 1
- fi
- fi
-
- echo
- echo "Creating a new access key for the current IAM user ..."
- NEW_KEY_RAW_OUTPUT=$(aws iam create-access-key --output text --profile "$selected_authprofile")
- NEW_KEY_DATA=($(printf '%s' "${NEW_KEY_RAW_OUTPUT}" | awk {'printf ("%5s\t%s", $2, $4)'}))
- NEW_AWS_ACCESS_KEY_ID="${NEW_KEY_DATA[0]}"
- NEW_AWS_SECRET_ACCESS_KEY="${NEW_KEY_DATA[1]}"
-
- echo "Verifying that the new key was created ..."
- EXISTING_KEYS_ACCESS_IDS=($(aws iam list-access-keys --query 'AccessKeyMetadata[].AccessKeyId' --output text --profile "$selected_authprofile"))
- NUM_EXISTING_KEYS=${#EXISTING_KEYS_ACCESS_IDS[@]}
- if [ ${NUM_EXISTING_KEYS} -lt 2 ]; then
- >&2 echo "Something went wrong; the new key was not created."
- echo "Aborting"
- exit 1
- fi
-
- echo "Pausing to wait for the IAM changes to propagate ..."
- COUNT=0
- MAX_COUNT=20
- SUCCESS=false
- while [ "$SUCCESS" = false ] && [ "$COUNT" -lt "$MAX_COUNT" ]; do
- sleep 10
- aws iam list-access-keys --profile "$selected_authprofile" > /dev/null && RETURN_CODE=$? || RETURN_CODE=$?
- if [ "$RETURN_CODE" -eq 0 ]; then
- SUCCESS=true
- else
- COUNT=$((COUNT+1))
- echo "(Still waiting for the key propagation to complete ...)"
- fi
- done
-
- if [ "$SUCCESS" = "true" ]; then
-
- echo "Key propagation complete."
- echo "Configuring new access key for AWS CLI ..."
- aws configure set aws_access_key_id "$NEW_AWS_ACCESS_KEY_ID" --profile "$final_selection"
- aws configure set aws_secret_access_key "$NEW_AWS_SECRET_ACCESS_KEY" --profile "$final_selection"
-
- echo "Verifying the new key is in place, and that IAM access still works ..."
- revert=false
- CONFIGURED_ACCESS_KEY=$(aws configure get aws_access_key_id --profile "$final_selection")
- if [ "$CONFIGURED_ACCESS_KEY" != "$NEW_AWS_ACCESS_KEY_ID" ]; then
- >&2 echo "Something went wrong; the new key could not be taken into use; the local 'aws configure' failed."
- revert=true
- fi
-
- # this is just to test access via AWS CLI; the content here doesn't matter (other than that we get a result)
- EXISTING_KEYS_ACCESS_IDS=($(aws iam list-access-keys --query 'AccessKeyMetadata[].AccessKeyId' --output text --profile "$selected_authprofile"))
- NUM_EXISTING_KEYS=${#EXISTING_KEYS_ACCESS_IDS[@]}
- if [ ${NUM_EXISTING_KEYS} -ne 2 ]; then
- >&2 echo "Something went wrong; the new key could not access AWS CLI."
- revert=true
- fi
-
- if [ "${revert}" = "true" ]; then
- echo "Reverting configuration to use the old keys."
- aws configure set aws_access_key_id "$ORIGINAL_ACCESS_KEY_ID" --profile "$final_selection"
- aws configure set aws_secret_access_key "$ORIGINAL_SECRET_ACCESS_KEY" --profile "$final_selection"
-
- echo "Original configuration restored."
- echo "Aborting."
- exit 1
- fi
-
- echo "Deleting the previously active access key ..."
- aws iam delete-access-key --access-key-id "$ORIGINAL_ACCESS_KEY_ID" --profile "$selected_authprofile"
-
- echo "Verifying old access key got deleted ..."
- # this is just to test access via AWS CLI; the content here doesn't matter (other than that we get a result)
- EXISTING_KEYS_ACCESS_IDS=($(aws iam list-access-keys --query 'AccessKeyMetadata[].AccessKeyId' --output text --profile "$selected_authprofile"))
- NUM_EXISTING_KEYS=${#EXISTING_KEYS_ACCESS_IDS[@]}
- if [ ${NUM_EXISTING_KEYS} -ne 1 ]; then
- echo
- >&2 echo "Something went wrong deleting the old key, however, YOUR NEW KEY IS NOW IN USE."
- if [ "$use_mfaprofile" = "true" ]; then
- echo -e "\nNOTE: If you see access denied/not authorized error above,\nyour MFA session may have expired.\n"
- else
- echo -e "\nNOTE: If you see access denied/not authorized error above, you may need\nto use MFA session to authorize the key rotation.\n"
- fi
- fi
- echo
- echo "The key for the profile '${final_selection}' (IAM user '${final_selection_name}') has been rotated."
- echo "Successfully switched from the old access key ${ORIGINAL_ACCESS_KEY_ID} to ${NEW_AWS_ACCESS_KEY_ID}"
- echo "Process complete."
- echo
- exit 0
-
- else
-
- echo "Key propagation did not complete within the allotted time. This delay is caused by AWS, and does \
+echo
+echo "Verifying that AWS CLI has configured credentials ..."
+ORIGINAL_ACCESS_KEY_ID=$(aws configure get aws_access_key_id --profile "$final_selection")
+ORIGINAL_SECRET_ACCESS_KEY=$(aws configure get aws_secret_access_key --profile "$final_selection")
+if [ -z "$ORIGINAL_ACCESS_KEY_ID" ]; then
+ >&2 echo "ERROR: No aws_access_key_id/aws_secret_access_key configured for AWS CLI. Run 'aws configure' with your current keys."
+ exit 1
+fi
+
+EXISTING_KEYS_CREATEDATES=0
+EXISTING_KEYS_CREATEDATES=($(aws iam list-access-keys --query 'AccessKeyMetadata[].CreateDate' --output text --profile "$selected_authprofile"))
+NUM_EXISTING_KEYS=${#EXISTING_KEYS_CREATEDATES[@]}
+if [ ${NUM_EXISTING_KEYS} -lt 2 ]; then
+ echo "You have only one existing key. Now proceeding with new key creation."
+else
+ echo "You have two keys (maximum number). We must make space ..."
+
+ IFS=$'\n' sorted_createdates=($(sort <<<"${EXISTING_KEYS_CREATEDATES[*]}"))
+ unset IFS
+
+ echo "Now acquiring data for the older key ..."
+ OLDER_KEY_CREATEDATE="${sorted_createdates[0]}"
+ OLDER_KEY_ID=$(aws iam list-access-keys --query "AccessKeyMetadata[?CreateDate=='${OLDER_KEY_CREATEDATE}'].AccessKeyId" --output text --profile "$selected_authprofile")
+ OLDER_KEY_STATUS=$(aws iam list-access-keys --query "AccessKeyMetadata[?CreateDate=='${OLDER_KEY_CREATEDATE}'].Status" --output text --profile "$selected_authprofile")
+
+ echo "Now acquiring data for the newer key ..."
+ NEWER_KEY_CREATEDATE="${sorted_createdates[1]}"
+ NEWER_KEY_ID=$(aws iam list-access-keys --query "AccessKeyMetadata[?CreateDate=='${NEWER_KEY_CREATEDATE}'].AccessKeyId" --output text --profile "$selected_authprofile")
+ NEWER_KEY_STATUS=$(aws iam list-access-keys --query "AccessKeyMetadata[?CreateDate=='${NEWER_KEY_CREATEDATE}'].Status" --output text --profile "$selected_authprofile")
+
+ key_in_use=""
+ allow_older_key_delete="false"
+ allow_newer_key_delete="false"
+ if [ ${OLDER_KEY_STATUS} = "Active" ] &&
+ [ ${NEWER_KEY_STATUS} = "Active" ] &&
+ [ "${NEWER_KEY_ID}" = "${ORIGINAL_ACCESS_KEY_ID}" ]; then
+ # both keys are active, newer key is in use
+ key_in_use="newer"
+ allow_older_key_delete="true"
+ key_id_can_delete=$OLDER_KEY_ID
+ key_id_remaining=$NEWER_KEY_ID
+ elif [ ${OLDER_KEY_STATUS} = "Active" ] &&
+ [ ${NEWER_KEY_STATUS} = "Active" ] &&
+ [ "${OLDER_KEY_ID}" = "${ORIGINAL_ACCESS_KEY_ID}" ]; then
+ # both keys are active, older key is in use
+ key_in_use="older"
+ allow_newer_key_delete="true"
+ key_id_can_delete=$NEWER_KEY_ID
+ key_id_remaining=$OLDER_KEY_ID
+ elif [ ${OLDER_KEY_STATUS} = "Inactive" ] &&
+ [ ${NEWER_KEY_STATUS} = "Active" ]; then
+ # newer key is active and in use
+ key_in_use="newer"
+ allow_older_key_delete="true"
+ key_id_can_delete=$OLDER_KEY_ID
+ key_id_remaining=$NEWER_KEY_ID
+ elif [ ${OLDER_KEY_STATUS} = "Active" ] &&
+ [ ${NEWER_KEY_STATUS} = "Inactive" ]; then
+ # older key is active and in use
+ key_in_use="older"
+ allow_newer_key_delete="true"
+ key_id_can_delete=$NEWER_KEY_ID
+ else
+ echo "You don't have keys I can delete to make space for the new key. Please delete a key manually and then try again."
+ echo "Aborting."
+ exit 1
+ fi
+
+fi
+
+if [ "${allow_older_key_delete}" = "true" ] ||
+ [ "${allow_newer_key_delete}" = "true" ]; then
+ echo "To proceed you must delete one of your two existing keys; they are listed below:"
+ echo
+ echo "OLDER EXISTING KEY (${OLDER_KEY_STATUS}, created on ${OLDER_KEY_CREATEDATE}):"
+ echo -n "Key Access ID: ${OLDER_KEY_ID} "
+ if [ "${allow_older_key_delete}" = "true" ]; then
+ echo "(this key can be deleted)"
+ elif [ "${key_in_use}" = "older" ]; then
+ echo "(this key is currently your active key)"
+ fi
+ echo
+ echo "NEWER EXISTING KEY (${NEWER_KEY_STATUS}, created on ${NEWER_KEY_CREATEDATE}):"
+ echo -n "Key Access ID: ${NEWER_KEY_ID} "
+ if [ "${allow_newer_key_delete}" = "true" ]; then
+ echo "(this key can be deleted)"
+ elif [ "${key_in_use}" = "newer" ]; then
+ echo "(this key is currently your active key)"
+ fi
+ echo
+ echo
+ echo "Enter below the Access Key ID of the key to delete, or leave empty to cancel, then press enter."
+ read key_in
+
+ if [ "${key_in}" = "${key_id_can_delete}" ]; then
+ echo "Now deleting the key ${key_id_can_delete}"
+ aws iam delete-access-key --access-key-id "${key_id_can_delete}" --profile "$selected_authprofile"
+ if [ $? -ne 0 ]; then
+ echo
+ echo "Could not delete the access keyID ${key_id_can_delete}. Cannot proceed."
+ if [ "$use_mfaprofile" = "true" ]; then
+ echo -e "\nNOTE: If you see access denied/not authorized error above,\nyour MFA session may have expired.\n"
+ else
+ echo -e "\nNOTE: If you see access denied/not authorized error above, you may need\nto use MFA session to authorize the key rotation.\n"
+ fi
+ echo "Aborting."
+ exit 1
+ fi
+ elif [ "${key_in}" = "" ]; then
+ echo Aborting.
+ exit 1
+ else
+ echo "The input did not match the Access Key ID of the key that can be deleted. Run the script again to retry."
+ echo "Aborting."
+ exit 1
+ fi
+fi
+
+echo
+echo "Creating a new access key for the current IAM user ..."
+NEW_KEY_RAW_OUTPUT=$(aws iam create-access-key --output text --profile "$selected_authprofile")
+NEW_KEY_DATA=($(printf '%s' "${NEW_KEY_RAW_OUTPUT}" | awk {'printf ("%5s\t%s", $2, $4)'}))
+NEW_AWS_ACCESS_KEY_ID="${NEW_KEY_DATA[0]}"
+NEW_AWS_SECRET_ACCESS_KEY="${NEW_KEY_DATA[1]}"
+
+echo "Verifying that the new key was created ..."
+EXISTING_KEYS_ACCESS_IDS=($(aws iam list-access-keys --query 'AccessKeyMetadata[].AccessKeyId' --output text --profile "$selected_authprofile"))
+NUM_EXISTING_KEYS=${#EXISTING_KEYS_ACCESS_IDS[@]}
+if [ ${NUM_EXISTING_KEYS} -lt 2 ]; then
+ >&2 echo "Something went wrong; the new key was not created."
+ echo "Aborting"
+ exit 1
+fi
+
+echo "Pausing to wait for the IAM changes to propagate ..."
+COUNT=0
+MAX_COUNT=20
+SUCCESS="false"
+while [ "$SUCCESS" = "false" ] && [ "$COUNT" -lt "$MAX_COUNT" ]; do
+ sleep 10
+ aws iam list-access-keys --profile "$selected_authprofile" > /dev/null && RETURN_CODE=$? || RETURN_CODE=$?
+ if [ "$RETURN_CODE" -eq 0 ]; then
+ SUCCESS="true"
+ else
+ COUNT=$((COUNT+1))
+ echo "(Still waiting for the key propagation to complete ...)"
+ fi
+done
+
+if [ "$SUCCESS" = "true" ]; then
+
+ echo "Key propagation complete."
+ echo "Configuring new access key for AWS CLI ..."
+ aws configure set aws_access_key_id "$NEW_AWS_ACCESS_KEY_ID" --profile "$final_selection"
+ aws configure set aws_secret_access_key "$NEW_AWS_SECRET_ACCESS_KEY" --profile "$final_selection"
+
+ echo "Verifying the new key is in place, and that IAM access still works ..."
+ revert="false"
+ CONFIGURED_ACCESS_KEY=$(aws configure get aws_access_key_id --profile "$final_selection")
+ if [ "$CONFIGURED_ACCESS_KEY" != "$NEW_AWS_ACCESS_KEY_ID" ]; then
+ >&2 echo "Something went wrong; the new key could not be taken into use; the local 'aws configure' failed."
+ revert="true"
+ fi
+
+ # this is just to test access via AWS CLI; the content here doesn't matter (other than that we get a result)
+ EXISTING_KEYS_ACCESS_IDS=($(aws iam list-access-keys --query 'AccessKeyMetadata[].AccessKeyId' --output text --profile "$selected_authprofile"))
+ NUM_EXISTING_KEYS=${#EXISTING_KEYS_ACCESS_IDS[@]}
+ if [ ${NUM_EXISTING_KEYS} -ne 2 ]; then
+ >&2 echo "Something went wrong; the new key could not access AWS CLI."
+ revert="true"
+ fi
+
+ if [ "${revert}" = "true" ]; then
+ echo "Reverting configuration to use the old keys."
+ aws configure set aws_access_key_id "$ORIGINAL_ACCESS_KEY_ID" --profile "$final_selection"
+ aws configure set aws_secret_access_key "$ORIGINAL_SECRET_ACCESS_KEY" --profile "$final_selection"
+
+ echo "Original configuration restored."
+ echo "Aborting."
+ exit 1
+ fi
+
+ echo "Deleting the previously active access key ..."
+ aws iam delete-access-key --access-key-id "$ORIGINAL_ACCESS_KEY_ID" --profile "$selected_authprofile"
+
+ echo "Verifying old access key got deleted ..."
+ # this is just to test access via AWS CLI; the content here doesn't matter (other than that we get a result)
+ EXISTING_KEYS_ACCESS_IDS=($(aws iam list-access-keys --query 'AccessKeyMetadata[].AccessKeyId' --output text --profile "$selected_authprofile"))
+ NUM_EXISTING_KEYS=${#EXISTING_KEYS_ACCESS_IDS[@]}
+ if [ ${NUM_EXISTING_KEYS} -ne 1 ]; then
+ echo
+ >&2 echo "Something went wrong deleting the old key, however, YOUR NEW KEY IS NOW IN USE."
+ if [ "$use_mfaprofile" = "true" ]; then
+ echo -e "\nNOTE: If you see access denied/not authorized error above,\nyour MFA session may have expired.\n"
+ else
+ echo -e "\nNOTE: If you see access denied/not authorized error above, you may need\nto use MFA session to authorize the key rotation.\n"
+ fi
+ fi
+ echo
+ echo "The key for the profile '${final_selection}' (IAM user '${final_selection_name}') has been rotated."
+ echo "Successfully switched from the old access key ${ORIGINAL_ACCESS_KEY_ID} to ${NEW_AWS_ACCESS_KEY_ID}"
+ echo "Process complete."
+ echo
+ exit 0
+
+else
+
+ echo "Key propagation did not complete within the allotted time. This delay is caused by AWS, and does \
not necessarily indicate an error. However, the newly generated key cannot be safely taken into use before \
the propagation has completed. Please wait for some time, and try to temporarily replace the Access Key ID \
and the Secret Access Key in your ~/.aws/credentials file with the new key details (below). Keep the old keys safe \
until you have confirmed that the new key works."
- echo
- echo "PLEASE MAKE NOTE OF THE NEW KEY DETAILS BELOW; THEY HAVE NOT BEEN SAVED ELSEWHERE!"
- echo
- echo "New AWS Access Key ID: ${NEW_AWS_ACCESS_KEY_ID}"
- echo "New AWS Secret Access Key: ${NEW_AWS_SECRET_ACCESS_KEY}"
- echo
- exit 1
-
- fi
+ echo
+ echo "PLEASE MAKE NOTE OF THE NEW KEY DETAILS BELOW; THEY HAVE NOT BEEN SAVED ELSEWHERE!"
+ echo
+ echo "New AWS Access Key ID: ${NEW_AWS_ACCESS_KEY_ID}"
+ echo "New AWS Secret Access Key: ${NEW_AWS_SECRET_ACCESS_KEY}"
+ echo
+ exit 1
+
+fi
diff --git a/awscli-mfa.sh b/awscli-mfa.sh
deleted file mode 100755
index bdf4419..0000000
--- a/awscli-mfa.sh
+++ /dev/null
@@ -1,436 +0,0 @@
-#!/bin/bash
-
-# Set the session length in seconds below;
-# note that this only sets the client-side
-# validity of the MFA session token;
-# the maximum length of a valid session
-# is enforced in the IAM policy, and
-# is unaffected by this value.
-#
-# The minimum valid session length
-# is 900 seconds.
-MFA_SESSION_LENGTH_IN_SECONDS=900
-
-## PREREQUISITES CHECK
-
-# `exists` for commands
-exists() {
- command -v "$1" >/dev/null 2>&1
-}
-
-# is AWS CLI installed?
-if ! exists aws ; then
- printf "\n******************************************************************************************************************************\n\
-This script requires the AWS CLI. See the details here: http://docs.aws.amazon.com/cli/latest/userguide/cli-install-macos.html\n\
-******************************************************************************************************************************\n\n"
- exit 1
-fi
-
-# check for ~/.aws directory, and ~/.aws/{config|credentials} files
-if [ ! -d ~/.aws ]; then
- echo
- echo -e "'~/.aws' directory not present.\nMake sure it exists, and that you have at least one profile configured\nusing the 'config' and 'credentials' files within that directory."
- echo
- exit 1
-fi
-
-if [[ ! -f ~/.aws/config && ! -f ~/.aws/credentials ]]; then
- echo
- echo -e "'~/.aws/config' and '~/.aws/credentials' files not present.\nMake sure they exist. See http://docs.aws.amazon.com/cli/latest/userguide/cli-config-files.html for details on how to set them up."
- echo
- exit 1
-elif [ ! -f ~/.aws/config ]; then
- echo
- echo -e "'~/.aws/config' file not present.\nMake sure it and '~/.aws/credentials' files exists. See http://docs.aws.amazon.com/cli/latest/userguide/cli-config-files.html for details on how to set them up."
- echo
- exit 1
-elif [ ! -f ~/.aws/credentials ]; then
- echo
- echo -e "'~/.aws/credentials' file not present.\nMake sure it and '~/.aws/config' files exists. See http://docs.aws.amazon.com/cli/latest/userguide/cli-config-files.html for details on how to set them up."
- echo
- exit 1
-fi
-
-CREDFILE=~/.aws/credentials
-# check that at least one profile is configured
-ONEPROFILE="false"
-while IFS='' read -r line || [[ -n "$line" ]]; do
- [[ "$line" =~ ^\[(.*)\].* ]] &&
- profile_ident=${BASH_REMATCH[1]}
-
- if [ $profile_ident != "" ]; then
- ONEPROFILE="true"
- fi
-done < $CREDFILE
-
-
-if [[ "$ONEPROFILE" = "false" ]]; then
- echo
- echo -e "NO CONFIGURED AWS PROFILES FOUND.\nPlease make sure you have '~/.aws/config' (profile configurations),\nand '~/.aws/credentials' (profile credentials) files, and at least\none configured profile. For more info, see AWS CLI documentation at:\nhttp://docs.aws.amazon.com/cli/latest/userguide/cli-config-files.html"
- echo
-
-else
-
- # Check OS for some supported platforms
- OS="`uname`"
- case $OS in
- 'Linux')
- OS='Linux'
- ;;
- 'Darwin')
- OS='macOS'
- ;;
- *)
- OS='unknown'
- echo
- echo "** NOTE: THIS SCRIPT HAS NOT BEEN TESTED ON YOUR CURRENT PLATFORM."
- echo
- ;;
- esac
-
- # make sure ~/.aws/credentials has a linefeed in the end
- c=$(tail -c 1 "$CREDFILE")
- if [ "$c" != "" ]; then
- echo "" >> "$CREDFILE"
- fi
-
- ## PREREQS PASSED; PROCEED..
-
- declare -a cred_profiles
- declare -a cred_profile_status
- declare -a cred_profile_user
- declare -a cred_profile_arn
- declare -a profile_region
- declare -a profile_output
- declare -a mfa_profiles
- declare -a mfa_arns
- declare -a mfa_profile_status
- cred_profilecounter=0
-
- echo "Please wait..."
-
- while IFS='' read -r line || [[ -n "$line" ]]; do
- [[ "$line" =~ ^\[(.*)\].* ]] &&
- profile_ident=${BASH_REMATCH[1]}
-
- # only process if profile identifier is present,
- # and if it's not a mfasession profile
- if [ "$profile_ident" != "" ] &&
- ! [[ "$profile_ident" =~ -mfasession$ ]]; then
-
- cred_profiles[$cred_profilecounter]=$profile_ident
-
- profile_region[$cred_profilecounter]=$(aws --profile $profile_ident configure get region)
- profile_output[$cred_profilecounter]=$(aws --profile $profile_ident configure get output)
-
- # get user ARN; this should be always available
- user_arn="$(aws sts get-caller-identity --profile $profile_ident --output text --query 'Arn' 2>&1)"
- if [[ "$user_arn" =~ ^arn:aws ]]; then
- cred_profile_arn[$cred_profilecounter]=$user_arn
- else
- cred_profile_arn[$cred_profilecounter]=""
- fi
-
- # get the actual username (may be different from the arbitrary profile ident)
- [[ "$user_arn" =~ ([^/]+)$ ]] &&
- profile_username="${BASH_REMATCH[1]}"
- if [[ "$profile_username" =~ error ]]; then
- cred_profile_user[$cred_profilecounter]=""
- else
- cred_profile_user[$cred_profilecounter]="$profile_username"
- fi
-
- # find existing MFA sessions for the current profile
- while IFS='' read -r line || [[ -n "$line" ]]; do
- [[ "$line" =~ \[(${profile_ident}-mfasession)\]$ ]] &&
- mfa_profile_ident="${BASH_REMATCH[1]}"
- done < $CREDFILE
- mfa_profiles[$cred_profilecounter]="$mfa_profile_ident"
-
- # check to see if this profile has access
- # (this is not 100% as it depends on IAM access)
- profile_check="$(aws iam get-user --output text --query "User.Arn" --profile $profile_ident 2>&1)"
- if [[ "$profile_check" =~ ^arn:aws ]]; then
- cred_profile_status[$cred_profilecounter]="OK"
- else
- cred_profile_status[$cred_profilecounter]="LIMITED"
- fi
-
- # get MFA ARN if available
- mfa_arn="$(aws iam list-virtual-mfa-devices --profile $profile_ident --output text --query "VirtualMFADevices[?User.Arn=='${user_arn}'].SerialNumber" 2>&1)"
- if [[ "$mfa_arn" =~ ^arn:aws ]]; then
- mfa_arns[$cred_profilecounter]="$mfa_arn"
- else
- mfa_arns[$cred_profilecounter]=""
- fi
-
- # if existing MFA profile was found, check its status
- # (this is not 100% as it depends on IAM access)
- if [ "$mfa_profile_ident" != "" ]; then
- mfa_profile_check="$(aws iam get-user --output text --query "User.Arn" --profile $mfa_profile_ident 2>&1)"
- if [[ "$mfa_profile_check" =~ ^arn:aws ]]; then
- mfa_profile_status[$cred_profilecounter]="OK"
- elif [[ "$mfa_profile_check" =~ ExpiredToken ]]; then
- mfa_profile_status[$cred_profilecounter]="EXPIRED"
- else
- mfa_profile_status[$cred_profilecounter]="LIMITED"
- fi
- fi
-
-## DEBUG
-# echo "PROFILE IDENT: $profile_ident (${cred_profile_status[$cred_profilecounter]})"
-# echo "USER ARN: ${cred_profile_arn[$cred_profilecounter]}"
-# echo "USER NAME: ${cred_profile_user[$cred_profilecounter]}"
-# echo "MFA ARN: ${mfa_arns[$cred_profilecounter]}"
-# if [ "${mfa_profiles[$cred_profilecounter]}" == "" ]; then
-# echo "MFA PROFILE IDENT:"
-# else
-# echo "MFA PROFILE IDENT: ${mfa_profiles[$cred_profilecounter]} (${mfa_profile_status[$cred_profilecounter]})"
-# fi
-# echo
-
- # erase variables & increase iterator for the next iteration
- mfa_arn=""
- user_arn=""
- profile_ident=""
- profile_check=""
- profile_username=""
- mfa_profile_ident=""
- mfa_profile_check=""
-
- cred_profilecounter=$(($cred_profilecounter+1))
-
- fi
- done < $CREDFILE
-
- # create profile selections
- echo "AVAILABLE AWS PROFILES:"
- echo
- SELECTR=0
- ITER=1
- for i in "${cred_profiles[@]}"
- do
- if [ "${mfa_arns[$SELECTR]}" != "" ]; then
- mfa_notify=", MFA configured"
- else
- mfa_notify=""
- fi
-
- echo "${ITER}: $i (${cred_profile_user[$SELECTR]}${mfa_notify})"
-
- if [ "${mfa_profile_status[$SELECTR]}" = "OK" ] ||
- [ "${mfa_profile_status[$SELECTR]}" = "LIMITED" ]; then
- echo "${ITER}m: $i MFA profile in ${mfa_profile_status[$SELECTR]} status"
- fi
-
- echo
- let ITER=${ITER}+1
- let SELECTR=${SELECTR}+1
- done
-
- # prompt for profile selection
- printf "SELECT A PROFILE BY THE ID: "
- read -r selprofile
-
- # process the selection
- if [ "$selprofile" != "" ]; then
- #capture the numeric part of the selection
- [[ $selprofile =~ ^([[:digit:]]+) ]] &&
- selprofile_check="${BASH_REMATCH[1]}"
- if [ "$selprofile_check" != "" ]; then
-
- # if the numeric selection was found,
- # translate it to the array index and validate
- let actual_selprofile=${selprofile_check}-1
-
- profilecount=${#cred_profiles[@]}
- if [[ $actual_selprofile -ge $profilecount ||
- $actual_selprofile -lt 0 ]]; then
- # a selection outside of the existing range was specified
- echo "There is no profile '${selprofile}'."
- echo
- exit 1
- fi
-
- # was an existing MFA profile selected?
- [[ $selprofile =~ ^[[:digit:]]+(m)$ ]] &&
- selprofile_mfa_check="${BASH_REMATCH[1]}"
-
- if [[ "$selprofile_mfa_check" != "" &&
- ( "${mfa_profile_status[$actual_selprofile]}" = "OK" ||
- "${mfa_profile_status[$actual_selprofile]}" = "LIMITED" ) ]]; then
-
- echo "SELECTED MFA PROFILE: ${mfa_profiles[$actual_selprofile]}"
- final_selection="${mfa_profiles[$actual_selprofile]}"
-
- elif [[ "$selprofile_mfa_check" != "" &&
- "${mfa_profile_status[$actual_selprofile]}" = "" ]]; then
- # mfa ('m') profile was selected for a profile that no mfa profile exists
- echo "There is no profile '${selprofile}'."
- echo
- exit 1
-
- else
- # a base profile was selected
- if [[ $selprofile =~ ^[[:digit:]]+$ ]]; then
- echo "SELECTED PROFILE: ${cred_profiles[$actual_selprofile]}"
- final_selection="${cred_profiles[$actual_selprofile]}"
- else
- # non-acceptable characters were present in the selection
- echo "There is no profile '${selprofile}'."
- echo
- exit 1
- fi
- fi
-
- else
- # no numeric part in selection
- echo "There is no profile '${selprofile}'."
- echo
- exit 1
- fi
- else
- # empty selection
- echo "There is no profile '${selprofile}'."
- echo
- exit 1
- fi
-
- if [ "${mfa_arns[$actual_selprofile]}" != "" ]; then
- mfaprofile="true"
- # prompt for the MFA code since MFA has been configured for this profile
- echo
- echo -e "Enter the current MFA one time pass code for profile '${cred_profiles[$actual_selprofile]}' to start/renew an MFA session,\nor leave empty (just press [ENTER]) to use the selected profile as-is."
- while :
- do
- read mfacode
- if ! [[ "$mfacode" =~ ^$ || "$mfacode" =~ [0-9]{6} ]]; then
- echo "The MFA code must be exactly six digits, or blank to bypass."
- continue
- else
- break
- fi
- done
-
- else
- mfaprofile="false"
- mfacode=""
- echo
- echo -e "MFA has not been set up for this profile."
- fi
-
- if [ "$mfacode" != "" ]; then
-
- # init the MFA session (request a MFA session token)
- AWS_USER_PROFILE=${cred_profiles[$actual_selprofile]}
- AWS_2AUTH_PROFILE=${AWS_USER_PROFILE}-mfasession
- ARN_OF_MFA=${mfa_arns[$actual_selprofile]}
- MFA_TOKEN_CODE=$mfacode
- DURATION=$MFA_SESSION_LENGTH_IN_SECONDS
-
- echo "GETTING AN MFA SESSION TOKEN FOR THE PROFILE: $AWS_USER_PROFILE"
-
- 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 $MFA_TOKEN_CODE \
- --output text | awk '{ print $2, $4, $5 }')
-
- if [ -z "$AWS_ACCESS_KEY_ID" ]; then
- echo
- echo "Could not initialize the requested MFA session."
- echo
- exit 1
- fi
-
-## DEBUG
-# echo "AWS_ACCESS_KEY_ID: $AWS_ACCESS_KEY_ID"
-# echo "AWS_SECRET_ACCESS_KEY: $AWS_SECRET_ACCESS_KEY"
-# echo "AWS_SESSION_TOKEN: $AWS_SESSION_TOKEN"
-
- # set the temp aws_access_key_id, aws_secret_access_key, and aws_session_token for the MFA profile
- `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"`
-
- # get the current region and output for the region (are they set?)
- get_region=$(aws --profile $AWS_2AUTH_PROFILE configure get region)
- get_output=$(aws --profile $AWS_2AUTH_PROFILE configure get output)
-
- # if the region and output were not set, use the base profile values for
- # the MFA profiles (or deafults, if not set for the base, either)
- if [ "${get_region}" = "" ]; then
- if [ ${profile_region[$actual_selprofile]} != "" ]; then
- set_new_region=${profile_region[$actual_selprofile]}
- echo "Default region was not set for the MFA profile. It was set to same ('$set_new_region') as the base profile."
- else
- set_new_region="us-east-1"
- echo "Default region was not set for the MFA profile. It was set to the default 'us-east-1')."
- fi
-
- `aws --profile $AWS_2AUTH_PROFILE configure set region "${set_new_region}"`
- fi
-
- if [ "${get_output}" = "" ]; then
- if [ ${profile_output[$actual_selprofile]} != "" ]; then
- set_new_output=${profile_output[$actual_selprofile]}
- echo "Default output format was not set for the MFA profile. It was set to same ('$set_new_output') as the base profile."
- else
- set_new_region="json"
- echo "Default output format was not set for the MFA profile. It was set to the default 'json')."
- fi
-
- `aws --profile $AWS_2AUTH_PROFILE configure set output "${set_new_output}"`
- fi
-
- # Make sure the final selection profile name has '-mfasession' suffix
- # (it's not present when going from base profile to MFA profile)
- if ! [[ "$final_selection" =~ -mfasession$ ]]; then
- final_selection="${final_selection}-mfasession"
- fi
-
- fi
-
- # get region and output format for display (even when not entering MFA code)
- get_region=$(aws --profile $final_selection configure get region)
- get_output=$(aws --profile $final_selection configure get output)
-
- echo
- if [[ "$mfaprofile" = "true" && "$mfacode" != "" ]]; then
- echo "MFA profile name: '${final_selection}'"
- echo
- else
- echo "Profile name '${final_selection}'"
- echo "** NOTE: This is not an MFA session!"
- echo
- fi
- echo "Region is set to: $get_region"
- echo "Output format is set to: $get_output"
- echo
- if [ "$OS" = "macOS" ]; then
- echo "Execute the following in Terminal to activate this profile:"
- echo
- echo "export AWS_PROFILE=${final_selection}"
- echo
- echo -n "export AWS_PROFILE=${final_selection}" | pbcopy
- echo "(the activation command is now on your clipboard -- just paste in Terminal, and press [ENTER])"
- elif [ "$OS" = "Linux" ]; then
- echo "Execute the following on the command line to activate this profile:"
- echo
- echo "export AWS_PROFILE=${final_selection}"
- echo
- if exists xclip ; then
- echo -n "export AWS_PROFILE=${final_selection}" | xclip -i
- echo "(xclip found; the activation command is now on your X PRIMARY clipboard -- just paste on the command line, and press [ENTER])"
- else
- echo "If you're using an X GUI on Linux, install 'xclip' to have the activation command copied to the clipboard automatically."
- fi
- else
- echo "Execute the following on the command line to activate this profile:"
- echo
- echo "export AWS_PROFILE=${final_selection}"
- fi
- echo
-
-fi
diff --git a/awscli-mfa/README.md b/awscli-mfa/README.md
new file mode 100644
index 0000000..dde993c
--- /dev/null
+++ b/awscli-mfa/README.md
@@ -0,0 +1,353 @@
+
+# awscli-mfa and its companion scripts
+
+The `awscli-mfa.sh` and its companion scripts `enable-disable-vmfa-device.sh` `mfastatus.sh`, and `source-this-to-clear-AWS-envvars.sh` were created to make handling AWS MFA sessions on the command line easy.
+
+### Usage, quick!
+
+1. Configure the AWS profile using `aws configure` for the default profile (if you don't have any profiles configured yet), or `aws configure --profile "SomeDescriptiveProfileName"` for a new named profile. You can view the any existing profiles with `cat ~/.aws/credentials`. For an overview of the AWS configuration files, check out their [documentation page](https://docs.aws.amazon.com/cli/latest/userguide/cli-config-files.html).
+
+2. Execute `enable-disable-vmfa-device.sh` to create and enable a virtual MFA device with Google Authenticator compatible Authy app ([Android](https://play.google.com/store/apps/details?id=com.authy.authy&hl=en_US), [iOS](https://itunes.apple.com/us/app/authy/id494168017)) on your portable device. Follow the interactive directions from the script.
+
+3. Execute `awscli-mfa.sh` to start an MFA session using the vMFAd you just configured. Follow the interactive directions from the script.
+
+4. View the status and remaining validity periods for the current MFA sessions using the `mfastatus.sh` script.
+
+5. If you need to switch between the base profiles and/or active MFA sessions, re-execute `awscli-mfa.sh` and follow its prompts. If you need to disable/detach (and possibly delete) a vMFAd from an account, re-execute `enable-disable-vmfa-device.sh` and follow its interactive guidance.
+
+Keep reading for the rationale, overview, and in-depth usage information...
+
+### Rationale
+
+When the presence of a multi-factor authentication session to execute AWS commands (i.e. not just the login to the web console) is enforced using an IAM policy, the enforcement cannot be limited to the web console operations. This is because the AWS web console is basially a front-end to the AWS APIs which can also be accessed using the `aws cli`. When you log in to the web console and enter an MFA code, the browser takes care of caching the credentials and the session token, and so beyond that point the MFA session is transparent to the user until the session eventually expires, and the AWS web console prompts the user to log in again. On the command line it's different. To create, enable, or disable a virtual MFA device (vMFAd), or to start an MFA session, complex sequences of commands are required, followed by the need to painstakingly save the session token/credentials in the `~/.aws/credentials` file, and then either refer to that session profile by using the `--profile` switch on each `aws cli` command, or add/modify/delete various `aws_*` environment variables by cut-and-pasting at least the key id, the secret key, and the session token. Furthermore, the only way to know that the session has expired is that the `aws cli` commands start failing, thus making it difficult to plan long-running command execution, and potentially being confusing as to why such failures should occur.
+
+The `awscli-mfa.sh` and its companion scripts change all this by making use of the MFA sessions with `aws cli` a breeze! Let's first look at what each script does on the high level.
+
+### Overview
+
+These scripts provide significant interactive guidance as well as user-friendly failure information when something doesn't work as expected.
+
+The scripts have been tested in macOS (High Sierra with stock bash 3.2.x, and homebrew-installed bash 4.4.x) as well as with Linux (Ubuntu 16.04 with modern default bash 4.3.x). The only dependency is `aws cli` (and Python 2.6.5+ or Python 3.3+ for it), and the scripts will notify the user if `aws cli` is not present.
+
+* **awscli-mfa.sh** - Makes it easy to start MFA sessions with `aws cli`, and to switch between active sessions and base profiles. Multiple profiles are supported, but if only a single profile ("default") is in use, a simplified user interface is presented.
This is an interactive script since it prompts for the current MFA one time pass code from the Google Authenticator/Authy app, and as such it is an interactive script. The only command line argument it takes is `--debug` / `-d` which enables debug output. The script was originally written for macOS, but compatibility for Linux has been added.
When an MFA session is started with this script, it automatically records the initialization time of the session and names the MFA session with the `-mfasession` postfix.
For more details, read [my blog post](https://random.ac/cess/2017/10/29/easy-mfa-and-profile-switching-in-aws-cli/) about this script.
+
+* **enable-disable-vmfa-device.sh** - Makes it easy to enable/attach and disable/detach (as well as to delete) a virtual MFA device ("vMFAd"). Assumes that each IAM user can have one vMFAd configured at a time, and that the vMFAd is named the same as their IAM username (i.e. the serial number which is known as the ARN or "Amazon Resource Name" of the vMFAd is of the format `arn:aws:iam::{AWS_account_id}:mfa/{IAM_username}` when the IAM user ARN is of the format `arn:aws:iam::{AWS_account_id}:user/{IAM_username}`). Disabling a vMFAd requires usually an active MFA session with that profile. However, you can also use another profile that is authorized to detach a vMFAd. If you no longer have access to the vMFAd in your Google Authenticator or Authy app, you either need to have access to an AWS account which is authorized to detach vMFAd for other users and/or without an active MFA session. In the absence of such, contact the admin/ops with a request to delete the vMFAd off of your account so that you can create a new one.
As with `awscli-mfa.sh`, this script supports multiple configured profiles, but if only a single profile ("default") is in use, a simplified user interface is presented to either create/enable a vMFAd if none is present, or disable/deleted a vMFAd if one is active.
+
+* **mfastatus.sh** - Displays the currently active MFA sessions and their remaining activity period. Also indicates expired persistent (or in-environment) profiles with "EXPIRED" status.
+
+* **source-this-to-clear-AWS-envvars.sh** - A simple sourceable script that removes any AWS secrets/settings that may have been set in the local environment by the `awscli-mfa.sh` script. Source it, like so: `source ./source-this-to-clear-AWS-envvars.sh`, or set an alias, like so: `alias clearaws='source ~/awscli-mfa/source-this-to-clear-AWS-envvars.sh'`
+
+* **example-MFA-enforcement-policy.txt** - An example IAM policy to enforce an active MFA session to allow `aws cli` command execution. This policy has been carefully crafted to work with the above scripts, and it has been inspired by (but improved from) the example policies provided by [AWS](https://docs.aws.amazon.com/IAM/latest/UserGuide/tutorial_users-self-manage-mfa-and-creds.html) and [Trek10](https://www.trek10.com/blog/improving-the-aws-force-mfa-policy-for-IAM-users/) (both of those policies had problems which have been resolved in this example policy). Note that when a MFA is enabled on the command line using this script, it is also enabled for the web console login.
+
+### Usage (the long form)
+
+These scripts create a workflow to easily and quickly create/configure a virtual MFA device vMFAd for a profile, then start an MFA session, and then monitor the remaining session validity period for any of the active sessions. You can have multiple concurrent active MFA sessions and easily switch between them (and the base profiles where no MFA session is used/desired) by re-executing the `awscli-mfa.sh` script. Or, if you create 'persistent' sessions (it's the default when starting a new MFA session), you can always use the `--profile` switch with your `aws cli` command to temporarily select another active session or base profile without running `awscli-mfa.sh`. Here is how it works:
+
+First make sure you have `aws cli` installed. AWS has details for [Mac](https://docs.aws.amazon.com/cli/latest/userguide/cli-install-macos.html) and [Linux](https://docs.aws.amazon.com/cli/latest/userguide/awscli-install-linux.html).
+
+1. You have received a set of AWS credentials, so add them to your `~/.aws/credentials` file first. If that file doesn't exist yet, or if there are no credentials present, configure the default profile with `aws configure`. If you already have existing profiles in the `~/.aws/credentials` file, configure a named profile with:
+
+ `aws configure --profile "SomeDescriptiveProfileName"`
+.. and you will be prompted for the AWS Access Key ID, AWS Secret Access Key, Default Region name, and Default output format. An example (these are of course not valid, so enter your own :-)
+
+ AWS Access Key ID [None]: AKIAIL3VDLRPTXVU3ART
+ AWS Secret Access Key [None]: hlR98dzjwFKW3rZLNf32sdjRkelLPdrRh2H4hzn8
+ Default region name [None]: us-east-1
+ Default output format [None]: table
+
+2. Make sure you have Authy installed on your portable device. It is available for [Android](https://play.google.com/store/apps/details?id=com.authy.authy&hl=en_US) and [iOS](https://itunes.apple.com/us/app/authy/id494168017). Now execute `enable-disable-vmfa-device.sh`. If you have only one profile present and you don't have a vMFAd configured yet for it, the process will be like so (the in-line comments indicated with '///'). If something goes wrong with the vMFAd activation process, the script gives a hopefully clear/obvious guidance.
+
+ Executing this script as the AWS/IAM user 'mfa-test-user' (profile 'default').
+
+ Please wait.
+
+ You have one configured profile: default (IAM: mfa-test-user)
+ .. but it doesn't have a virtual MFA device attached/enabled.
+
+ Do you want to attach/enable a vMFAd? Y/N
+
+ ///
+ /// ANSWERED 'Y'
+ ///
+
+ Preparing to enable the vMFAd for the profile...
+
+ No available vMFAd found; creating new...
+
+ A new vMFAd has been created. Please scan
+ the QRCode with Authy to add the vMFAd on
+ your portable device.
+
+ NOTE: The QRCode file, "default vMFAd QRCode.png",
+ is on your DESKTOP!
+
+ /// OPENED THE QRCODE FILE MENTIONED ABOVE AND SCANNED IT IN AUTHY:
+ /// In Authy, select "Add Account" in the top right menu, then click
+ /// "Scan QR Code", and once scanned, give the profile a descriptive
+ /// name and click on "DONE"
+
+ Press 'x' once you have scanned the QRCode to proceed.
+
+ NOTE: Anyone who gains possession of the QRCode file
+ can initialize the vMFDd like you just did, so
+ optimally it should not be kept around.
+
+ Do you want to delete the QRCode securely? Y/N
+
+ /// ANSWERED 'Y'. DON'T KEEP THE QRCODE FILE AROUND
+ /// UNLESS YOU NEED TO INITIALIZE THE SAME vMFAd ON
+ /// ANOTHER DEVICE! NOTE THAT THE QRCODE FILE IS EQUAL
+ /// TO A PASSWORD AND SHOULD BE STORED SECURELY IF NOT
+ /// DELETED.
+
+ QRCode file deleted securely.
+
+ Enabling the newly created virtual MFA device:
+ arn:aws:iam::123456789123:mfa/mfa-test-user
+
+ Please enter two consecutively generated authcodes from your
+ GA/Authy app for this profile. Enter the two six-digit codes
+ separated by a space (e.g. 123456 456789), then press enter
+ to complete the process.
+
+ >>> 923558 212566
+
+ vMFAd successfully enabled for the profile 'default' (IAM user name 'mfa-test-user').
+
+ You can now use the 'awscli-mfa.sh' script to start an MFA session for this profile!
+
+ If you have more than one profile configured, or one or more active MFA sessions, you'll be presented with a menu (below). If you select a base profile you have the option to not enter an MFA pass code in which case the base profile is used rather than initiating an MFA session for it. If you select an existing active MFA profile (indicated with the `m` postfix), then the MFA code is not requested and just the envvar exports are copied on the clipboard for pasting on the command line to activate that profile:
+
+ Executing this script as the AWS/IAM user 'mfa-test-user' (profile 'default').
+
+ Please wait..
+
+ AVAILABLE AWS PROFILES:
+
+ 1: default (IAM: mfa-test-user; vMFAd enabled)
+ 1m: default MFA profile (07h:17m:17s remaining)
+
+ 2: profile OtherProfile (IAM: mfa-test-user; vMFAd enabled)
+
+ You can switch to a base profile to use it as-is, start an MFA session
+ for a profile if it is marked as "vMFAd enabled", or switch to an existing
+ active MFA session if any are available (indicated by the letter 'm' after
+ the profile ID, e.g. '1m'; NOTE: the expired MFA sessions are not shown).
+
+ SELECT A PROFILE BY THE ID:
+
+
+3. Now execute `awscli-mfa.sh` to start the first MFA session. The process for a single configured profile looks like this (again, the in-line comments indicated with '///'):
+
+ Executing this script as the AWS/IAM user 'mfa-test-user' (profile 'default').
+
+ Please wait.
+
+ You have one configured profile: default (IAM: mfa-test-user)
+ .. its vMFAd is enabled
+ .. but no active persistent MFA sessions exist
+
+ Do you want to:
+ 1: Start/renew an MFA session for the profile mentioned above?
+ 2: Use the above profile as-is (without MFA)?
+
+ ///
+ /// ANSWERED '1'
+ ///
+
+ Starting an MFA session..
+ SELECTED PROFILE: default
+
+ Enter the current MFA one time pass code for the profile 'default'
+ to start/renew an MFA session, or leave empty (just press [ENTER])
+ to use the selected profile without the MFA.
+
+ >>> 764257
+
+ Acquiring MFA session token for the profile: default...
+ MFA session token acquired.
+
+ Make this MFA session persistent? (Saves the session in /Users/ville/.aws/credentials
+ so that you can return to it during its validity period, 09h:00m:00s.)
+ Yes (default) - make peristent; No - only the envvars will be used [Y]/N
+
+ /// PRESSED 'ENTER' FOR THE DEFAULT 'Y'; THE MFA SESSION IS MADE PERSISTENT
+ /// BY SAVING IT IN `~/.aws/credentials` FILE WITH '{baseprofile}-mfasession'
+ /// PROFILE NAME. THIS MAKES IT POSSIBLE TO SWITCH BETWEEN THE ACTIVE MFA
+ /// SESSIONS AND BASE PROFILES, AND ALSO RETURN TO THE MFA SESSION AFTER
+ /// SYSTEM REBOOT WITHOUT REACQUIRING A MFA SESSION.
+
+ NOTE: Region had not been configured for the selected MFA profile;
+ it has been set to same as the parent profile ('us-east-1').
+ NOTE: Output format had not been configured for the selected MFA profile;
+ it has been set to same as the parent profile ('table').
+
+ /// THE SCRIPT AUTOMATICALLY SETS THE REGION AND THE DEFAULT OUTPUT
+ /// FORMAT IF THEY WEREN'T PREVIOUSLY SET. THE BASE PROFILE SETTINGS
+ /// ARE USED BY DEFAULT FOR ITS MFA SESSIONS. IF THE BASE PROFILE DOESN'T
+ /// HAVE THEM SET EITHER, THE DEFAULT SETTINGS ARE USED.
+
+ * * * PROFILE DETAILS * * *
+
+ MFA profile name: 'default-mfasession'
+
+ Region is set to: us-east-1
+ Output format is set to: table
+
+ Do you want to export the selected profile's secrets to the environment (for s3cmd, etc)? - Y/[N]
+
+ /// PRESSED 'ENTER' FOR THE DEFAULT 'N'. BY DEFAULT ONLY THE MFA PROFILE
+ /// REFERENCE IS EXPORTED TO THE ENVIRONMENT. IF YOU SELECT 'Y', THEN ALSO
+ /// THE `AWS_ACCESS_KEY_ID`, `AWS_SECRET_ACCESS_KEY`, AND `AWS_SESSION_TOKEN`
+ /// ARE EXPORTED. THIS MAY BE DESIRABLE IF YOU ARE USING AN APPLICATION SUCH
+ /// AS s3cmd WHICH READS THE ACCESS CREDENTIALS FROM THE ENVIRONMENT RATHER
+ /// THAN FROM THE `~/.aws/credentials` FILE.
+
+ *** It is imperative that the following environment variables are exported/unset
+ as specified below in order to activate your selection! The required
+ export/unset commands have already been copied on your clipboard!
+ Just paste on the command line with Command-v, then press [ENTER]
+ to complete the process!
+
+ export AWS_PROFILE="default-mfasession"
+ unset AWS_ACCESS_KEY_ID
+ unset AWS_SECRET_ACCESS_KEY
+ unset AWS_DEFAULT_REGION
+ unset AWS_DEFAULT_OUTPUT
+ unset AWS_SESSION_INIT_TIME
+ unset AWS_SESSION_DURATION
+ unset AWS_SESSION_TOKEN
+
+
+ *** Make sure to export/unset all the new values as instructed above to
+ make sure no conflicting profile/secrets remain in the envrionment!
+
+ *** You can temporarily override the profile set/selected in the environment
+ using the "--profile AWS_PROFILE_NAME" switch with awscli. For example:
+ aws sts get-caller-identity --profile default
+
+ *** To easily remove any all AWS profile settings and secrets information
+ from the environment, simply source the included script, like so:
+ source ./source-this-to-clear-AWS-envvars.sh
+
+ PASTE THE PROFILE ACTIVATION COMMAND FROM THE CLIPBOARD
+ ON THE COMMAND LINE NOW, AND PRESS ENTER! THEN YOU'RE DONE!
+
+ ~$ export AWS_PROFILE="default-mfasession"; unset AWS_ACCESS_KEY_ID; unset AWS_SECRET_ACCESS_KEY; unset AWS_SESSION_TOKEN; unset AWS_SESSION_INIT_TIME; unset AWS_SESSION_DURATION; unset AWS_DEFAULT_REGION; unset AWS_DEFAULT_OUTPUT
+
+ /// PASTED ON THE COMMAND LINE THE EXPORT COMMAND THAT THE SCRIPT PLACED
+ /// ON THE CLIPBOARD AND PRESSED ENTER TO EXPORT/CLEAR THE AWS_* ENVIRONMENT
+ /// VARIABLES TO ACTIVATE THIS NEWLY INITIALIZED MFA PROFILE.
+
+ TIP: If you use [**s3cmd**](http://s3tools.org/s3cmd), it's a good practice to not keep the AWS credentials in `~/.s3cfg`. Instead, use `awscli-mfa.sh` to select a profile, even if you want to use a non-MFA base profile. When using a base profile, simply leave the MFA one time pass code empty and press Enter. Then choose 'Yes' when asked if you want to export the selected profile's secrets to the environment (and paste then paste/enter in Terminal to export). That way `s3cmd` will pick up the credentials from the environment instead of its own configuration file. This also makes it easy to switch between the profiles when using `s3cmd`.
+
+ The Route53 utility [**cli53**](https://github.com/barnybug/cli53) honors the profile selector envvar (`AWS_PROFILE`); so for it you don't need to select "export secrets".
+
+4. Now you can execute `mfastatus.sh` to view the remaining activity period on the MFA session:
+
+ ENVIRONMENT
+ ===========
+
+ ENVVAR 'AWS_PROFILE' SELECTING A PERSISTENT MFA SESSION (as below): default-mfasession
+
+
+ PERSISTENT MFA SESSIONS (in /Users/ville/.aws/credentials)
+ ==========================================================
+
+ MFA SESSION IDENT: default-mfasession (IAM user: 'mfa-test-user')
+ MFA SESSION REMAINING TO EXPIRATION: 08h:13m:48s
+
+
+ NOTE: Execute 'awscli-mfa.sh' to renew/start a new MFA session,
+ or to select (switch to) an existing active MFA session.
+
+5. A sourceable `source-this-to-clear-AWS-envvars.sh` is provided to make it easy to clear out any any `AWS_*` envvars, like so: `source ./source-this-to-clear-AWS-envvars.sh`. This purges any secrets and/or references to persistent profiles from the local environment.
+
+6. If you want to detach/disable (and maybe delete) a vMFAd off of an account, you can run `enable-disable-vmfa-device.sh` script again. Below also a situation with more than one base profile is shown:
+
+ ~$ ./enable-disable-vmfa-device.sh
+
+ ** NOTE: THE FOLLOWING AWS_* ENVIRONMENT VARIABLES ARE CURRENTLY IN EFFECT:
+
+ AWS_PROFILE: default-mfasession
+
+ Executing this script as the AWS/IAM user 'mfa-test-user' (profile 'default-mfasession').
+
+ Please wait..
+
+ AWS PROFILES WITH NO ATTACHED/ENABLED VIRTUAL MFA DEVICE (vMFAd):
+ Select a profile to which you want to attach/enable a vMFAd.
+ A new vMFAd is created/initialized if one doesn't exist.
+
+ 1: OtherProfile (IAM: my-real-IAM-username)
+
+ AWS PROFILES WITH ACTIVE (ENABLED) VIRTUAL MFA DEVICE (vMFAd):
+ Select a profile whose vMFAd you want to detach/disable.
+ Once detached, you'll have the option to delete the vMFAd.
+ NOTE: A profile must have an active MFA session to disable!
+
+ 2: default (IAM: mfa-test-user)
+
+ SELECT A PROFILE BY THE NUMBER: 2
+
+ Preparing to disable the vMFAd for the profile...
+
+ vMFAd disabled/detached for the profile 'default'.
+
+ Do you want to DELETE the disabled/detached vMFAd? Y/N
+
+ /// SELECTED 'Y'
+
+ vMFAd deleted for the profile 'default'.
+
+ To set up a new vMFAd, run this script again.
+
+ Note: If configured on the AWS side, an automated process may delete the detached virtual MFA devices that have been left unattached for some period of time (but this script automatically creates a new vMFAd if none are found). When a vMFAd is deleted, the entry on GA/Authy becomes void.
Note: In order to disable/detach a vMFAd off of a profile that profile must have an active MFA session. If the script doesn't detect an MFA session, the following message is displayed:
+
+ Preparing to disable the vMFAd for the profile...
+
+ No active MFA session found for the profile 'OtherProfile'.
+
+ To disable/detach a vMFAd from the profile, you must have
+ an active MFA session established with it. Use the 'awscli-mfa.sh'
+ script to establish an MFA session for the profile first, then
+ run this script again.
+
+ If you do not have possession of the vMFAd for this profile
+ (in GA/Authy app), please request ops to disable the vMFAd
+ for your profile, or if you have admin credentials for AWS,
+ use them outside this script to disable the vMFAd for this
+ profile.
+
+### Session Activity Period
+
+Because the MFA session expiration time is encoded in the encrypted AWS session token, there is no way to retrieve the expiration time for a specific session from the AWS. To keep track of the remaining activity period, the following variables are used:
+
+* `MFA_SESSION_LENGTH_IN_SECONDS` - This __user-configurable__ variable is set on top of the `awscli-mfa.sh`, `enable-disable-vmfa-device.sh`, and `mfastatus.sh` scripts. It needs to equal to the maximum length for MFA sessions defined by your IAM policy in seconds (see the two `"aws:MultiFactorAuthAge": "32400"` entries in `example-MFA-enforcement-policy.txt`). If you decide on a different maximum session length than 9h (32400 seconds), make sure to adjust both your active IAM MFA enforcement policy and the above mentioned variable in the three scripts.
+
+* `mfasec` - An __optional, user-configurable__ proprietary variable may be defined in `~/.aws/config` for any base profile (i.e. any profile whose name doesn't end in `-mfasession`). It sets the profile-specific session length, and as such overrides the default `MFA_SESSION_LENGTH_IN_SECONDS`. This makes it possible for different AWS profiles (and thus often different AWS accounts) to have their MFA session enforcement policy be set to different maximum session lengths. If you're not an AWS admin, ask your DevOps/admin contact what the enforced MFA session lifetime is set to. There is no way to find out this value otherwise as it is an arbitrary number between 900 seconds (15 minutes) and 129000 seconds (36 hours) decided by the AWS account administrator. Note: The valid session length for the root (non-IAM) account is limited to 900-3600 seconds, but you should not use - and preferably delete - the access keys for the root/account as they are considered a security risk.
The optional `mfasec` value in `~/.aws/config` looks as follows (here the session length of the MFA sessions started for the `test-user` base profile are set to 21600 seconds, or 6 hours):
+
+ ```
+ [profile test-user]
+ region = us-east-1
+ output = table
+ mfasec = 21600
+ ```
+
+* `aws_session_init_time` - This __automatically configured__ proprietary variable is set in `~/.aws/credentials` file for the persistent MFA profiles (indicated by the `-mfasession` postfix in the profile name). It is a timestamp of the initialization time of the session in question. __This value is never adjusted by the user__, and it looks like this:
+
+ ```
+ [test-user-mfasession]
+ aws_session_init_time = 1522910812 <---
+ aws_access_key_id = XXXXXXXXXXXXXXXXXXXX
+ aws_secret_access_key = xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
+ aws_session_token = FQoDYXdzEHAaDENknHJokLPf40ffGCKwAQUGXOPjUl9m8j3q+ZbwyfRAUoQa8lMYy+ubhgKaYes5ZC+NuQGV98v5r1OEMABBYqAfCx2e+0wXBKicG/HetxrG1PP43242lNN1IyVxHbJLKjn9YM5m3MJTZjR7+BcZQfafugcdwzkgPD7yfKoDbqU8j5lCHWk0KkLPLIWFhi0nQPLoL1a4zDc8ibxXhezKJiWOrrmteTuRIK7jiZQB5CzjfQsQ0BI5mM8AOzwdY/LWKNOMl9YF
+ ```
+
+### Alternative Configuration Files
+
+These scripts recognize and honor custom configuration and credentials file locations set with `AWS_CONFIG_FILE` and `AWS_SHARED_CREDENTIALS_FILE` envvars, respectively. Only if the named/default profile such such files is not valid, the scripts let the user know, and then revert to the default files `~/.aws/config` and `~/.aws/credentials`.
+
+### Debugging
+
+Enable the debugging output temporarily by using a command line switch `-d` or `--debug`, or by uncommeting `DEBUG=true` on top of `awscli-mfa.sh` or `enable-disable-vmfa-device.sh` files. The debugging output displays the raw `aws cli` returns in `awscli-mfa.sh` and `enable-disable-vmfa-device.sh` files, so you'll be able to see any results/error messages as-is. Note that key ids, keys, or session tokens may be included in the debugging output!
diff --git a/awscli-mfa/awscli-mfa.sh b/awscli-mfa/awscli-mfa.sh
new file mode 100755
index 0000000..0faf55d
--- /dev/null
+++ b/awscli-mfa/awscli-mfa.sh
@@ -0,0 +1,1971 @@
+#!/usr/bin/env bash
+
+# todo: handle roles with MFA
+# todo: handle root account max session time @3600 & warn if present
+# todo: handle secondary role max session time @3600 & warn
+
+# NOTE: Debugging mode prints the secrets on the screen!
+DEBUG="false"
+
+# enable debugging with '-d' or '--debug' command line argument..
+[[ "$1" == "-d" || "$1" == "--debug" ]] && DEBUG="true"
+# .. or by uncommenting the line below:
+#DEBUG="true"
+
+# Set the global session length in seconds below; note that
+# this only sets the client-side duration for the MFA session
+# token! The maximum length of a valid session is enforced by
+# the IAM policy, and is unaffected by this value (if this
+# duration is set to a longer value than the enforcing value
+# in the IAM policy, the token will stop working before it
+# expires on the client side). Matching this value with the
+# enforcing IAM policy provides you with accurate detail
+# about how long a token will continue to be valid.
+#
+# THIS VALUE CAN BE OPTIONALLY OVERRIDDEN PER EACH PROFILE
+# BY ADDING A "mfasec" ENTRY FOR THE PROFILE IN ~/.aws/config
+#
+# The valid session lengths are from 900 seconds (15 minutes)
+# to 129600 seconds (36 hours); currently set (below) to
+# 32400 seconds, or 9 hours.
+MFA_SESSION_LENGTH_IN_SECONDS=32400
+
+# Define the standard locations for the AWS credentials and
+# config files; these can be statically overridden with
+# AWS_SHARED_CREDENTIALS_FILE and AWS_CONFIG_FILE envvars
+# (this script will override these envvars only if the
+# "[default]" profile in the defined custom file(s) is
+# defunct, thus reverting to the below default locations).
+CONFFILE=~/.aws/config
+CREDFILE=~/.aws/credentials
+
+# COLOR DEFINITIONS ==========================================================
+
+# Reset
+Color_Off='\033[0m' # Text Reset
+
+# Regular Colors
+Black='\033[0;30m' # Black
+Red='\033[0;31m' # Red
+Green='\033[0;32m' # Green
+Yellow='\033[0;33m' # Yellow
+Blue='\033[0;34m' # Blue
+Purple='\033[0;35m' # Purple
+Cyan='\033[0;36m' # Cyan
+White='\033[0;37m' # White
+
+# Bold
+BBlack='\033[1;30m' # Black
+BRed='\033[1;31m' # Red
+BGreen='\033[1;32m' # Green
+BYellow='\033[1;33m' # Yellow
+BBlue='\033[1;34m' # Blue
+BPurple='\033[1;35m' # Purple
+BCyan='\033[1;36m' # Cyan
+BWhite='\033[1;37m' # White
+
+# Underline
+UBlack='\033[4;30m' # Black
+URed='\033[4;31m' # Red
+UGreen='\033[4;32m' # Green
+UYellow='\033[4;33m' # Yellow
+UBlue='\033[4;34m' # Blue
+UPurple='\033[4;35m' # Purple
+UCyan='\033[4;36m' # Cyan
+UWhite='\033[4;37m' # White
+
+# Background
+On_Black='\033[40m' # Black
+On_Red='\033[41m' # Red
+On_Green='\033[42m' # Green
+On_Yellow='\033[43m' # Yellow
+On_Blue='\033[44m' # Blue
+On_Purple='\033[45m' # Purple
+On_Cyan='\033[46m' # Cyan
+On_White='\033[47m' # White
+
+# High Intensity
+IBlack='\033[0;90m' # Black
+IRed='\033[0;91m' # Red
+IGreen='\033[0;92m' # Green
+IYellow='\033[0;93m' # Yellow
+IBlue='\033[0;94m' # Blue
+IPurple='\033[0;95m' # Purple
+ICyan='\033[0;96m' # Cyan
+IWhite='\033[0;97m' # White
+
+# Bold High Intensity
+BIBlack='\033[1;90m' # Black
+BIRed='\033[1;91m' # Red
+BIGreen='\033[1;92m' # Green
+BIYellow='\033[1;93m' # Yellow
+BIBlue='\033[1;94m' # Blue
+BIPurple='\033[1;95m' # Purple
+BICyan='\033[1;96m' # Cyan
+BIWhite='\033[1;97m' # White
+
+# High Intensity backgrounds
+On_IBlack='\033[0;100m' # Black
+On_IRed='\033[0;101m' # Red
+On_IGreen='\033[0;102m' # Green
+On_DGreen='\033[48;5;28m' # Dark Green
+On_IYellow='\033[0;103m' # Yellow
+On_IBlue='\033[0;104m' # Blue
+On_IPurple='\033[0;105m' # Purple
+On_ICyan='\033[0;106m' # Cyan
+On_IWhite='\033[0;107m' # White
+
+# DEBUG MODE WARNING & BASH VERSION ==========================================
+
+if [[ "$DEBUG" == "true" ]]; then
+ echo -e "\\n${BIWhite}${On_Red} DEBUG MODE ACTIVE ${Color_Off}\\n\\n${BIRed}${On_Black}NOTE: Debug output may include secrets!!!${Color_Off}\\n\\n"
+ echo -e "Using bash version $BASH_VERSION\\n\\n"
+fi
+
+# FUNCTIONS ==================================================================
+
+# `exists` for commands
+exists() {
+ command -v "$1" >/dev/null 2>&1
+}
+
+# precheck envvars for existing/stale session definitions
+checkEnvSession() {
+ # $1 is the check type
+
+ local this_time
+ this_time=$(date +%s)
+
+ # COLLECT AWS_SESSION DATA FROM THE ENVIRONMENT
+ PRECHECK_AWS_PROFILE=$(env | grep AWS_PROFILE)
+ [[ "$PRECHECK_AWS_PROFILE" =~ ^AWS_PROFILE[[:space:]]*=[[:space:]]*(.*)$ ]] &&
+ PRECHECK_AWS_PROFILE="${BASH_REMATCH[1]}"
+
+ PRECHECK_AWS_ACCESS_KEY_ID=$(env | grep AWS_ACCESS_KEY_ID)
+ [[ "$PRECHECK_AWS_ACCESS_KEY_ID" =~ ^AWS_ACCESS_KEY_ID[[:space:]]*=[[:space:]]*(.*)$ ]] &&
+ PRECHECK_AWS_ACCESS_KEY_ID="${BASH_REMATCH[1]}"
+
+ PRECHECK_AWS_SECRET_ACCESS_KEY=$(env | grep AWS_SECRET_ACCESS_KEY)
+ [[ "$PRECHECK_AWS_SECRET_ACCESS_KEY" =~ ^AWS_SECRET_ACCESS_KEY[[:space:]]*=[[:space:]]*(.*)$ ]] &&
+ PRECHECK_AWS_SECRET_ACCESS_KEY="[REDACTED]"
+
+ PRECHECK_AWS_SESSION_TOKEN=$(env | grep AWS_SESSION_TOKEN)
+ [[ "$PRECHECK_AWS_SESSION_TOKEN" =~ ^AWS_SESSION_TOKEN[[:space:]]*=[[:space:]]*(.*)$ ]] &&
+ PRECHECK_AWS_SESSION_TOKEN="[REDACTED]"
+
+ PRECHECK_AWS_SESSION_INIT_TIME=$(env | grep AWS_SESSION_INIT_TIME)
+ [[ "$PRECHECK_AWS_SESSION_INIT_TIME" =~ ^AWS_SESSION_INIT_TIME[[:space:]]*=[[:space:]]*(.*)$ ]] &&
+ PRECHECK_AWS_SESSION_INIT_TIME="${BASH_REMATCH[1]}"
+
+ PRECHECK_AWS_SESSION_DURATION=$(env | grep AWS_SESSION_DURATION)
+ [[ "$PRECHECK_AWS_SESSION_DURATION" =~ ^AWS_SESSION_DURATION[[:space:]]*=[[:space:]]*(.*)$ ]] &&
+ PRECHECK_AWS_SESSION_DURATION="${BASH_REMATCH[1]}"
+
+ PRECHECK_AWS_DEFAULT_REGION=$(env | grep AWS_DEFAULT_REGION)
+ [[ "$PRECHECK_AWS_DEFAULT_REGION" =~ ^AWS_DEFAULT_REGION[[:space:]]*=[[:space:]]*(.*)$ ]] &&
+ PRECHECK_AWS_DEFAULT_REGION="${BASH_REMATCH[1]}"
+
+ PRECHECK_AWS_DEFAULT_OUTPUT=$(env | grep AWS_DEFAULT_OUTPUT)
+ [[ "$PRECHECK_AWS_DEFAULT_OUTPUT" =~ ^AWS_DEFAULT_OUTPUT[[:space:]]*=[[:space:]]*(.*)$ ]] &&
+ PRECHECK_AWS_DEFAULT_OUTPUT="${BASH_REMATCH[1]}"
+
+ PRECHECK_AWS_CA_BUNDLE=$(env | grep AWS_CA_BUNDLE)
+ [[ "$PRECHECK_AWS_CA_BUNDLE" =~ ^AWS_CA_BUNDLE[[:space:]]*=[[:space:]]*(.*)$ ]] &&
+ PRECHECK_AWS_CA_BUNDLE="${BASH_REMATCH[1]}"
+
+ PRECHECK_AWS_SHARED_CREDENTIALS_FILE=$(env | grep AWS_SHARED_CREDENTIALS_FILE)
+ [[ "$PRECHECK_AWS_SHARED_CREDENTIALS_FILE" =~ ^AWS_SHARED_CREDENTIALS_FILE[[:space:]]*=[[:space:]]*(.*)$ ]] &&
+ PRECHECK_AWS_SHARED_CREDENTIALS_FILE="${BASH_REMATCH[1]}"
+
+ PRECHECK_AWS_CONFIG_FILE=$(env | grep AWS_CONFIG_FILE)
+ [[ "$PRECHECK_AWS_CONFIG_FILE" =~ ^AWS_CONFIG_FILE[[:space:]]*=[[:space:]]*(.*)$ ]] &&
+ PRECHECK_AWS_CONFIG_FILE="${BASH_REMATCH[1]}"
+
+ # AWS_PROFILE must be empty or refer to *any* profile in ~/.aws/{credentials|config}
+ # (Even if all the values are overridden by AWS_* envvars they won't work if the
+ # AWS_PROFILE is set to an unknown value!)
+ if [[ "$PRECHECK_AWS_PROFILE" != "" ]]; then
+
+ idxLookup profiles_idx creds_ident[@] "$PRECHECK_AWS_PROFILE"
+ idxLookup confs_idx confs_ident[@] "$PRECHECK_AWS_PROFILE"
+
+ if [[ "$profiles_idx" == "" ]] && [[ "$confs_idx" == "" ]]; then
+
+ # AWS_PROFILE ident is not recognized;
+ # cannot continue unless it's changed!
+ continue_maybe "invalid"
+ fi
+ fi
+
+ # makes sure that the MFA session has not expired (whether it's
+ # defined in the environment or in ~/.aws/credentials).
+ #
+ # First checking the envvars
+ if [[ "$PRECHECK_AWS_SESSION_TOKEN" != "" ]] &&
+ [[ "$PRECHECK_AWS_SESSION_INIT_TIME" != "" ]] &&
+ [[ "$PRECHECK_AWS_SESSION_DURATION" != "" ]]; then
+ # this is a MFA profile in the environment;
+ # AWS_PROFILE is either empty or valid
+
+ getRemaining _ret "$PRECHECK_AWS_SESSION_INIT_TIME" "$PRECHECK_AWS_SESSION_DURATION"
+ [[ "${_ret}" -eq 0 ]] && continue_maybe "expired"
+
+ elif [[ "$PRECHECK_AWS_PROFILE" =~ -mfasession$ ]] &&
+ [[ "$profiles_idx" != "" ]]; then
+ # AWS_PROFILE is set (and valid, and refers to a persistent mfasession)
+ # but TOKEN, INIT_TIME, and/or DURATION are not, so this is
+ # likely a select of a named profile
+
+ # find the selected persistent MFA profile's init time if one exists
+ profile_time=${creds_aws_mfasession_init_time[$profiles_idx]}
+
+ # if the duration for the current profile is not set
+ # (as is usually the case with the mfaprofiles), use
+ # the parent/base profile's duration
+ if [[ "$profile_time" != "" ]]; then
+ getDuration parent_duration "$PRECHECK_AWS_PROFILE"
+ getRemaining _ret "$profile_time" "$parent_duration"
+ [[ "${_ret}" -eq 0 ]] && continue_maybe "expired"
+ fi
+ fi
+ # empty AWS_PROFILE + no in-env MFA session should flow through
+
+ # detect and print informative notice of
+ # effective AWS envvars
+ if [[ "${AWS_PROFILE}" != "" ]] ||
+ [[ "${AWS_ACCESS_KEY_ID}" != "" ]] ||
+ [[ "${AWS_SECRET_ACCESS_KEY}" != "" ]] ||
+ [[ "${AWS_SESSION_TOKEN}" != "" ]] ||
+ [[ "${AWS_SESSION_INIT_TIME}" != "" ]] ||
+ [[ "${AWS_SESSION_DURATION}" != "" ]] ||
+ [[ "${AWS_DEFAULT_REGION}" != "" ]] ||
+ [[ "${AWS_DEFAULT_OUTPUT}" != "" ]] ||
+ [[ "${AWS_CA_BUNDLE}" != "" ]] ||
+ [[ "${AWS_SHARED_CREDENTIALS_FILE}" != "" ]] ||
+ [[ "${AWS_CONFIG_FILE}" != "" ]]; then
+
+ echo
+ echo "NOTE: THE FOLLOWING AWS_* ENVIRONMENT VARIABLES ARE CURRENTLY IN EFFECT:"
+ echo
+ if [[ "$PRECHECK_AWS_PROFILE" != "$AWS_PROFILE" ]]; then
+ env_notice=" (overridden to 'default')"
+ else
+ env_notice=""
+ fi
+ [[ "$PRECHECK_AWS_PROFILE" != "" ]] && echo " AWS_PROFILE: ${PRECHECK_AWS_PROFILE}${env_notice}"
+ [[ "$PRECHECK_AWS_ACCESS_KEY_ID" != "" ]] && echo " AWS_ACCESS_KEY_ID: $PRECHECK_AWS_ACCESS_KEY_ID"
+ [[ "$PRECHECK_AWS_SECRET_ACCESS_KEY" != "" ]] && echo " AWS_SECRET_ACCESS_KEY: $PRECHECK_AWS_SECRET_ACCESS_KEY"
+ [[ "$PRECHECK_AWS_SESSION_TOKEN" != "" ]] && echo " AWS_SESSION_TOKEN: $PRECHECK_AWS_SESSION_TOKEN"
+ [[ "$PRECHECK_AWS_SESSION_INIT_TIME" != "" ]] && echo " AWS_SESSION_INIT_TIME: $PRECHECK_AWS_SESSION_INIT_TIME"
+ [[ "$PRECHECK_AWS_SESSION_DURATION" != "" ]] && echo " AWS_SESSION_DURATION: $PRECHECK_AWS_SESSION_DURATION"
+ [[ "$PRECHECK_AWS_DEFAULT_REGION" != "" ]] && echo " AWS_DEFAULT_REGION: $PRECHECK_AWS_DEFAULT_REGION"
+ [[ "$PRECHECK_AWS_DEFAULT_OUTPUT" != "" ]] && echo " AWS_DEFAULT_OUTPUT: $PRECHECK_AWS_DEFAULT_OUTPUT"
+ [[ "$PRECHECK_AWS_CA_BUNDLE" != "" ]] && echo " AWS_CA_BUNDLE: $PRECHECK_AWS_CA_BUNDLE"
+ [[ "$PRECHECK_AWS_SHARED_CREDENTIALS_FILE" != "" ]] && echo " AWS_SHARED_CREDENTIALS_FILE: $PRECHECK_AWS_SHARED_CREDENTIALS_FILE"
+ [[ "$PRECHECK_AWS_CONFIG_FILE" != "" ]] && echo " AWS_CONFIG_FILE: $PRECHECK_AWS_CONFIG_FILE"
+ echo
+ fi
+}
+
+# workaround function for lack of
+# macOS bash's assoc arrays
+idxLookup() {
+ # $1 is _ret (returns the index)
+ # $2 is the array
+ # $3 is the item to be looked up in the array
+
+ declare -a arr=("${!2}")
+ local key=$3
+ local result=""
+ local maxIndex
+
+ maxIndex=${#arr[@]}
+ ((maxIndex--))
+
+ for (( i=0; i<=maxIndex; i++ ))
+ do
+ if [[ "${arr[$i]}" == "$key" ]]; then
+ result=$i
+ break
+ fi
+ done
+
+ eval "$1=$result"
+}
+
+# save the MFA session initialization timestamp
+# in the session profile in ~/.aws/credentials
+addInitTime() {
+ # $1 is the profile (ident)
+
+ this_ident=$1
+ this_time=$(date +%s)
+
+ # find the selected profile's existing
+ # init time entry if one exists
+ getInitTime _ret "$this_ident"
+ profile_time=${_ret}
+
+ # update/add session init time
+ if [[ $profile_time != "" ]]; then
+ # time entry exists for the profile, update
+
+ if [[ "$OS" == "macOS" ]]; then
+ sed -i '' -e "s/${profile_time}/${this_time}/g" "$CREDFILE"
+ else
+ sed -i -e "s/${profile_time}/${this_time}/g" "$CREDFILE"
+ fi
+ else
+ # no time entry exists for the profile;
+ # add on a new line after the header "[${this_ident}]"
+ replace_me="\\[${this_ident}\\]"
+ DATA="[${this_ident}]\\naws_session_init_time = ${this_time}"
+ echo "$(awk -v var="${DATA//$'\n'/\\n}" '{sub(/'${replace_me}'/,var)}1' "${CREDFILE}")" > "${CREDFILE}"
+ fi
+
+ # update the selected profile's existing
+ # init time entry in this script
+ idxLookup idx creds_ident[@] "$this_ident"
+ creds_aws_mfasession_init_time[$idx]=$this_time
+}
+
+# return the MFA session init time for the given profile
+getInitTime() {
+ # $1 is _ret
+ # $2 is the profile ident
+
+ local this_ident=$2
+ local profile_time
+
+ # find the profile's init time entry if one exists
+ idxLookup idx creds_ident[@] "$this_ident"
+ profile_time=${creds_aws_mfasession_init_time[$idx]}
+
+ eval "$1=${profile_time}"
+}
+
+getDuration() {
+ # $1 is _ret
+ # $2 is the profile ident
+
+ local this_profile_ident="$2"
+ local this_duration
+
+ # use parent profile ident if this is an MFA session
+ [[ "$this_profile_ident" =~ ^(.*)-mfasession$ ]] &&
+ this_profile_ident="${BASH_REMATCH[1]}"
+
+ # look up possible custom duration for the parent profile
+ idxLookup idx confs_ident[@] "$this_profile_ident"
+
+ [[ $idx != "" && "${confs_mfasec[$idx]}" != "" ]] &&
+ this_duration=${confs_mfasec[$idx]} ||
+ this_duration=$MFA_SESSION_LENGTH_IN_SECONDS
+
+ eval "$1=${this_duration}"
+}
+
+# Returns remaining seconds for the given timestamp;
+# if the custom duration is not provided, the global
+# duration setting is used). In the result
+# 0 indicates expired, -1 indicates NaN input
+getRemaining() {
+ # $1 is _ret
+ # $2 is the timestamp
+ # $3 is the duration
+
+ local timestamp=$2
+ local duration=$3
+ local this_time
+ this_time=$(date +%s)
+ local remaining=0
+
+ [[ "${duration}" == "" ]] &&
+ duration=$MFA_SESSION_LENGTH_IN_SECONDS
+
+ if [ ! -z "${timestamp##*[!0-9]*}" ]; then
+ ((session_end=timestamp+duration))
+ if [[ $session_end -gt $this_time ]]; then
+ ((remaining=session_end-this_time))
+ else
+ remaining=0
+ fi
+ else
+ remaining=-1
+ fi
+ eval "$1=${remaining}"
+}
+
+# return printable output for given 'remaining' timestamp
+# (must be pre-incremented with profile duration,
+# such as getRemaining() output)
+getPrintableTimeRemaining() {
+ # $1 is _ret
+ # $2 is the timestamp
+
+ local timestamp=$2
+
+ case $timestamp in
+ -1)
+ response="N/A"
+ ;;
+ 0)
+ response="EXPIRED"
+ ;;
+ *)
+ response=$(printf '%02dh:%02dm:%02ds' $((timestamp/3600)) $((timestamp%3600/60)) $((timestamp%60)))
+ ;;
+ esac
+ eval "$1=${response}"
+}
+
+#BEGIN NONE OF THIS MAY BE NEEDED...
+does_valid_default_exist() {
+ # $1 is _ret
+
+ default_profile_arn="$(aws --profile default sts get-caller-identity --query 'Arn' --output text 2>&1)"
+
+ [[ "$DEBUG" == "true" ]] && echo -e "\\n${Cyan}${On_Black}result for: 'aws --profile default sts get-caller-identity --query 'Arn' --output text':\\n${ICyan}${default_profile_arn}${Color_Off}"
+
+ if [[ "$default_profile_arn" =~ ^arn:aws:iam:: ]] &&
+ [[ ! "$default_profile_arn" =~ 'error occurred' ]]; then
+
+ [[ "$DEBUG" == "true" ]] && echo -e "\\n${Cyan}${On_Black}The default profile exists and is valid.${Color_Off}"
+ response="true"
+ else
+ [[ "$DEBUG" == "true" ]] && echo -e "\\n${Cyan}${On_Black}The default profile not present or invalid.${Color_Off}"
+ response="false"
+ fi
+
+ eval "$1=${response}"
+}
+
+already_failed="false"
+# here are my args, so..
+continue_maybe() {
+ # $1 is "invalid" or "expired"
+
+ local failtype=$1
+
+ if [[ "$already_failed" == "false" ]]; then
+
+ if [[ "${failtype}" == "expired" ]]; then
+ echo -e "\\n${BIRed}${On_Black}NOTE: THE MFA SESSION SELECTED/CONFIGURED IN THE ENVIRONMENT HAS EXPIRED.${Color_Off}"
+ else
+ echo -e "\\n${BIRed}${On_Black}NOTE: THE AWS PROFILE SELECTED/CONFIGURED IN THE ENVIRONMENT IS INVALID.${Color_Off}"
+ fi
+
+#todo: remove below altogether?
+if [[ "true" == "false" ]]; then
+ read -s -p "$(echo -e "${BIWhite}${On_Black}Do you want to continue with the default profile?${Color_Off} - ${BIWhite}${On_Black}[Y]${Color_Off}/N ")" -n 1 -r
+ if [[ $REPLY =~ ^[Yy]$ ]] ||
+ [[ $REPLY == "" ]]; then
+
+ already_failed="true"
+
+ # If the default profile is already selected
+ # and the profile was still defunct (since
+ # we ended up here), make sure non-standard
+ # config/credentials files are not used
+ if [[ "$AWS_PROFILE" == "" ]] ||
+ [[ "$AWS_PROFILE" == "default" ]]; then
+
+ unset AWS_SHARED_CREDENTIALS_FILE
+ unset AWS_CONFIG_FILE
+
+ custom_configfiles_reset="true"
+ fi
+
+ unset AWS_PROFILE
+ unset AWS_ACCESS_KEY_ID
+ unset AWS_SECRET_ACCESS_KEY
+ unset AWS_SESSION_TOKEN
+ unset AWS_SESSION_INIT_TIME
+ unset AWS_SESSION_DURATION
+ unset AWS_DEFAULT_REGION
+ unset AWS_DEFAULT_OUTPUT
+ unset AWS_CA_BUNDLE
+
+ # override envvar for all the subshell commands
+ export AWS_PROFILE=default
+ echo
+ else
+ echo -e "\\n\\nExecute \"source ./source-this-to-clear-AWS-envvars.sh\", and try again to proceed.\\n"
+ exit 1
+ fi
+fi
+
+ fi
+}
+# END NONE OF THIS MAY BE NEEDED
+
+checkAWSErrors() {
+ # $1 is exit_on_error (true/false)
+ # $2 is the AWS return (may be good or bad)
+ # $3 is the 'default' keyword if present
+ # $4 is the custom message if present;
+ # only used when $3 is positively present
+ # (such as at MFA token request)
+
+ local exit_on_error=$1
+ local aws_raw_return=$2
+ local profile_in_use
+ local custom_error
+ [[ "$3" == "" ]] && profile_in_use="selected" || profile_in_use="$3"
+ [[ "$4" == "" ]] && custom_error="" || custom_error="${4}\\n"
+
+ local is_error="false"
+ if [[ "$aws_raw_return" =~ 'InvalidClientTokenId' ]]; then
+ echo -en "\\n${BIRed}${On_Black}${custom_error}The AWS Access Key ID does not exist!${Red}\\nCheck the ${profile_in_use} profile configuration including any 'AWS_*' environment variables.${Color_Off}\\n"
+ is_error="true"
+ elif [[ "$aws_raw_return" =~ 'SignatureDoesNotMatch' ]]; then
+ echo -en "\\n${BIRed}${On_Black}${custom_error}The Secret Access Key does not match the Access Key ID!${Red}\\nCheck the ${profile_in_use} profile configuration including any 'AWS_*' environment variables.${Color_Off}\\n"
+ is_error="true"
+ elif [[ "$aws_raw_return" =~ 'IncompleteSignature' ]]; then
+ echo -en "\\n${BIRed}${On_Black}${custom_error}Incomplete signature!${Red}\\nCheck the Secret Access Key of the ${profile_in_use} for typos/completeness (including any 'AWS_*' environment variables).${Color_Off}\\n"
+ is_error="true"
+ elif [[ "$aws_raw_return" =~ 'MissingAuthenticationToken' ]]; then
+ echo -en "\\n${BIRed}${On_Black}${custom_error}The Secret Access Key is not present!${Red}\\nCheck the ${profile_in_use} profile configuration (including any 'AWS_*' environment variables).${Color_Off}\\n"
+ is_error="true"
+ elif [[ "$aws_raw_return" =~ 'AccessDeniedException' ]]; then
+ echo -en "\\n${BIRed}${On_Black}${custom_error}Access denied!${Red}\\nThe effective MFA IAM policy may be too restrictive.${Color_Off}\\n"
+ is_error="true"
+ elif [[ "$aws_raw_return" =~ 'AuthFailure' ]]; then
+ echo -en "\\n${BIRed}${On_Black}${custom_error}Authentication failure!${Red}\\nCheck the credentials for the ${profile_in_use} profile (including any 'AWS_*' environment variables).${Color_Off}\\n"
+ is_error="true"
+ elif [[ "$aws_raw_return" =~ 'ServiceUnavailable' ]]; then
+ echo -en "\\n${BIRed}${On_Black}${custom_error}Service unavailable!${Red}\\nThis is likely a temporary problem with AWS; wait for a moment and try again.${Color_Off}\\n"
+ is_error="true"
+ elif [[ "$aws_raw_return" =~ 'ThrottlingException' ]]; then
+ echo -en "\\n${BIRed}${On_Black}${custom_error}Too many requests in too short amount of time!${Red}\\nWait for a few moments and try again.${Color_Off}\\n"
+ is_error="true"
+ elif [[ "$aws_raw_return" =~ 'InvalidAction' ]] ||
+ [[ "$aws_raw_return" =~ 'InvalidQueryParameter' ]] ||
+ [[ "$aws_raw_return" =~ 'MalformedQueryString' ]] ||
+ [[ "$aws_raw_return" =~ 'MissingAction' ]] ||
+ [[ "$aws_raw_return" =~ 'ValidationError' ]] ||
+ [[ "$aws_raw_return" =~ 'MissingParameter' ]] ||
+ [[ "$aws_raw_return" =~ 'InvalidParameterValue' ]]; then
+
+ echo -en "\\n${BIRed}${On_Black}${custom_error}AWS did not understand the request.${Red}\\nThis should never occur with this script. Maybe there was a glitch in\\nthe matrix (maybe the AWS API changed)?\\nRun the script with the '--debug' switch to see the exact error.${Color_Off}\\n"
+ is_error="true"
+ elif [[ "$aws_raw_return" =~ 'InternalFailure' ]]; then
+ echo -en "\\n${BIRed}${On_Black}${custom_error}An unspecified error occurred!${Red}\\n\"Internal Server Error 500\". Sorry I don't have more detail.${Color_Off}\\n"
+ is_error="true"
+ elif [[ "$aws_raw_return" =~ 'error occurred' ]]; then
+ echo -e "${BIRed}${On_Black}${custom_error}An unspecified error occurred!${Red}\\nCheck the ${profile_in_use} profile (including any 'AWS_*' environment variables).\\nRun the script with the '--debug' switch to see the exact error.${Color_Off}\\n"
+ is_error="true"
+ fi
+
+ # do not exit on profile ingest loop
+ [[ "$is_error" == "true" && "$exit_on_error" == "true" ]] && exit 1
+}
+
+getAccountAlias() {
+ # $1 is _ret (returns the index)
+ # $2 is the profile_ident
+
+ local local_profile_ident="$2"
+
+ if [[ "$local_profile_ident" == "" ]]; then
+ # no input, return blank result right away
+ result=""
+ eval "$1=$result"
+ fi
+
+ # get the account alias (if any) for the user/profile
+ account_alias_result="$(aws --profile "$local_profile_ident" iam list-account-aliases --output text --query 'AccountAliases' 2>&1)"
+
+ [[ "$DEBUG" == "true" ]] && echo -e "\\n\
+${Cyan}${On_Black}result for: 'aws --profile \"$local_profile_ident\" iam list-account-aliases --query 'AccountAliases' --output text':\\n\
+${ICyan}${account_alias_result}${Color_Off}\\n\\n"
+
+ if [[ "$account_alias_result" =~ 'error occurred' ]]; then
+ # no access to list account aliases for this profile or other error
+ result=""
+ else
+ result="$account_alias_result"
+ fi
+
+ eval "$1=$result"
+}
+
+## PREREQUISITES CHECK
+
+#todo: add awscli *version* check
+
+# is AWS CLI installed?
+if ! exists aws ; then
+ printf "\\n******************************************************************************************************************************\\n\
+This script requires the AWS CLI. See the details here: http://docs.aws.amazon.com/cli/latest/userguide/cli-install-macos.html\\n\
+******************************************************************************************************************************\\n\\n"
+ exit 1
+fi
+
+filexit="false"
+# check for ~/.aws directory
+# if the custom config defs aren't in effect
+if ( [[ "$AWS_CONFIG_FILE" == "" ]] ||
+ [[ "$AWS_SHARED_CREDENTIALS_FILE" == "" ]] ) &&
+ [ ! -d ~/.aws ]; then
+
+ echo
+ echo -e "${BIRed}${On_Black}\
+AWSCLI configuration directory '~/.aws' is not present.${Color_Off}\\n\
+Make sure it exists, and that you have at least one profile configured\\n\
+using the 'config' and/or 'credentials' files within that directory."
+ filexit="true"
+fi
+
+# SUPPORT CUSTOM CONFIG FILE SET WITH ENVVAR
+if [[ "$AWS_CONFIG_FILE" != "" ]] &&
+ [ -f "$AWS_CONFIG_FILE" ]; then
+
+ active_config_file=$AWS_CONFIG_FILE
+ echo
+ echo -e "${BIWhite}${On_Black}\
+NOTE: A custom configuration file defined with AWS_CONFIG_FILE envvar in effect: '$AWS_CONFIG_FILE'${Color_Off}"
+
+elif [[ "$AWS_CONFIG_FILE" != "" ]] &&
+ [ ! -f "$AWS_CONFIG_FILE" ]; then
+
+ echo
+ echo -e "${BIRed}${On_Black}\
+The custom config file defined with AWS_CONFIG_FILE envvar,\\n\
+'$AWS_CONFIG_FILE', is not present.${Color_Off}\\n\
+Make sure it is present or purge the envvar.\\n\
+See https://docs.aws.amazon.com/cli/latest/userguide/cli-config-files.html\\n\
+and https://docs.aws.amazon.com/cli/latest/topic/config-vars.html\\n\
+for the details on how to set them up."
+ filexit="true"
+
+elif [ -f "$CONFFILE" ]; then
+ active_config_file="$CONFFILE"
+else
+ echo
+ echo -e "${BIRed}${On_Black}\
+AWSCLI configuration file '$CONFFILE' was not found.${Color_Off}\\n\
+Make sure it and '$CREDFILE' files exist.\\n\
+See https://docs.aws.amazon.com/cli/latest/userguide/cli-config-files.html\\n\
+and https://docs.aws.amazon.com/cli/latest/topic/config-vars.html\\n\
+for the details on how to set them up."
+ filexit="true"
+fi
+
+# SUPPORT CUSTOM CREDENTIALS FILE SET WITH ENVVAR
+if [[ "$AWS_SHARED_CREDENTIALS_FILE" != "" ]] &&
+ [ -f "$AWS_SHARED_CREDENTIALS_FILE" ]; then
+
+ active_credentials_file="$AWS_SHARED_CREDENTIALS_FILE"
+ echo
+ echo -e "${BIWhite}${On_Black}\
+NOTE: A custom credentials file defined with AWS_SHARED_CREDENTIALS_FILE envvar in effect: '$AWS_SHARED_CREDENTIALS_FILE'${Color_Off}"
+
+elif [[ "$AWS_SHARED_CREDENTIALS_FILE" != "" ]] &&
+ [ ! -f "$AWS_SHARED_CREDENTIALS_FILE" ]; then
+
+ echo
+ echo -e "${BIRed}${On_Black}\
+The custom credentials file defined with AWS_SHARED_CREDENTIALS_FILE envvar,\\n\
+'$AWS_SHARED_CREDENTIALS_FILE', is not present.${Color_Off}\\n\
+Make sure it is present, or purge the envvar.\\n\
+See https://docs.aws.amazon.com/cli/latest/userguide/cli-config-files.html\\n\
+and https://docs.aws.amazon.com/cli/latest/topic/config-vars.html\\n\
+for the details on how to set them up."
+ filexit="true"
+
+elif [ -f "$CREDFILE" ]; then
+ active_credentials_file="$CREDFILE"
+else
+ # assume creds are in ~/.aws/config
+ active_credentials_file=""
+ echo
+ echo -e "${BIWhite}${On_Black}\
+NOTE: A shared credentials file (~/.aws/credentials) was not found.\\n
+ Assuming credentials are stored in the config file (~/.aws/config).${Color_Off}"
+fi
+
+if [[ "$filexit" == "true" ]]; then
+ echo
+ exit 1
+fi
+
+CONFFILE="$active_config_file"
+CREDFILE="$active_credentials_file"
+custom_configfiles_reset="false"
+
+# read the credentials and/or config files,
+# and make sure that at least one profile is configured
+ONEPROFILE="false"
+conffile_vars_in_credfile="false"
+
+if [[ $CREDFILE != "" ]]; then
+ while IFS='' read -r line || [[ -n "$line" ]]; do
+ [[ "$line" =~ ^\[(.*)\].* ]] &&
+ profile_ident="${BASH_REMATCH[1]}"
+
+ if [[ "$profile_ident" != "" ]]; then
+ ONEPROFILE="true"
+ fi
+
+ if [[ "$line" =~ ^[[:space:]]*ca_bundle.* ]] ||
+ [[ "$line" =~ ^[[:space:]]*cli_timestamp_format.* ]] ||
+ [[ "$line" =~ ^[[:space:]]*credential_source.* ]] ||
+ [[ "$line" =~ ^[[:space:]]*external_id.* ]] ||
+ [[ "$line" =~ ^[[:space:]]*mfa_serial.* ]] ||
+ [[ "$line" =~ ^[[:space:]]*output.* ]] ||
+ [[ "$line" =~ ^[[:space:]]*parameter_validation.* ]] ||
+ [[ "$line" =~ ^[[:space:]]*region.* ]] ||
+ [[ "$line" =~ ^[[:space:]]*role_arn.* ]] ||
+ [[ "$line" =~ ^[[:space:]]*role_session_name.* ]] ||
+ [[ "$line" =~ ^[[:space:]]*source_profile.* ]]; then
+
+ conffile_vars_in_credfile="true"
+ fi
+
+ done < "$CREDFILE"
+fi
+
+if [[ "$conffile_vars_in_credfile" == "true" ]]; then
+ echo -e "\\n${BIWhite}${On_Black}\
+NOTE: The credentials file ($CREDFILE) contains variables\\n\
+ only supported in the config file ($CONFFILE).${Color_Off}\\n\\n\
+ The credentials file may only contain credential and session information;\\n\
+ please see https://docs.aws.amazon.com/cli/latest/userguide/cli-config-files.html\\n\
+ and https://docs.aws.amazon.com/cli/latest/topic/config-vars.html\\n\
+ for the details on how to correctly set up config and credentials files."
+fi
+
+# check for presence of at least one set of credentials
+# in the CONFFILE (in the event CREDFILE is not used)
+profile_header_check="false"
+access_key_id_check="false"
+secret_access_key_check="false"
+while IFS='' read -r line || [[ -n "$line" ]]; do
+ [[ "$line" =~ ^\[(.*)\].* ]] &&
+ profile_ident="${BASH_REMATCH[1]}"
+
+ if [[ "$profile_ident" != "" ]]; then
+ profile_header_check="true"
+ fi
+
+ if [[ "$line" =~ ^[[:space:]]*aws_access_key_id.* ]]; then
+ access_key_id_check="true"
+ fi
+
+ if [[ "$line" =~ ^[[:space:]]*aws_secret_access_key.* ]]; then
+ secret_access_key_check="true"
+ fi
+
+done < "$CONFFILE"
+
+if [[ "$profile_header_check" == "true" ]] &&
+ [[ "$secret_access_key_check" == "true" ]] &&
+ [[ "$access_key_id_check" == "true" ]]; then
+
+ ONEPROFILE="true"
+fi
+
+if [[ "$ONEPROFILE" == "false" ]]; then
+ echo
+ echo -e "${BIRed}${On_Black}\
+NO CONFIGURED AWS PROFILES FOUND.${Color_Off}\\n\
+Please make sure you have at least one configured profile.\\n\
+For more info on how to set them up, see AWS CLI configuration\\n\
+documentation at the following URLs:\\n\
+https://docs.aws.amazon.com/cli/latest/userguide/cli-config-files.html\\n\
+and https://docs.aws.amazon.com/cli/latest/topic/config-vars.html"
+
+else
+
+ # Check OS for some supported platforms
+ OS="$(uname)"
+ case $OS in
+ 'Linux')
+ OS='Linux'
+ ;;
+ 'Darwin')
+ OS='macOS'
+ ;;
+ *)
+ OS='unknown'
+ echo
+ echo "NOTE: THIS SCRIPT HAS NOT BEEN TESTED ON YOUR CURRENT PLATFORM."
+ echo
+ ;;
+ esac
+
+ # make sure the selected/default CREDFILE exists
+ # even if the creds are in the CONFFILE, and that
+ # it has a linefeed in the end. The session data
+ # is always stored in the CREDFILE!
+ if [[ $CREDFILE != "" ]]; then
+ c=$(tail -c 1 "$CREDFILE")
+ if [[ "$c" != "" ]]; then
+ echo "" >> "$CREDFILE"
+ fi
+ else
+ echo "" > $CREDFILE
+ chmod 600 $CREDFILE
+ fi
+
+ # make sure the selected CONFFILE has a linefeed in the end
+ c=$(tail -c 1 "$CONFFILE")
+ if [[ "$c" != "" ]]; then
+ echo "" >> "$CONFFILE"
+ fi
+
+ ## FUNCTIONAL PREREQS PASSED; PROCEED WITH EXPIRED SESSION CHECK
+ ## AMD CUSTOM CONFIGURATION/PROPERTY READ-IN
+
+ # define profiles arrays, variables
+ declare -a creds_ident
+ declare -a creds_aws_access_key_id
+ declare -a creds_aws_secret_access_key
+ declare -a creds_aws_session_token
+ declare -a creds_aws_mfasession_init_time
+ declare -a creds_aws_rolesession_expiry
+ declare -a creds_type
+ persistent_MFA="false"
+ profiles_iterator=0
+ profiles_init=0
+
+ # an ugly hack to relate different values because
+ # macOS *still* does not provide bash 4.x by default,
+ # so associative arrays aren't available
+ # NOTE: this pass is quick as no aws calls are done
+ roles_in_credfile="false"
+ while IFS='' read -r line || [[ -n "$line" ]]; do
+ if [[ "$line" =~ ^\[(.*)\].* ]]; then
+ _ret="${BASH_REMATCH[1]}"
+
+ if [[ $profiles_init -eq 0 ]]; then
+ creds_ident[$profiles_iterator]="${_ret}"
+ profiles_init=1
+ fi
+
+ if [[ "$_ret" != "" ]] &&
+ [[ "$_ret" =~ -mfasession$ ]]; then
+
+ creds_type[$profiles_iterator]="mfasession"
+ elif [[ "$_ret" != "" ]] &&
+ [[ "$_ret" =~ -rolesession$ ]]; then
+
+ creds_type[$profiles_iterator]="rolesession"
+ else
+ creds_type[$profiles_iterator]="baseprofile"
+ fi
+
+ if [[ "${creds_ident[$profiles_iterator]}" != "$_ret" ]]; then
+ ((profiles_iterator++))
+ creds_ident[$profiles_iterator]=$_ret
+ fi
+ fi
+
+ # aws_access_key_id
+ [[ "$line" =~ ^[[:space:]]*aws_access_key_id[[:space:]]*=[[:space:]]*(.*)[[:space:]]*$ ]] &&
+ creds_aws_access_key_id[$profiles_iterator]="${BASH_REMATCH[1]}"
+
+ # aws_secret_access_key
+ [[ "$line" =~ ^[[:space:]]*aws_secret_access_key[[:space:]]*=[[:space:]]*(.*)[[:space:]]*$ ]] &&
+ creds_aws_secret_access_key[$profiles_iterator]="${BASH_REMATCH[1]}"
+
+ # aws_session_token
+ [[ "$line" =~ ^[[:space:]]*aws_session_token[[:space:]]*=[[:space:]]*(.*)[[:space:]]*$ ]] &&
+ creds_aws_session_token[$profiles_iterator]="${BASH_REMATCH[1]}"
+
+ # aws_session_init_time
+ [[ "$line" =~ ^[[:space:]]*aws_session_init_time[[:space:]]*=[[:space:]]*(.*)[[:space:]]*$ ]] &&
+ creds_aws_mfasession_init_time[$profiles_iterator]="${BASH_REMATCH[1]}"
+
+ # role_arn
+ if [[ "$line" =~ ^[[:space:]]*role_arn[[:space:]]*=[[:space:]]*(.*)[[:space:]]*$ ]]; then
+ this_role="${BASH_REMATCH[1]}"
+
+ echo -e "\\n${BIRed}${On_Black}\
+NOTE: The role '${BASH_REMATCH[1]}' is defined in\\n\
+ the credentials file ($CREDFILE) and will be ignored.${Color_Off}\\n\\n\
+ The credentials file may only contain profile/session secrets;\\n\
+ you can define roles in the config file ($CONFFILE).\\n"
+
+ fi
+
+ done < "$CREDFILE"
+
+ # init arrays to hold profile configuration detail
+ # (may also include credentials)
+ declare -a confs_ident
+
+#todo: merge from the creds array:
+# baseprofile creds -> baseprofile ident
+# rolesession creds -> role profile ident
+# mfasession creds -> baseprofile mfa arrays -or- new profile for each?
+
+ declare -a confs_aws_access_key_id
+ declare -a confs_aws_secret_access_key
+ declare -a confs_aws_session_init_time
+ declare -a confs_aws_session_token
+ declare -a confs_ca_bundle
+ declare -a confs_cli_timestamp_format
+ declare -a confs_credential_source
+ declare -a confs_external_id
+ declare -a confs_mfa_serial
+ declare -a confs_mfasec
+ declare -a confs_output
+ declare -a confs_parameter_validation
+ declare -a confs_region
+ declare -a confs_role_arn
+ declare -a confs_role_session_name
+ declare -a confs_role_source
+ declare -a confs_type
+ confs_init=0
+ confs_iterator=0
+
+ # read in the config file params
+ while IFS='' read -r line || [[ -n "$line" ]]; do
+
+ if [[ "$line" =~ ^\[[[:space:]]*profile[[:space:]]*(.*)[[:space:]]*\].* ]]; then
+ _ret="${BASH_REMATCH[1]}"
+
+ if [[ $confs_init -eq 0 ]]; then
+ confs_ident[$confs_iterator]="${_ret}"
+ confs_init=1
+ elif [[ "${confs_ident[$confs_iterator]}" != "$_ret" ]]; then
+ ((confs_iterator++))
+ confs_ident[$confs_iterator]="${_ret}"
+ fi
+
+ # assume baseprofile type; this is overridden for roles
+ confs_type[$confs_iterator]="baseprofile"
+ fi
+
+ # aws_access_key_id
+ [[ "$line" =~ ^[[:space:]]*aws_access_key_id[[:space:]]*=[[:space:]]*(.*)[[:space:]]*$ ]] &&
+ confs_aws_access_key_id[$confs_iterator]="${BASH_REMATCH[1]}"
+
+ # aws_secret_access_key
+ [[ "$line" =~ ^[[:space:]]*aws_secret_access_key[[:space:]]*=[[:space:]]*(.*)[[:space:]]*$ ]] &&
+ confs_aws_secret_access_key[$confs_iterator]="${BASH_REMATCH[1]}"
+
+ # aws_session_init_time (should always be blank in cofig, but just in case)
+ [[ "$line" =~ ^[[:space:]]*aws_session_init_time[[:space:]]*=[[:space:]]*(.*)[[:space:]]*$ ]] &&
+ creds_aws_mfasession_init_time[$confs_iterator]="${BASH_REMATCH[1]}"
+
+ # aws_session_token
+ [[ "$line" =~ ^[[:space:]]*aws_session_token[[:space:]]*=[[:space:]]*(.*)[[:space:]]*$ ]] &&
+ creds_aws_session_token[$confs_iterator]="${BASH_REMATCH[1]}"
+
+ # ca_bundle
+ [[ "$line" =~ ^[[:space:]]*ca_bundle[[:space:]]*=[[:space:]]*(.*)[[:space:]]*$ ]] &&
+ confs_ca_bundle[$confs_iterator]=${BASH_REMATCH[1]}
+
+ # cli_timestamp_format
+ [[ "$line" =~ ^[[:space:]]*cli_timestamp_format[[:space:]]*=[[:space:]]*(.*)[[:space:]]*$ ]] &&
+ confs_cli_timestamp_format[$confs_iterator]=${BASH_REMATCH[1]}
+
+ # credential_source
+ [[ "$line" =~ ^[[:space:]]*credential_source[[:space:]]*=[[:space:]]*(.*)[[:space:]]*$ ]] &&
+ confs_credential_source[$confs_iterator]=${BASH_REMATCH[1]}
+
+ # external_id
+ [[ "$line" =~ ^[[:space:]]*external_id[[:space:]]*=[[:space:]]*(.*)[[:space:]]*$ ]] &&
+ confs_external_id[$confs_iterator]=${BASH_REMATCH[1]}
+
+ # mfa_serial
+ [[ "$line" =~ ^[[:space:]]*mfa_serial[[:space:]]*=[[:space:]]*(.*)[[:space:]]*$ ]] &&
+ confs_mfa_serial[$confs_iterator]=${BASH_REMATCH[1]}
+
+ # mfasec
+ [[ "$line" =~ ^[[:space:]]*mfasec[[:space:]]*=[[:space:]]*(.*)[[:space:]]*$ ]] &&
+ confs_mfasec[$confs_iterator]=${BASH_REMATCH[1]}
+
+ # output
+ [[ "$line" =~ ^[[:space:]]*output[[:space:]]*=[[:space:]]*(.*)[[:space:]]*$ ]] &&
+ confs_output[$confs_iterator]=${BASH_REMATCH[1]}
+
+ # parameter_validation
+ [[ "$line" =~ ^[[:space:]]*parameter_validation[[:space:]]*=[[:space:]]*(.*)[[:space:]]*$ ]] &&
+ confs_parameter_validation[$confs_iterator]=${BASH_REMATCH[1]}
+
+ # region
+ [[ "$line" =~ ^[[:space:]]*region[[:space:]]*=[[:space:]]*(.*)[[:space:]]*$ ]] &&
+ confs_region[$confs_iterator]=${BASH_REMATCH[1]}
+
+ # role_arn
+ if [[ "$line" =~ ^[[:space:]]*role_arn[[:space:]]*=[[:space:]]*(.*)[[:space:]]*$ ]]; then
+ confs_role_arn[$confs_iterator]=${BASH_REMATCH[1]}
+ confs_type[$confs_iterator]="role"
+ fi
+
+ # role_session_name
+ [[ "$line" =~ ^[[:space:]]*role_session_name[[:space:]]*=[[:space:]]*(.*)[[:space:]]*$ ]] &&
+ confs_role_session_name[$confs_iterator]=${BASH_REMATCH[1]}
+
+ # role_source
+ [[ "$line" =~ ^[[:space:]]*role_source[[:space:]]*=[[:space:]]*(.*)[[:space:]]*$ ]] &&
+ confs_role_source[$confs_iterator]=${BASH_REMATCH[1]}
+
+ done < "$CONFFILE"
+
+ # make sure environment has either no config
+ # or a functional config before we proceed
+ checkEnvSession
+
+ # get default region and output format
+ # (since at least one profile should exist
+ # at this point, and one should be selected)
+ default_region=$(aws --profile default configure get region)
+ [[ "$DEBUG" == "true" ]] && echo -e "\\n${Cyan}${On_Black}result for 'aws configure get region --profile default':\\n${ICyan}'${default_region}'${Color_Off}\\n\\n"
+
+ default_output=$(aws --profile default configure get output)
+ [[ "$DEBUG" == "true" ]] && echo -e "\\n${Cyan}${On_Black}result for 'aws configure get output --profile default':\\n${ICyan}'${default_output}'${Color_Off}\\n\\n"
+
+ if [[ "$default_output" == "" ]]; then
+ # default output is not set in the config;
+ # set the default to the AWS default
+ # internally (so that it's available
+ # for the MFA sessions)
+ default_output="json"
+ [[ "$DEBUG" == "true" ]] && echo -e "\\n${Cyan}${On_Black}default output for this script was set to: ${ICyan}json${Color_Off}\\n\\n"
+ echo -e "\\n${BIWhite}${On_Black}\
+The default output format has not been configured; 'json' format is used.\\n\
+You can modify it, for example, like so:\\n\
+${BIWhite}${On_Black}source ./source-this-to-clear-AWS-envvars.sh\\n\
+aws configure set output \"table\"${Color_Off}\\n"
+ fi
+
+ if [[ "$default_region" == "" ]]; then
+ echo -e "${BIWhite}${On_Black}\
+NOTE: The default region has not been configured.${Color_Off}\\n\
+ Some operations may fail if each [parent] profile doesn't\\n\
+ have the region set. You can set the default region in\\n\
+ '$CONFFILE', for example, like so:\\n\
+ ${BIWhite}${On_Black}source ./source-this-to-clear-AWS-envvars.sh\\n\
+ aws configure set region \"us-east-1\"${Color_Off}\\n
+ (do not use the '--profile' switch when configuring the defaults)"
+ fi
+
+ echo
+
+# todo: remove default requirement below altogether?
+#
+## BEGIN REMOVE?
+if [[ "true" == "false" ]]; then
+
+ if [[ "$AWS_ACCESS_KEY_ID" != "" ]]; then
+ current_aws_access_key_id="${AWS_ACCESS_KEY_ID}"
+ else
+ current_aws_access_key_id="$(aws configure get aws_access_key_id)"
+ [[ "$DEBUG" == "true" ]] && echo -e "\\n${Cyan}${On_Black}result for: 'aws configure get aws_access_key_id':\\n${ICyan}${current_aws_access_key_id}${Color_Off}\\n\\n"
+ fi
+
+ idxLookup idx creds_aws_access_key_id[@] "$current_aws_access_key_id"
+
+ if [[ $idx != "" ]]; then
+ currently_selected_profile_ident_printable="'${creds_ident[$idx]}'"
+ else
+ if [[ "${PRECHECK_AWS_PROFILE}" != "" ]]; then
+ currently_selected_profile_ident_printable="'${PRECHECK_AWS_PROFILE}' [transient]"
+ else
+ currently_selected_profile_ident_printable="unknown/transient"
+ fi
+ fi
+
+ process_user_arn="$(aws sts get-caller-identity --output text --query 'Arn' 2>&1)"
+ [[ "$DEBUG" == "true" ]] && echo -e "\\n${Cyan}${On_Black}result for: 'aws sts get-caller-identity --query 'Arn' --output text':\\n${ICyan}${process_user_arn}${Color_Off}\\n\\n"
+
+ # prompt to switch to default on any error
+ if [[ "$process_user_arn" =~ 'error occurred' ]]; then
+ continue_maybe "invalid"
+
+ currently_selected_profile_ident_printable="'default'"
+ process_user_arn="$(aws sts get-caller-identity --output text --query 'Arn' 2>&1)"
+ [[ "$DEBUG" == "true" ]] && echo -e "\\n${Cyan}${On_Black}result for: 'aws sts get-caller-identity --query 'Arn' --output text' \\(after profile reset\\):\\n${ICyan}${process_user_arn}${Color_Off}\\n\\n"
+ fi
+
+ # this bails out on errors
+ checkAWSErrors "true" "$process_user_arn" "$currently_selected_profile_ident_printable"
+
+ # we didn't bail out; continuing...
+ # get the actual username and user account
+ # (username may be different from the arbitrary profile ident)
+ if [[ "$process_user_arn" =~ ([[:digit:]]+):user.*/([^/]+)$ ]]; then
+ profile_user_acc="${BASH_REMATCH[1]}"
+ process_username="${BASH_REMATCH[2]}"
+ fi
+
+ getAccountAlias _ret
+ if [[ "${_ret}" != "" ]]; then
+ account_alias_if_any="@${_ret}"
+ else
+ account_alias_if_any="@${profile_user_acc}"
+ fi
+
+ echo -e "Executing this script as the AWS/IAM user $process_username $account_alias_if_any (profile $currently_selected_profile_ident_printable).\\n"
+
+fi
+## END REMOVE?
+
+ # declare the arrays for baseprofile loop
+ declare -a baseprofile_ident
+ declare -a baseprofile_status
+ declare -a baseprofile_user
+ declare -a baseprofile_arn
+ declare -a baseprofile_account
+ declare -a baseprofile_account_alias
+ declare -a baseprofile_region
+ declare -a baseprofile_output
+ declare -a baseprofile_mfa
+ declare -a baseprofile_mfa_arn
+ declare -a baseprofile_mfa_status
+ declare -a baseprofile_mfa_mfasec
+ cred_profilecounter=0
+
+ echo -ne "${BIWhite}${On_Black}Please wait"
+
+
+
+#todo: instead of re-reading credentials file, loop over the unified array?
+#todo: create at least roleprofile_ arrays; mfaprofiles are probably embedded in baseprofile arrays
+
+ # read the credentials file
+ while IFS='' read -r line || [[ -n "$line" ]]; do
+
+ [[ "$line" =~ ^\[(.*)\].* ]] &&
+ profile_ident="${BASH_REMATCH[1]}"
+
+ # transfer possible MFA mfasec from config array
+ idxLookup idx confs_ident[@] "$profile_ident"
+ if [[ $idx != "" ]]; then
+ baseprofile_mfa_mfasec[$cred_profilecounter]=${confs_mfasec[$idx]}
+ fi
+#----------------
+ # only process if profile identifier is present,
+ # and if it's not a mfasession profile
+ # (mfasession profiles have '-mfasession' postfix)
+ if [[ "$profile_ident" != "" ]] &&
+ [[ ! "$profile_ident" =~ -mfasession$ ]] &&
+ [[ ! "$profile_ident" =~ -rolesession$ ]] ; then
+
+ # store this profile ident
+ baseprofile_ident[$cred_profilecounter]="$profile_ident"
+
+#todo: we already have this info in the profiles (creds) array, no?
+ # store this profile region and output format
+ baseprofile_region[$cred_profilecounter]=$(aws --profile "$profile_ident" configure get region)
+ [[ "$DEBUG" == "true" ]] && echo -e "\\n${Cyan}${On_Black}result for: 'aws --profile \"$profile_ident\" configure get region':\\n${ICyan}${baseprofile_region[$cred_profilecounter]}${Color_Off}\\n\\n"
+ baseprofile_output[$cred_profilecounter]=$(aws --profile "$profile_ident" configure get output)
+ [[ "$DEBUG" == "true" ]] && echo -e "\\n${Cyan}${On_Black}result for: 'aws --profile \"$profile_ident\" configure get output':\\n${ICyan}${baseprofile_output[$cred_profilecounter]}${Color_Off}\\n\\n"
+
+ # get the user ARN; this should be always
+ # available for valid profiles
+ user_arn="$(aws --profile "$profile_ident" sts get-caller-identity --output text --query 'Arn' 2>&1)"
+ [[ "$DEBUG" == "true" ]] && echo -e "\\n${Cyan}${On_Black}result for: 'aws --profile \"$profile_ident\" sts get-caller-identity --query 'Arn' --output text':\\n${ICyan}${user_arn}${Color_Off}\\n\\n"
+
+ if [[ "$user_arn" =~ ^arn:aws ]]; then
+ baseprofile_arn[$cred_profilecounter]=$user_arn
+ else
+ # must be a bad profile
+ baseprofile_arn[$cred_profilecounter]=""
+ fi
+
+ # get the actual username
+ # (may be different from the arbitrary profile ident)
+ if [[ "$user_arn" =~ ([[:digit:]]+):user.*/([^/]+)$ ]]; then
+ profile_user_acc="${BASH_REMATCH[1]}"
+ profile_username="${BASH_REMATCH[2]}"
+ fi
+
+ if [[ "$user_arn" =~ 'error occurred' ]]; then
+ baseprofile_user[$cred_profilecounter]=""
+ baseprofile_account[$cred_profilecounter]=""
+ else
+ baseprofile_user[$cred_profilecounter]="$profile_username"
+ baseprofile_account[$cred_profilecounter]="$profile_user_acc"
+ fi
+
+ # get the account alias (if any) for the user/profile
+ getAccountAlias _ret "$profile_ident"
+ baseprofile_account_alias[$cred_profilecounter]="${_ret}"
+
+ # find the MFA session for the current profile if one exists ("There can be only one")
+ # (profile with profilename + "-mfasession" postfix)
+
+#todo: this information is already in the profiles (creds) array, stop re-reading the CREDFILE over and over again!
+ while IFS='' read -r line || [[ -n "$line" ]]; do
+ [[ "$line" =~ \[(${profile_ident}-mfasession)\]$ ]] &&
+ mfa_profile_ident="${BASH_REMATCH[1]}"
+ done < "$CREDFILE"
+ baseprofile_mfa[$cred_profilecounter]="$mfa_profile_ident"
+
+ # check to see if this profile has access currently
+ # (this is not 100% as it depends on the defined IAM access;
+ # however if MFA enforcement is set following the example policy,
+ # this should produce a reasonably reliable result)
+ profile_check="$(aws --profile "$profile_ident" iam get-user --query 'User.Arn' --output text 2>&1)"
+ [[ "$DEBUG" == "true" ]] && echo -e "\\n${Cyan}${On_Black}result for: 'aws --profile \"$profile_ident\" iam get-user --query 'User.Arn' --output text':\\n${ICyan}${profile_check}${Color_Off}\\n\\n"
+
+ if [[ "$profile_check" =~ ^arn:aws ]]; then
+ baseprofile_status[$cred_profilecounter]="OK"
+ else
+ baseprofile_status[$cred_profilecounter]="LIMITED"
+ fi
+
+ # get MFA ARN if available
+ # (obviously not available if a vMFA device
+ # isn't configured for the profile)
+ mfa_arn="$(aws --profile "$profile_ident" iam list-mfa-devices \
+ --user-name "${baseprofile_user[$cred_profilecounter]}" \
+ --output text \
+ --query 'MFADevices[].SerialNumber' 2>&1)"
+
+ [[ "$DEBUG" == "true" ]] && echo -e "\\n${Cyan}${On_Black}result for: 'aws --profile \"$profile_ident\" iam list-mfa-devices --user-name \"${baseprofile_user[$cred_profilecounter]}\" --query 'MFADevices[].SerialNumber' --output text':\\n${ICyan}${mfa_arn}${Color_Off}\\n\\n"
+
+ if [[ "$mfa_arn" =~ ^arn:aws ]]; then
+ baseprofile_mfa_arn[$cred_profilecounter]="$mfa_arn"
+ else
+ baseprofile_mfa_arn[$cred_profilecounter]=""
+ fi
+
+ # If an existing MFA profile was found, check its status
+ # (uses timestamps first if available; falls back to
+ # less reliable get-user command -- its output depends
+ # on IAM policy settings, and while it's usually accurate
+ # it's still not as reliable)
+ if [[ "$mfa_profile_ident" != "" ]]; then
+
+ getInitTime _ret_timestamp "$mfa_profile_ident"
+ getDuration _ret_duration "$mfa_profile_ident"
+ getRemaining _ret_remaining "${_ret_timestamp}" "${_ret_duration}"
+
+ if [[ ${_ret_remaining} -eq 0 ]]; then
+ # session has expired
+
+ baseprofile_mfa_status[$cred_profilecounter]="EXPIRED"
+ elif [[ ${_ret_remaining} -gt 0 ]]; then
+ # session time remains
+
+ getPrintableTimeRemaining _ret "${_ret_remaining}"
+ baseprofile_mfa_status[$cred_profilecounter]="${_ret} remaining"
+ elif [[ ${_ret_remaining} -eq -1 ]]; then
+ # no timestamp; legacy or initialized outside of this utility
+
+ mfa_profile_check="$(aws --profile "$mfa_profile_ident" iam get-user --query 'User.Arn' --output text 2>&1)"
+ [[ "$DEBUG" == "true" ]] && echo -e "\\n${Cyan}${On_Black}result for: 'aws --profile \"$mfa_profile_ident\" iam get-user --query 'User.Arn' --output text':\\n${ICyan}${mfa_profile_check}${Color_Off}\\n\\n"
+
+ if [[ "$mfa_profile_check" =~ ^arn:aws ]]; then
+ baseprofile_mfa_status[$cred_profilecounter]="OK"
+ elif [[ "$mfa_profile_check" =~ ExpiredToken ]]; then
+ baseprofile_mfa_status[$cred_profilecounter]="EXPIRED"
+ else
+ baseprofile_mfa_status[$cred_profilecounter]="LIMITED"
+ fi
+ fi
+ fi
+#----------------
+ ## DEBUG (enable with DEBUG="true" on top of the file)
+ if [[ "$DEBUG" == "true" ]]; then
+
+ echo
+ echo "PROFILE IDENT: $profile_ident (${baseprofile_status[$cred_profilecounter]})"
+ echo "USER ARN: ${baseprofile_arn[$cred_profilecounter]}"
+ echo "USER NAME: ${baseprofile_user[$cred_profilecounter]}"
+ echo "ACCOUNT ALIAS: ${baseprofile_account_alias[$cred_profilecounter]}"
+ echo "MFA ARN: ${baseprofile_mfa_arn[$cred_profilecounter]}"
+ echo "MFA SESSION CUSTOM LENGTH (MFASEC): ${baseprofile_mfa_mfasec[$cred_profilecounter]}"
+ if [[ "${baseprofile_mfa[$cred_profilecounter]}" == "" ]]; then
+ echo "MFA PROFILE IDENT:"
+ else
+ echo "MFA PROFILE IDENT: ${baseprofile_mfa[$cred_profilecounter]} (${baseprofile_mfa_status[$cred_profilecounter]})"
+ fi
+ echo
+ ## END DEBUG
+ else
+ echo -n "."
+ fi
+
+ # erase variables & increase iterator for the next iteration
+ mfa_arn=""
+ user_arn=""
+ profile_ident=""
+ profile_check=""
+ profile_username=""
+ mfa_profile_ident=""
+ mfa_profile_check=""
+
+ ((cred_profilecounter++))
+
+ else
+
+ [[ "$DEBUG" == "true" ]] && echo -e "\\n${Cyan}${On_Black}Skipping a role or MFA session profile: '$profile_ident'${Color_Off}\\n"
+
+ fi
+ done < "$CREDFILE"
+ echo -e "${Color_Off}"
+
+ # select the profile (first, single profile + a possible persistent MFA session)
+ mfa_req="false"
+ if [[ ${#baseprofile_ident[@]} == 1 ]]; then
+ echo
+ [[ "${baseprofile_user[0]}" != "" ]] && prcpu="${baseprofile_user[0]}" || prcpu="unknown — a bad profile?"
+
+ if [[ "${baseprofile_account_alias[0]}" != "" ]]; then
+ prcpaa=" @${baseprofile_account_alias[0]}"
+ elif [[ "${baseprofile_account[0]}" != "" ]]; then
+ # use the AWS account number if no alias has been defined
+ prcpaa=" @${baseprofile_account[0]}"
+ else
+ # or nothing for a bad profile
+ prcpaa=""
+ fi
+
+ echo -e "${Green}${On_Black}You have one configured profile: ${BIGreen}${baseprofile_ident[0]} ${Green}(IAM: ${prcpu}${prcpaa})${Color_Off}"
+
+ mfa_session_status="false"
+ if [[ "${baseprofile_mfa_arn[0]}" != "" ]]; then
+ echo ".. its vMFAd is enabled"
+
+ if [[ "${baseprofile_mfa_status[0]}" != "EXPIRED" &&
+ "${baseprofile_mfa_status[0]}" != "" ]]; then
+
+ echo -e ".. and it ${BIWhite}${On_Black}has an active MFA session with ${baseprofile_mfa_status[0]}${Color_Off}"
+
+ mfa_session_status="true"
+ else
+ echo -e ".. but no active persistent MFA sessions exist"
+ fi
+ else
+ echo -e "${BIRed}${On_Black}.. but it doesn't have a virtual MFA device attached/enabled;\\n cannot continue${Color_Off} (use 'enable-disable-vmfa-device.sh' script\\n first to enable a vMFAd)!"
+ echo
+ exit 1
+ fi
+
+ echo
+ echo "Do you want to:"
+ echo -e "${BIWhite}${On_Black}1${Color_Off}: Start/renew an MFA session for the profile mentioned above?"
+ echo -e "${BIWhite}${On_Black}2${Color_Off}: Use the above profile as-is (without MFA)?"
+ [[ "${mfa_session_status}" == "true" ]] && echo -e "${BIWhite}${On_Black}3${Color_Off}: Resume the existing active MFA session (${baseprofile_mfa_status[0]})?"
+ echo
+ while :
+ do
+ read -s -n 1 -r
+ case $REPLY in
+ 1)
+ echo "Starting an MFA session.."
+ selprofile="1"
+ mfa_req="true"
+ break
+ ;;
+ 2)
+ echo "Selecting the profile as-is (no MFA).."
+ selprofile="1"
+ break
+ ;;
+ 3)
+ if [[ "${mfa_session_status}" == "true" ]]; then
+ echo "Resuming the existing MFA session.."
+ selprofile="1m"
+ break
+ else
+ echo "Please select one of the options above!"
+ fi
+ ;;
+ *)
+ echo "Please select one of the options above!"
+ ;;
+ esac
+ done
+
+ else # more than 1 profile
+
+ # create the profile selections
+ echo
+ echo -e "${BIWhite}${On_DGreen} AVAILABLE AWS PROFILES: ${Color_Off}"
+ echo
+ SELECTR=0
+ ITER=1
+ for i in "${baseprofile_ident[@]}"
+ do
+ if [[ "${baseprofile_mfa_arn[$SELECTR]}" != "" ]]; then
+ mfa_notify="; ${Green}${On_Black}vMFAd enabled${Color_Off}"
+ else
+ mfa_notify="; vMFAd not configured"
+ fi
+
+ [[ "${baseprofile_user[$SELECTR]}" != "" ]] && prcpu="${baseprofile_user[$SELECTR]}" || prcpu="unknown — a bad profile?"
+
+ if [[ "${baseprofile_account_alias[$SELECTR]}" != "" ]]; then
+ prcpaa=" @${baseprofile_account_alias[$SELECTR]}"
+ elif [[ "${baseprofile_account[$SELECTR]}" != "" ]]; then
+ # use the AWS account number if no alias has been defined
+ prcpaa=" @${baseprofile_account[$SELECTR]}"
+ else
+ # or nothing for a bad profile
+ prcpaa=""
+ fi
+
+ echo -en "${BIWhite}${On_Black}${ITER}: $i${Color_Off} (IAM: ${prcpu}${prcpaa}${mfa_notify})\\n"
+
+ if [[ "${baseprofile_mfa_status[$SELECTR]}" != "EXPIRED" &&
+ "${baseprofile_mfa_status[$SELECTR]}" != "" ]]; then
+ echo -e "${BIWhite}${On_Black}${ITER}m: $i MFA profile${Color_Off} (${baseprofile_mfa_status[$SELECTR]})"
+ fi
+
+ echo
+ ((ITER++))
+ ((SELECTR++))
+ done
+
+ # this is used to determine whether to trigger a MFA request for a MFA profile
+ active_mfa="false"
+
+ # this is used to determine whether to print MFA questions/details
+ mfaprofile="false"
+
+ # prompt for profile selection
+ printf "You can switch to a base profile to use it as-is, start an MFA session\\nfor a profile if it is marked as \"vMFAd enabled\", or switch to an existing\\nactive MFA session if any are available (indicated by the letter 'm' after\\nthe profile ID, e.g. '1m'; NOTE: the expired MFA sessions are not shown).\\n"
+ echo -en "\\n${BIWhite}${On_Black}SELECT A PROFILE BY THE ID:${Color_Off} "
+ read -r selprofile
+ echo -en "\\n"
+
+ fi # end profile selection
+
+ # process the selection
+ if [[ "$selprofile" != "" ]]; then
+ # capture the numeric part of the selection
+ [[ $selprofile =~ ^([[:digit:]]+) ]] &&
+ selprofile_check="${BASH_REMATCH[1]}"
+ if [[ "$selprofile_check" != "" ]]; then
+
+ # if the numeric selection was found,
+ # translate it to the array index and validate
+ ((actual_selprofile=selprofile_check-1))
+
+ profilecount=${#baseprofile_ident[@]}
+ if [[ $actual_selprofile -ge $profilecount ||
+ $actual_selprofile -lt 0 ]]; then
+ # a selection outside of the existing range was specified
+ echo -e "There is no profile '${selprofile}'.\\n"
+
+ exit 1
+ fi
+
+ # was an existing MFA profile selected?
+ [[ $selprofile =~ ^[[:digit:]]+(m)$ ]] &&
+ selprofile_mfa_check="${BASH_REMATCH[1]}"
+
+ # if this is an MFA profile, it must be in OK or LIMITED status to select
+ if [[ "$selprofile_mfa_check" != "" &&
+ "${baseprofile_mfa_status[$actual_selprofile]}" != "EXPIRED" &&
+ "${baseprofile_mfa_status[$actual_selprofile]}" != "" ]]; then
+
+ # get the parent profile name
+ # transpose selection (starting from 1) to array index (starting from 0)
+ mfa_parent_profile_ident="${baseprofile_ident[$actual_selprofile]}"
+
+ final_selection="${baseprofile_mfa[$actual_selprofile]}"
+ echo "SELECTED MFA PROFILE: ${final_selection} (for the base profile \"${mfa_parent_profile_ident}\")"
+
+ # this is used to determine whether to print MFA questions/details
+ mfaprofile="true"
+
+ # this is used to determine whether to trigger a MFA request for a MFA profile
+ active_mfa="true"
+
+ elif [[ "$selprofile_mfa_check" != "" &&
+ "${baseprofile_mfa_status[$actual_selprofile]}" == "" ]]; then
+ # mfa ('m') profile was selected for a profile that no mfa profile exists
+ echo -e "${BIRed}${On_Black}There is no profile '${selprofile}'.${Color_Off}\\n"
+ exit 1
+
+ else
+ # a base profile was selected
+ if [[ $selprofile =~ ^[[:digit:]]+$ ]]; then
+ echo "SELECTED PROFILE: ${baseprofile_ident[$actual_selprofile]}"
+ final_selection="${baseprofile_ident[$actual_selprofile]}"
+ else
+ # non-acceptable characters were present in the selection
+ echo -e "${BIRed}There is no profile '${selprofile}'.${Color_Off}\\n"
+ exit 1
+ fi
+ fi
+
+ else
+ # no numeric part in selection
+ echo -e "${BIRed}${On_Black}There is no profile '${selprofile}'.${Color_Off}\\n"
+ exit 1
+ fi
+ else
+ # empty selection
+ echo -e "${BIRed}${On_Black}There is no profile '${selprofile}'.${Color_Off}\\n"
+ exit 1
+ fi
+
+ # this is an MFA request (an MFA ARN exists but the MFA is not active)
+ if ( [[ "${baseprofile_mfa_arn[$actual_selprofile]}" != "" &&
+ "$active_mfa" == "false" ]] ) ||
+ [[ "$mfa_req" == "true" ]]; then # mfa_req is a single profile MFA request
+
+ # prompt for the MFA code
+ echo -e "\\n${BIWhite}${On_Black}\
+Enter the current MFA one time pass code for the profile '${baseprofile_ident[$actual_selprofile]}'${Color_Off} to start/renew an MFA session,\\n\
+or leave empty (just press [ENTER]) to use the selected profile without the MFA.\\n"
+
+ while :
+ do
+ echo -en "${BIWhite}${On_Black}"
+ read -p ">>> " -r mfacode
+ echo -en "${Color_Off}"
+ if ! [[ "$mfacode" =~ ^$ || "$mfacode" =~ [0-9]{6} ]]; then
+ echo -e "${BIRed}${On_Black}The MFA pass code must be exactly six digits, or blank to bypass (to use the profile without an MFA session).${Color_Off}"
+ continue
+ else
+ break
+ fi
+ done
+
+ elif [[ "$active_mfa" == "false" ]]; then # no vMFAd configured (no vMFAd ARN); print a notice
+
+ # this is used to determine whether to print MFA questions/details
+ mfaprofile="false"
+
+ # reset entered MFA code (just to be safe)
+ mfacode=""
+ echo -e "\\nA vMFAd has not been set up for this profile (run 'enable-disable-vmfa-device.sh' script to configure the vMFAd)."
+ fi
+
+ if [[ "$mfacode" != "" ]]; then
+ # init an MFA session (request an MFA session token)
+ AWS_USER_PROFILE="${baseprofile_ident[$actual_selprofile]}"
+ AWS_2AUTH_PROFILE="${AWS_USER_PROFILE}-mfasession"
+ ARN_OF_MFA=${baseprofile_mfa_arn[$actual_selprofile]}
+
+ # make sure an entry exists for the MFA profile in ~/.aws/config
+ profile_lookup="$(grep "$CONFFILE" -e '^[[:space:]]*\[[[:space:]]*profile '"${AWS_2AUTH_PROFILE}"'[[:space:]]*\][[:space:]]*$')"
+ if [[ "$profile_lookup" == "" ]]; then
+ echo -en "\\n\\n">> "$CONFFILE"
+ echo "[profile ${AWS_2AUTH_PROFILE}]" >> "$CONFFILE"
+ fi
+
+ echo -e "\\nAcquiring MFA session token for the profile: ${BIWhite}${On_Black}${AWS_USER_PROFILE}${Color_Off}..."
+
+ getDuration AWS_SESSION_DURATION "$AWS_USER_PROFILE"
+
+ mfa_credentials_result=$(aws --profile "$AWS_USER_PROFILE" sts get-session-token \
+ --duration "$AWS_SESSION_DURATION" \
+ --serial-number "$ARN_OF_MFA" \
+ --token-code $mfacode \
+ --output text)
+
+ if [[ "$DEBUG" == "true" ]]; then
+ echo -e "\\n${Cyan}${On_Black}result for: 'aws --profile \"$AWS_USER_PROFILE\" sts get-session-token --duration \"$AWS_SESSION_DURATION\" --serial-number \"$ARN_OF_MFA\" --token-code $mfacode --output text':\\n${ICyan}${mfa_credentials_result}${Color_Off}\\n\\n"
+ fi
+
+ # this bails out on errors
+ checkAWSErrors "true" "$mfa_credentials_result" "$AWS_USER_PROFILE" "An error occurred while attempting to acquire the MFA session credentials; cannot continue!"
+
+ # we didn't bail out; continuing...
+ read -r AWS_ACCESS_KEY_ID AWS_SECRET_ACCESS_KEY AWS_SESSION_TOKEN <<< $(printf '%s' "$mfa_credentials_result" | awk '{ print $2, $4, $5 }')
+
+ if [ -z "$AWS_ACCESS_KEY_ID" ]; then
+ echo -e "\\n${BIRed}${On_Black}Could not initialize the requested MFA session.${Color_Off}\\n"
+ exit 1
+ else
+ # this is used to determine whether to print MFA questions/details
+ mfaprofile="true"
+ echo -e "${Green}${On_Black}MFA session token acquired.${Color_Off}\\n"
+
+ # export the selection to the remaining subshell commands in this script
+ # so that "--profile" selection is not required, and in fact should not
+ # be used for setting the credentials (or else they go to the conffile)
+ export AWS_PROFILE=${AWS_2AUTH_PROFILE}
+ # Make sure the final selection profile name has '-mfasession' suffix
+ # (before this assignment it's not present when going from a base profile to an MFA profile)
+ final_selection="$AWS_2AUTH_PROFILE"
+
+ # optionally set the persistent (~/.aws/credentials or custom cred file entries):
+ # aws_access_key_id, aws_secret_access_key, and aws_session_token
+ # for the MFA profile
+ getPrintableTimeRemaining _ret "$AWS_SESSION_DURATION"
+ validity_period=${_ret}
+
+ echo -e "${BIWhite}${On_Black}\
+Make this MFA session persistent?${Color_Off} (Saves the session in $CREDFILE\\n\
+so that you can return to it during its validity period, ${validity_period}.)"
+
+ read -s -p "$(echo -e "${BIWhite}${On_Black}Yes (default) - make peristent${Color_Off}; No - only the envvars will be used ${BIWhite}${On_Black}[Y]${Color_Off}/N ")" -n 1 -r
+ echo
+ if [[ $REPLY =~ ^[Yy]$ ]] ||
+ [[ $REPLY == "" ]]; then
+
+ persistent_MFA="true"
+ # NOTE: These do not require the "--profile" switch because AWS_PROFILE
+ # has been exported above. If you set --profile, the details
+ # go to the CONFFILE instead of CREDFILE (so don't set it! :-)
+ aws configure set aws_access_key_id "$AWS_ACCESS_KEY_ID"
+ aws configure set aws_secret_access_key "$AWS_SECRET_ACCESS_KEY"
+ aws configure set aws_session_token "$AWS_SESSION_TOKEN"
+
+ # MFA session profiles: set Init Time in the static profile (a custom key in ~/.aws/credentials)
+ # Role session profiles: set Expiration time in the static profile (a custom key in ~/.aws/credentials)
+ addInitTime "${AWS_2AUTH_PROFILE}"
+ fi
+ # init time for envvar exports (if selected)
+ AWS_SESSION_INIT_TIME=$(date +%s)
+
+ ## DEBUG
+ if [[ "$DEBUG" == "true" ]]; then
+ echo
+ echo "AWS_ACCESS_KEY_ID: $AWS_ACCESS_KEY_ID"
+ echo "AWS_SECRET_ACCESS_KEY: $AWS_SECRET_ACCESS_KEY"
+ echo "AWS_SESSION_TOKEN: $AWS_SESSION_TOKEN"
+ echo "AWS_SESSION_INIT_TIME: $AWS_SESSION_INIT_TIME"
+ echo "AWS_SESSION_DURATION: $AWS_SESSION_DURATION"
+ fi
+ ## END DEBUG
+ fi
+
+ elif [[ "$active_mfa" == "false" ]]; then
+
+ # this is used to determine whether to print MFA questions/details
+ mfaprofile="false"
+ fi
+
+ # export final selection to the environment
+ # (no change for the initialized MFA sessions)
+ export AWS_PROFILE=$final_selection
+
+ # get region and output format for the selected profile
+ AWS_DEFAULT_REGION=$(aws --profile "${final_selection}" configure get region)
+ [[ "$DEBUG" == "true" ]] && echo -e "\\n${Cyan}${On_Black}result for: 'aws --profile \"${final_selection}\" configure get region':\\n${ICyan}${AWS_DEFAULT_REGION}${Color_Off}\\n\\n"
+
+ AWS_DEFAULT_OUTPUT=$(aws --profile "${final_selection}" configure get output)
+ [[ "$DEBUG" == "true" ]] && echo -e "\\n${Cyan}${On_Black}result for: 'aws --profile \"${final_selection}\" configure get output':\\n${ICyan}${AWS_DEFAULT_OUTPUT}${Color_Off}\\n\\n"
+
+ # If the region and output format have not been set for this profile, set them.
+ # For the parent/base profiles, use defaults; for MFA profiles use first
+ # the base/parent settings if present, then the defaults
+ if [[ "${AWS_DEFAULT_REGION}" == "" ]]; then
+ # retrieve parent profile region if an MFA profie
+ if [[ "${baseprofile_region[$actual_selprofile]}" != "" &&
+ "${mfaprofile}" == "true" ]]; then
+ set_new_region=${baseprofile_region[$actual_selprofile]}
+ echo -e "\\n
+NOTE: Region had not been configured for the selected MFA profile;\\n
+ it has been set to same as the parent profile ('$set_new_region')."
+ fi
+ if [[ "${set_new_region}" == "" ]]; then
+ if [[ "$default_region" != "" ]]; then
+ set_new_region=${default_region}
+ echo -e "\\n
+NOTE: Region had not been configured for the selected profile;\\n
+ it has been set to the default region ('${default_region}')."
+ else
+ echo -e "\\n${BIRed}${On_Black}\
+NOTE: Region had not been configured for the selected profile\\n\
+ and the defaults were not available (the base profiles:\\n\
+ the default region; the MFA/role sessions: the region of\\n\
+ the parent profile, then the default region). Cannot continue.\\n\\n\
+ Please set the default region, or region for the profile\\n\
+ (or the parent profile for MFA/role sessions) and try again."
+
+ exit 1
+ fi
+ fi
+
+ AWS_DEFAULT_REGION="${set_new_region}"
+ if [[ "$mfacode" == "" ]] ||
+ ( [[ "$mfacode" != "" ]] && [[ "$persistent_MFA" == "true" ]] ); then
+
+ aws configure --profile "${final_selection}" set region "${set_new_region}"
+ fi
+ fi
+
+ if [[ "${AWS_DEFAULT_OUTPUT}" == "" ]]; then
+ # retrieve parent profile output format if an MFA profile
+ if [[ "${baseprofile_output[$actual_selprofile]}" != "" &&
+ "${mfaprofile}" == "true" ]]; then
+ set_new_output=${baseprofile_output[$actual_selprofile]}
+ echo -e "\
+NOTE: The output format had not been configured for the selected MFA profile;\\n
+ it has been set to same as the parent profile ('$set_new_output')."
+ fi
+ if [[ "${set_new_output}" == "" ]]; then
+ set_new_output=${default_output}
+ echo -e "\
+NOTE: The output format had not been configured for the selected profile;\\n
+ it has been set to the default output format ('${default_output}')."
+ fi
+#todo^ was the default set, or is 'json' being used as the default internally?
+
+ AWS_DEFAULT_OUTPUT="${set_new_output}"
+ if [[ "$mfacode" == "" ]] ||
+ ( [[ "$mfacode" != "" ]] && [[ "$persistent_MFA" == "true" ]] ); then
+
+ aws configure --profile "${final_selection}" set output "${set_new_output}"
+ fi
+ fi
+
+ if [[ "$mfacode" == "" ]]; then # this is _not_ a new MFA session, so read in selected persistent values;
+ # for new MFA sessions they are already present
+ AWS_ACCESS_KEY_ID=$(aws configure --profile "${final_selection}" get aws_access_key_id)
+ [[ "$DEBUG" == "true" ]] && echo -e "\\n${Cyan}${On_Black}result for: 'aws configure --profile \"${final_selection}\" get aws_access_key_id':\\n${ICyan}${AWS_ACCESS_KEY_ID}${Color_Off}\\n\\n"
+
+ AWS_SECRET_ACCESS_KEY=$(aws configure --profile "${final_selection}" get aws_secret_access_key)
+ [[ "$DEBUG" == "true" ]] && echo -e "\\n${Cyan}${On_Black}result for: 'aws configure --profile \"${final_selection}\" get aws_access_key_id':\\n${ICyan}${AWS_SECRET_ACCESS_KEY}${Color_Off}\\n\\n"
+
+ if [[ "$mfaprofile" == "true" ]]; then # this is a persistent MFA profile (a subset of [[ "$mfacode" == "" ]])
+ AWS_SESSION_TOKEN=$(aws configure --profile "${final_selection}" get aws_session_token)
+ [[ "$DEBUG" == "true" ]] && echo -e "\\n${Cyan}${On_Black}result for: 'aws configure --profile \"${final_selection}\" get aws_session_token':\\n${ICyan}${AWS_SESSION_TOKEN}${Color_Off}\\n\\n"
+
+ getInitTime _ret "${final_selection}"
+ AWS_SESSION_INIT_TIME=${_ret}
+ getDuration _ret "${final_selection}"
+ AWS_SESSION_DURATION=${_ret}
+ fi
+ fi
+
+ echo -e "\\n\\n${BIWhite}${On_DGreen} * * * PROFILE DETAILS * * * ${Color_Off}\\n"
+
+ if [[ "$mfaprofile" == "true" ]]; then
+ echo -e "${BIWhite}${On_Black}MFA profile name: '${final_selection}'${Color_Off}"
+ echo
+ else
+ echo -e "${BIWhite}${On_Black}Profile name '${final_selection}'${Color_Off}"
+ echo -e "\\n${BIWhite}${On_Black}NOTE: This is not an MFA session!${Color_Off}"
+ echo
+ fi
+ echo -e "Region is set to: ${BIWhite}${On_Black}${AWS_DEFAULT_REGION}${Color_Off}"
+ echo -e "Output format is set to: ${BIWhite}${On_Black}${AWS_DEFAULT_OUTPUT}${Color_Off}"
+ echo
+
+ if [[ "$mfacode" == "" ]] || # re-entering a persistent profile, MFA or not
+ ( [[ "$mfacode" != "" ]] && [[ "$persistent_MFA" == "true" ]] ); then # a new persistent MFA session was initialized;
+ # Display the persistent profile's envvar details for export?
+ read -s -p "$(echo -e "${BIWhite}${On_Black}Do you want to export the selected profile's secrets to the environment${Color_Off} (for s3cmd, etc)? - Y/${BIWhite}${On_Black}[N]${Color_Off} ")" -n 1 -r
+ if [[ $REPLY =~ ^[Nn]$ ]] ||
+ [[ $REPLY == "" ]]; then
+
+ secrets_out="false"
+ else
+ secrets_out="true"
+ fi
+ echo
+ echo
+ else
+ # A new transient MFA session was initialized;
+ # its details have to be displayed for export or it can't be used
+ secrets_out="true"
+ fi
+
+ if [[ "$mfacode" != "" ]] && [[ "$persistent_MFA" == "false" ]]; then
+ echo -e "${BIWhite}${On_Black}*** THIS IS A NON-PERSISTENT MFA SESSION${Color_Off}! THE MFA SESSION ACCESS KEY ID,\\n SECRET ACCESS KEY, AND THE SESSION TOKEN ARE *ONLY* SHOWN BELOW!"
+ echo
+ fi
+
+ if [[ "$OS" == "macOS" ]] ||
+ [[ "$OS" == "Linux" ]] ; then
+
+ echo -e "${BIGreen}${On_Black}\
+*** It is imperative that the following environment variables are exported/unset\\n\
+ as specified below in order to activate your selection! The required\\n\
+ export/unset commands have already been copied on your clipboard!\\n\
+ ${BIWhite}Just paste on the command line with Command-v, then press [ENTER]\\n\
+ to complete the process!${Color_Off}"
+ echo
+
+ # since the custom configfile settings were reset,
+ # the selected profile is from the default config,
+ # and so we need to reset the references in env for
+ # consistency
+ if [[ "$custom_configfiles_reset" == "true" ]]; then
+ envvar_config_clear_custom_config="; unset AWS_CONFIG_FILE; unset AWS_SHARED_CREDENTIALS_FILE"
+ else
+ envvar_config_clear_custom_config=""
+ fi
+
+ if [[ "$final_selection" == "default" ]]; then
+ # default profile doesn't need to be selected with an envvar
+ envvar_config="unset AWS_PROFILE; unset AWS_ACCESS_KEY_ID; unset AWS_SECRET_ACCESS_KEY; unset AWS_SESSION_TOKEN; unset AWS_SESSION_INIT_TIME; unset AWS_SESSION_DURATION; unset AWS_DEFAULT_REGION; unset AWS_DEFAULT_OUTPUT${envvar_config_clear_custom_config}"
+ if [[ "$OS" == "macOS" ]]; then
+ echo -n "$envvar_config" | pbcopy
+ elif [[ "$OS" == "Linux" ]] &&
+ exists xclip; then
+
+ echo -n "$envvar_config" | xclip -i
+ xclip -o | xclip -sel clip
+
+ echo
+ fi
+ echo "unset AWS_PROFILE"
+ else
+ envvar_config="export AWS_PROFILE=\"${final_selection}\"; unset AWS_ACCESS_KEY_ID; unset AWS_SECRET_ACCESS_KEY; unset AWS_SESSION_TOKEN; unset AWS_SESSION_INIT_TIME; unset AWS_SESSION_DURATION; unset AWS_DEFAULT_REGION; unset AWS_DEFAULT_OUTPUT${envvar_config_clear_custom_config}"
+ if [[ "$OS" == "macOS" ]]; then
+ echo -n "$envvar_config" | pbcopy
+ elif [[ "$OS" == "Linux" ]] &&
+ exists xclip; then
+
+ echo -n "$envvar_config" | xclip -i
+ xclip -o | xclip -sel clip
+ fi
+ echo "export AWS_PROFILE=\"${final_selection}\""
+ fi
+
+ if [[ "$custom_configfiles_reset" == "true" ]]; then
+ echo "unset AWS_CONFIG_FILE"
+ echo "unset AWS_SHARED_CREDENTIALS_FILE"
+ fi
+
+ if [[ "$secrets_out" == "false" ]]; then
+ echo "unset AWS_ACCESS_KEY_ID"
+ echo "unset AWS_SECRET_ACCESS_KEY"
+ echo "unset AWS_DEFAULT_REGION"
+ echo "unset AWS_DEFAULT_OUTPUT"
+ echo "unset AWS_SESSION_INIT_TIME"
+ echo "unset AWS_SESSION_DURATION"
+ echo "unset AWS_SESSION_TOKEN"
+ else
+ echo "export AWS_ACCESS_KEY_ID=\"${AWS_ACCESS_KEY_ID}\""
+ echo "export AWS_SECRET_ACCESS_KEY=\"${AWS_SECRET_ACCESS_KEY}\""
+ echo "export AWS_DEFAULT_REGION=${AWS_DEFAULT_REGION}"
+ echo "export AWS_DEFAULT_OUTPUT=${AWS_DEFAULT_OUTPUT}"
+ if [[ "$mfaprofile" == "true" ]]; then
+ echo "export AWS_SESSION_INIT_TIME=${AWS_SESSION_INIT_TIME}"
+ echo "export AWS_SESSION_DURATION=${AWS_SESSION_DURATION}"
+ echo "export AWS_SESSION_TOKEN=\"${AWS_SESSION_TOKEN}\""
+
+ envvar_config="export AWS_PROFILE=\"${final_selection}\"; export AWS_ACCESS_KEY_ID=\"${AWS_ACCESS_KEY_ID}\"; export AWS_SECRET_ACCESS_KEY=\"${AWS_SECRET_ACCESS_KEY}\"; export AWS_DEFAULT_REGION=${AWS_DEFAULT_REGION}; export AWS_DEFAULT_OUTPUT=${AWS_DEFAULT_OUTPUT}; export AWS_SESSION_INIT_TIME=${AWS_SESSION_INIT_TIME}; export AWS_SESSION_DURATION=${AWS_SESSION_DURATION}; export AWS_SESSION_TOKEN=\"${AWS_SESSION_TOKEN}\"${envvar_config_clear_custom_config}"
+
+ if [[ "$OS" == "macOS" ]]; then
+ echo -n "$envvar_config" | pbcopy
+ elif [[ "$OS" == "Linux" ]] &&
+ exists xclip; then
+
+ echo -n "$envvar_config" | xclip -i
+ xclip -o | xclip -sel clip
+ fi
+ else
+ echo "unset AWS_SESSION_INIT_TIME"
+ echo "unset AWS_SESSION_DURATION"
+ echo "unset AWS_SESSION_TOKEN"
+
+ envvar_config="export AWS_PROFILE=\"${final_selection}\"; export AWS_ACCESS_KEY_ID=\"${AWS_ACCESS_KEY_ID}\"; export AWS_SECRET_ACCESS_KEY=\"${AWS_SECRET_ACCESS_KEY}\"; export AWS_DEFAULT_REGION=${AWS_DEFAULT_REGION}; export AWS_DEFAULT_OUTPUT=${AWS_DEFAULT_OUTPUT}; unset AWS_SESSION_INIT_TIME; unset AWS_SESSION_DURATION; unset AWS_SESSION_TOKEN${envvar_config_clear_custom_config}"
+
+ if [[ "$OS" == "macOS" ]]; then
+ echo -n "$envvar_config" | pbcopy
+ elif [[ "$OS" == "Linux" ]] &&
+ exists xclip; then
+
+ echo -n "$envvar_config" | xclip -i
+ xclip -o | xclip -sel clip
+ fi
+ fi
+ fi
+ echo
+ if [[ "$OS" == "Linux" ]]; then
+ if exists xclip; then
+ echo -e "${BIGreen}${On_Black}\
+NOTE: xclip found; the envvar configuration command is now on\\n\
+ your X PRIMARY clipboard -- just paste on the command line,\\n\
+ and press [ENTER])${Color_Off}"
+
+ else
+
+ echo -e "\\n\
+NOTE: If you're using an X GUI on Linux, install 'xclip' to have\\n\\
+ the activation command copied to the clipboard automatically!"
+ fi
+ fi
+
+ echo -e "${Green}${On_Black}\\n\
+** Make sure to export/unset all the new values as instructed above to\\n\
+ make sure no conflicting profile/secrets remain in the environment!${Color_Off}\\n"
+
+ echo -e "${Green}${On_Black}\
+** You can temporarily override the profile set/selected in the environment\\n\
+ using the \"--profile AWS_PROFILE_NAME\" switch with awscli. For example:${Color_Off}\\n\
+ ${BIGreen}${On_Black}aws --profile default sts get-caller-identity${Color_Off}\\n"
+
+ echo -e "${Green}${On_Black}\
+** To easily remove any all AWS profile settings and secrets information\\n
+ from the environment, simply source the included script, like so:${Color_Off}\\n\
+ ${BIGreen}${On_Black}source ./source-this-to-clear-AWS-envvars.sh\\n"
+
+ echo -e "\\n${BIWhite}${On_Black}\
+PASTE THE PROFILE ACTIVATION COMMAND FROM THE CLIPBOARD\\n\
+ON THE COMMAND LINE NOW, AND PRESS ENTER! THEN YOU'RE DONE!${Color_Off}\\n"
+
+ else # not macOS, not Linux, so some other weird OS like Windows..
+
+ echo -e "\
+It is imperative that the following environment variables\\n\
+are exported/unset to activate the selected profile!\\n"
+
+ echo -e "\
+Execute the following on the command line to activate\\n\
+this profile for the 'aws', 's3cmd', etc. commands.\\n"
+
+ echo -e "\
+NOTE: Even if you only use a named profile ('AWS_PROFILE'),\\n\
+ it's important to execute all of the export/unset commands\\n\
+ to make sure previously set environment variables won't override\\n\
+ the selected configuration.\\n"
+
+ if [[ "$final_selection" == "default" ]]; then
+ # default profile doesn't need to be selected with an envvar
+ echo "unset AWS_PROFILE \\"
+ else
+ echo "export AWS_PROFILE=\"${final_selection}\" \\"
+ fi
+
+ # since the custom configfile settings were reset,
+ # the selected profile is from the default config,
+ # and so we need to reset the references in env for
+ # consistency
+ if [[ "$custom_configfiles_reset" == "true" ]]; then
+ echo "unset AWS_CONFIG_FILE \\"
+ echo "unset AWS_SHARED_CREDENTIALS_FILE \\"
+ fi
+
+ if [[ "$secrets_out" == "false" ]]; then
+ echo "unset AWS_ACCESS_KEY_ID \\"
+ echo "unset AWS_SECRET_ACCESS_KEY \\"
+ echo "unset AWS_DEFAULT_REGION \\"
+ echo "unset AWS_DEFAULT_OUTPUT \\"
+ echo "unset AWS_SESSION_INIT_TIME \\"
+ echo "unset AWS_SESSION_DURATION \\"
+ echo "unset AWS_SESSION_TOKEN"
+ else
+ echo "export AWS_PROFILE=\"${final_selection}\" \\"
+ echo "export AWS_ACCESS_KEY_ID=\"${AWS_ACCESS_KEY_ID}\" \\"
+ echo "export AWS_SECRET_ACCESS_KEY=\"${AWS_SECRET_ACCESS_KEY}\" \\"
+ echo "export AWS_DEFAULT_REGION=${AWS_DEFAULT_REGION} \\"
+ echo "export AWS_DEFAULT_OUTPUT=${AWS_DEFAULT_OUTPUT} \\"
+ if [[ "$mfaprofile" == "true" ]]; then
+ echo "export AWS_SESSION_INIT_TIME=${AWS_SESSION_INIT_TIME} \\"
+ echo "export AWS_SESSION_DURATION=${AWS_SESSION_DURATION} \\"
+ echo "export AWS_SESSION_TOKEN=\"${AWS_SESSION_TOKEN}\""
+ else
+ echo "unset AWS_SESSION_INIT_TIME \\"
+ echo "unset AWS_SESSION_DURATION \\"
+ echo "unset AWS_SESSION_TOKEN"
+ fi
+ fi
+
+ echo -e "\\n\
+** Make sure to export/unset all the new values as instructed above to\\n\
+ make sure no conflicting profile/secrets remain in the envrionment!\\n"
+
+ echo -e "\\n\
+** You can temporarily override the profile set/selected in the environment\\n\
+ using the \"--profile AWS_PROFILE_NAME\" switch with awscli. For example:\\n\
+ aws --profile default sts get-caller-identity\\n"
+
+ echo -e "\\n\
+** To easily remove any all AWS profile settings and secrets information\\n\
+ from the environment, simply source the included script, like so:\\n\
+ source ./source-this-to-clear-AWS-envvars.sh\\n"
+
+ fi
+ echo
+fi
diff --git a/awscli-mfa/enable-disable-vmfa-device.sh b/awscli-mfa/enable-disable-vmfa-device.sh
new file mode 100755
index 0000000..fc8391c
--- /dev/null
+++ b/awscli-mfa/enable-disable-vmfa-device.sh
@@ -0,0 +1,1578 @@
+#!/usr/bin/env bash
+
+# todo: handle roles with MFA
+
+# NOTE: Debugging mode prints the secrets on the screen!
+DEBUG="false"
+
+# enable debugging with '-d' or '--debug' command line argument..
+[[ "$1" == "-d" || "$1" == "--debug" ]] && DEBUG="true"
+# .. or by uncommenting the line below:
+#DEBUG="true"
+
+# Set the global session length in seconds below; note that
+# this only sets the client-side duration for the MFA session
+# token! The maximum length of a valid session is enforced by
+# the IAM policy, and is unaffected by this value (if this
+# duration is set to a longer value than the enforcing value
+# in the IAM policy, the token will stop working before it
+# expires on the client side). Matching this value with the
+# enforcing IAM policy provides you with accurate detail
+# about how long a token will continue to be valid.
+#
+# THIS VALUE CAN BE OPTIONALLY OVERRIDDEN PER EACH PROFILE
+# BY ADDING A "mfasec" ENTRY FOR THE PROFILE IN ~/.aws/config
+#
+# The valid session lengths are from 900 seconds (15 minutes)
+# to 129600 seconds (36 hours); currently set (below) to
+# 32400 seconds, or 9 hours.
+MFA_SESSION_LENGTH_IN_SECONDS=32400
+
+# Define the standard locations for the AWS credentials and
+# config files; these can be statically overridden with
+# AWS_SHARED_CREDENTIALS_FILE and AWS_CONFIG_FILE envvars
+# (this script will override these envvars only if the
+# "[default]" profile in the defined custom file(s) is
+# defunct, thus reverting to the below default locations).
+CONFFILE=~/.aws/config
+CREDFILE=~/.aws/credentials
+
+# COLOR DEFINITIONS ==========================================================
+
+# Reset
+Color_Off='\033[0m' # Text Reset
+
+# Regular Colors
+Black='\033[0;30m' # Black
+Red='\033[0;31m' # Red
+Green='\033[0;32m' # Green
+Yellow='\033[0;33m' # Yellow
+Blue='\033[0;34m' # Blue
+Purple='\033[0;35m' # Purple
+Cyan='\033[0;36m' # Cyan
+White='\033[0;37m' # White
+
+# Bold
+BBlack='\033[1;30m' # Black
+BRed='\033[1;31m' # Red
+BGreen='\033[1;32m' # Green
+BYellow='\033[1;33m' # Yellow
+BBlue='\033[1;34m' # Blue
+BPurple='\033[1;35m' # Purple
+BCyan='\033[1;36m' # Cyan
+BWhite='\033[1;37m' # White
+
+# Underline
+UBlack='\033[4;30m' # Black
+URed='\033[4;31m' # Red
+UGreen='\033[4;32m' # Green
+UYellow='\033[4;33m' # Yellow
+UBlue='\033[4;34m' # Blue
+UPurple='\033[4;35m' # Purple
+UCyan='\033[4;36m' # Cyan
+UWhite='\033[4;37m' # White
+
+# Background
+On_Black='\033[40m' # Black
+On_Red='\033[41m' # Red
+On_Green='\033[42m' # Green
+On_Yellow='\033[43m' # Yellow
+On_Blue='\033[44m' # Blue
+On_Purple='\033[45m' # Purple
+On_Cyan='\033[46m' # Cyan
+On_White='\033[47m' # White
+
+# High Intensity
+IBlack='\033[0;90m' # Black
+IRed='\033[0;91m' # Red
+IGreen='\033[0;92m' # Green
+IYellow='\033[0;93m' # Yellow
+IBlue='\033[0;94m' # Blue
+IPurple='\033[0;95m' # Purple
+ICyan='\033[0;96m' # Cyan
+IWhite='\033[0;97m' # White
+
+# Bold High Intensity
+BIBlack='\033[1;90m' # Black
+BIRed='\033[1;91m' # Red
+BIGreen='\033[1;92m' # Green
+BIYellow='\033[1;93m' # Yellow
+BIBlue='\033[1;94m' # Blue
+BIPurple='\033[1;95m' # Purple
+BICyan='\033[1;96m' # Cyan
+BIWhite='\033[1;97m' # White
+
+# High Intensity backgrounds
+On_IBlack='\033[0;100m' # Black
+On_IRed='\033[0;101m' # Red
+On_IGreen='\033[0;102m' # Green
+On_DGreen='\033[48;5;28m' # Dark Green
+On_IYellow='\033[0;103m' # Yellow
+On_IBlue='\033[0;104m' # Blue
+On_IPurple='\033[0;105m' # Purple
+On_ICyan='\033[0;106m' # Cyan
+On_IWhite='\033[0;107m' # White
+
+
+# DEBUG MODE WARNING & BASH VERSION ==========================================
+
+if [[ "$DEBUG" == "true" ]]; then
+ echo -e "\\n${BIWhite}${On_Red} DEBUG MODE ACTIVE ${Color_Off}\\n\\n${BIRed}${On_Black}NOTE: Debug output may include secrets!!!${Color_Off}\\n\\n"
+ echo -e "Using bash version $BASH_VERSION\\n\\n"
+fi
+
+# FUNCTIONS ==================================================================
+
+# `exists` for commands
+exists() {
+ command -v "$1" >/dev/null 2>&1
+}
+
+# precheck envvars for existing/stale session definitions
+checkEnvSession() {
+ # $1 is the check type
+
+ local this_time
+ this_time=$(date +%s)
+
+ # COLLECT AWS_SESSION DATA FROM THE ENVIRONMENT
+ PRECHECK_AWS_PROFILE=$(env | grep AWS_PROFILE)
+ [[ "$PRECHECK_AWS_PROFILE" =~ ^AWS_PROFILE[[:space:]]*=[[:space:]]*(.*)$ ]] &&
+ PRECHECK_AWS_PROFILE="${BASH_REMATCH[1]}"
+
+ PRECHECK_AWS_ACCESS_KEY_ID=$(env | grep AWS_ACCESS_KEY_ID)
+ [[ "$PRECHECK_AWS_ACCESS_KEY_ID" =~ ^AWS_ACCESS_KEY_ID[[:space:]]*=[[:space:]]*(.*)$ ]] &&
+ PRECHECK_AWS_ACCESS_KEY_ID="${BASH_REMATCH[1]}"
+
+ PRECHECK_AWS_SECRET_ACCESS_KEY=$(env | grep AWS_SECRET_ACCESS_KEY)
+ [[ "$PRECHECK_AWS_SECRET_ACCESS_KEY" =~ ^AWS_SECRET_ACCESS_KEY[[:space:]]*=[[:space:]]*(.*)$ ]] &&
+ PRECHECK_AWS_SECRET_ACCESS_KEY="[REDACTED]"
+
+ PRECHECK_AWS_SESSION_TOKEN=$(env | grep AWS_SESSION_TOKEN)
+ [[ "$PRECHECK_AWS_SESSION_TOKEN" =~ ^AWS_SESSION_TOKEN[[:space:]]*=[[:space:]]*(.*)$ ]] &&
+ PRECHECK_AWS_SESSION_TOKEN="[REDACTED]"
+
+ PRECHECK_AWS_SESSION_INIT_TIME=$(env | grep AWS_SESSION_INIT_TIME)
+ [[ "$PRECHECK_AWS_SESSION_INIT_TIME" =~ ^AWS_SESSION_INIT_TIME[[:space:]]*=[[:space:]]*(.*)$ ]] &&
+ PRECHECK_AWS_SESSION_INIT_TIME="${BASH_REMATCH[1]}"
+
+ PRECHECK_AWS_SESSION_DURATION=$(env | grep AWS_SESSION_DURATION)
+ [[ "$PRECHECK_AWS_SESSION_DURATION" =~ ^AWS_SESSION_DURATION[[:space:]]*=[[:space:]]*(.*)$ ]] &&
+ PRECHECK_AWS_SESSION_DURATION="${BASH_REMATCH[1]}"
+
+ PRECHECK_AWS_DEFAULT_REGION=$(env | grep AWS_DEFAULT_REGION)
+ [[ "$PRECHECK_AWS_DEFAULT_REGION" =~ ^AWS_DEFAULT_REGION[[:space:]]*=[[:space:]]*(.*)$ ]] &&
+ PRECHECK_AWS_DEFAULT_REGION="${BASH_REMATCH[1]}"
+
+ PRECHECK_AWS_DEFAULT_OUTPUT=$(env | grep AWS_DEFAULT_OUTPUT)
+ [[ "$PRECHECK_AWS_DEFAULT_OUTPUT" =~ ^AWS_DEFAULT_OUTPUT[[:space:]]*=[[:space:]]*(.*)$ ]] &&
+ PRECHECK_AWS_DEFAULT_OUTPUT="${BASH_REMATCH[1]}"
+
+ PRECHECK_AWS_CA_BUNDLE=$(env | grep AWS_CA_BUNDLE)
+ [[ "$PRECHECK_AWS_CA_BUNDLE" =~ ^AWS_CA_BUNDLE[[:space:]]*=[[:space:]]*(.*)$ ]] &&
+ PRECHECK_AWS_CA_BUNDLE="${BASH_REMATCH[1]}"
+
+ PRECHECK_AWS_SHARED_CREDENTIALS_FILE=$(env | grep AWS_SHARED_CREDENTIALS_FILE)
+ [[ "$PRECHECK_AWS_SHARED_CREDENTIALS_FILE" =~ ^AWS_SHARED_CREDENTIALS_FILE[[:space:]]*=[[:space:]]*(.*)$ ]] &&
+ PRECHECK_AWS_SHARED_CREDENTIALS_FILE="${BASH_REMATCH[1]}"
+
+ PRECHECK_AWS_CONFIG_FILE=$(env | grep AWS_CONFIG_FILE)
+ [[ "$PRECHECK_AWS_CONFIG_FILE" =~ ^AWS_CONFIG_FILE[[:space:]]*=[[:space:]]*(.*)$ ]] &&
+ PRECHECK_AWS_CONFIG_FILE="${BASH_REMATCH[1]}"
+
+ # AWS_PROFILE must be empty or refer to *any* profile in ~/.aws/{credentials|config}
+ # (Even if all the values are overridden by AWS_* envvars they won't work if the
+ # AWS_PROFILE is set to an unknown value!)
+ if [[ "$PRECHECK_AWS_PROFILE" != "" ]]; then
+
+ idxLookup profiles_idx profiles_ident[@] "$PRECHECK_AWS_PROFILE"
+ idxLookup confs_idx confs_ident[@] "$PRECHECK_AWS_PROFILE"
+
+ if [[ "$profiles_idx" == "" ]] && [[ "$confs_idx" == "" ]]; then
+
+ # AWS_PROFILE ident is not recognized;
+ # cannot continue unless it's changed!
+ continue_maybe "invalid"
+ fi
+ fi
+
+ # makes sure that the MFA session has not expired (whether it's
+ # defined in the environment or in ~/.aws/credentials).
+ #
+ # First checking the envvars
+ if [[ "$PRECHECK_AWS_SESSION_TOKEN" != "" ]] &&
+ [[ "$PRECHECK_AWS_SESSION_INIT_TIME" != "" ]] &&
+ [[ "$PRECHECK_AWS_SESSION_DURATION" != "" ]]; then
+ # this is a MFA profile in the environment;
+ # AWS_PROFILE is either empty or valid
+
+ getRemaining _ret "$PRECHECK_AWS_SESSION_INIT_TIME" "$PRECHECK_AWS_SESSION_DURATION"
+ [[ "${_ret}" -eq 0 ]] && continue_maybe "expired"
+
+ elif [[ "$PRECHECK_AWS_PROFILE" =~ -mfasession$ ]] &&
+ [[ "$profiles_idx" != "" ]]; then
+ # AWS_PROFILE is set (and valid, and refers to a persistent mfasession)
+ # but TOKEN, INIT_TIME, and/or DURATION are not, so this is
+ # likely a select of a named profile
+
+ # find the selected persistent MFA profile's init time if one exists
+ profile_time=${profiles_session_init_time[$profiles_idx]}
+
+ # since the duration for the current profile is not set
+ # (as is the case with the mfaprofiles), use the parent/base
+ # profile's duration
+ if [[ "$profile_time" != "" ]]; then
+ getDuration parent_duration "$PRECHECK_AWS_PROFILE"
+ getRemaining _ret "$profile_time" "$parent_duration"
+ [[ "${_ret}" -eq 0 ]] && continue_maybe "expired"
+ fi
+ fi
+ # empty AWS_PROFILE + no in-env MFA session should flow through
+
+ # detect and print informative notice of
+ # effective AWS envvars
+ if [[ "${AWS_PROFILE}" != "" ]] ||
+ [[ "${AWS_ACCESS_KEY_ID}" != "" ]] ||
+ [[ "${AWS_SECRET_ACCESS_KEY}" != "" ]] ||
+ [[ "${AWS_SESSION_TOKEN}" != "" ]] ||
+ [[ "${AWS_SESSION_INIT_TIME}" != "" ]] ||
+ [[ "${AWS_SESSION_DURATION}" != "" ]] ||
+ [[ "${AWS_DEFAULT_REGION}" != "" ]] ||
+ [[ "${AWS_DEFAULT_OUTPUT}" != "" ]] ||
+ [[ "${AWS_CA_BUNDLE}" != "" ]] ||
+ [[ "${AWS_SHARED_CREDENTIALS_FILE}" != "" ]] ||
+ [[ "${AWS_CONFIG_FILE}" != "" ]]; then
+
+ echo
+ echo "** NOTE: THE FOLLOWING AWS_* ENVIRONMENT VARIABLES ARE CURRENTLY IN EFFECT:"
+ echo
+ if [[ "$PRECHECK_AWS_PROFILE" != "$AWS_PROFILE" ]]; then
+ env_notice=" (overridden to 'default')"
+ else
+ env_notice=""
+ fi
+ [[ "$PRECHECK_AWS_PROFILE" != "" ]] && echo " AWS_PROFILE: ${PRECHECK_AWS_PROFILE}${env_notice}"
+ [[ "$PRECHECK_AWS_ACCESS_KEY_ID" != "" ]] && echo " AWS_ACCESS_KEY_ID: $PRECHECK_AWS_ACCESS_KEY_ID"
+ [[ "$PRECHECK_AWS_SECRET_ACCESS_KEY" != "" ]] && echo " AWS_SECRET_ACCESS_KEY: $PRECHECK_AWS_SECRET_ACCESS_KEY"
+ [[ "$PRECHECK_AWS_SESSION_TOKEN" != "" ]] && echo " AWS_SESSION_TOKEN: $PRECHECK_AWS_SESSION_TOKEN"
+ [[ "$PRECHECK_AWS_SESSION_INIT_TIME" != "" ]] && echo " AWS_SESSION_INIT_TIME: $PRECHECK_AWS_SESSION_INIT_TIME"
+ [[ "$PRECHECK_AWS_SESSION_DURATION" != "" ]] && echo " AWS_SESSION_DURATION: $PRECHECK_AWS_SESSION_DURATION"
+ [[ "$PRECHECK_AWS_DEFAULT_REGION" != "" ]] && echo " AWS_DEFAULT_REGION: $PRECHECK_AWS_DEFAULT_REGION"
+ [[ "$PRECHECK_AWS_DEFAULT_OUTPUT" != "" ]] && echo " AWS_DEFAULT_OUTPUT: $PRECHECK_AWS_DEFAULT_OUTPUT"
+ [[ "$PRECHECK_AWS_CA_BUNDLE" != "" ]] && echo " AWS_CA_BUNDLE: $PRECHECK_AWS_CA_BUNDLE"
+ [[ "$PRECHECK_AWS_SHARED_CREDENTIALS_FILE" != "" ]] && echo " AWS_SHARED_CREDENTIALS_FILE: $PRECHECK_AWS_SHARED_CREDENTIALS_FILE"
+ [[ "$PRECHECK_AWS_CONFIG_FILE" != "" ]] && echo " AWS_CONFIG_FILE: $PRECHECK_AWS_CONFIG_FILE"
+ echo
+ fi
+
+}
+
+# workaround function for lack of
+# macOS bash's assoc arrays
+idxLookup() {
+ # $1 is _ret (returns the index)
+ # $2 is the array
+ # $3 is the item to be looked up in the array
+
+ declare -a arr=("${!2}")
+ local key=$3
+ local result=""
+ local maxIndex
+
+ maxIndex=${#arr[@]}
+ ((maxIndex--))
+
+ for (( i=0; i<=maxIndex; i++ ))
+ do
+ if [[ "${arr[$i]}" == "$key" ]]; then
+ result=$i
+ break
+ fi
+ done
+
+ eval "$1=$result"
+}
+
+# return the MFA session init time for the given profile
+getInitTime() {
+ # $1 is _ret
+ # $2 is the profile ident
+
+ local this_ident=$2
+ local profile_time
+
+ # find the profile's init time entry if one exists
+ idxLookup idx profiles_ident[@] "$this_ident"
+ profile_time=${profiles_session_init_time[$idx]}
+
+ eval "$1=${profile_time}"
+}
+
+getDuration() {
+ # $1 is _ret
+ # $2 is the profile ident
+
+ local this_profile_ident="$2"
+ local this_duration
+
+ # use parent profile ident if this is an MFA session
+ [[ "$this_profile_ident" =~ ^(.*)-mfasession$ ]] &&
+ this_profile_ident="${BASH_REMATCH[1]}"
+
+ # look up possible custom duration for the parent profile
+ idxLookup idx confs_ident[@] "$this_profile_ident"
+
+ [[ $idx != "" && "${confs_mfasec[$idx]}" != "" ]] &&
+ this_duration=${confs_mfasec[$idx]} ||
+ this_duration=$MFA_SESSION_LENGTH_IN_SECONDS
+
+ eval "$1=${this_duration}"
+}
+
+# Returns remaining seconds for the given timestamp;
+# if the custom duration is not provided, the global
+# duration setting is used). In the result
+# 0 indicates expired, -1 indicates NaN input
+getRemaining() {
+ # $1 is _ret
+ # $2 is the timestamp
+ # $3 is the duration
+
+ local timestamp=$2
+ local duration=$3
+ local this_time
+ this_time=$(date +%s)
+ local remaining=0
+
+ [[ "${duration}" == "" ]] &&
+ duration=$MFA_SESSION_LENGTH_IN_SECONDS
+
+ if [ ! -z "${timestamp##*[!0-9]*}" ]; then
+ ((session_end=timestamp+duration))
+ if [[ $session_end -gt $this_time ]]; then
+ ((remaining=session_end-this_time))
+ else
+ remaining=0
+ fi
+ else
+ remaining=-1
+ fi
+ eval "$1=${remaining}"
+}
+
+# return printable output for given 'remaining' timestamp
+# (must be pre-incremented with profile duration,
+# such as getRemaining() output)
+getPrintableTimeRemaining() {
+ # $1 is _ret
+ # $2 is the timestamp
+
+ local timestamp=$2
+
+ case $timestamp in
+ -1)
+ response="N/A"
+ ;;
+ 0)
+ response="EXPIRED"
+ ;;
+ *)
+ response=$(printf '%02dh:%02dm:%02ds' $((timestamp/3600)) $((timestamp%3600/60)) $((timestamp%60)))
+ ;;
+ esac
+ eval "$1=${response}"
+}
+
+already_failed="false"
+# here are my args, so..
+continue_maybe() {
+ # $1 is "invalid" or "expired"
+
+ local failtype=$1
+
+ if [[ "$already_failed" == "false" ]]; then
+
+ if [[ "${failtype}" == "expired" ]]; then
+ echo -e "\\n${BIRed}${On_Black}THE MFA SESSION SELECTED/CONFIGURED IN THE ENVIRONMENT HAS EXPIRED.${Color_Off}\\n"
+ else
+ echo -e "\\n${BIRed}${On_Black}THE AWS PROFILE SELECTED/CONFIGURED IN THE ENVIRONMENT IS INVALID.${Color_Off}\\n"
+ fi
+
+ read -s -p "$(echo -e "${BIWhite}${On_Black}Do you want to continue with the default profile?${Color_Off} - ${BIWhite}${On_Black}[Y]${Color_Off}/N ")" -n 1 -r
+ if [[ $REPLY =~ ^[Yy]$ ]] ||
+ [[ $REPLY == "" ]]; then
+
+ already_failed="true"
+
+ # If the defaut profile is already selected
+ # and the profile was still defunct (since
+ # we ended up here), make sure non-standard
+ # config/credentials files are not used
+ if [[ "$AWS_PROFILE" == "" ]] ||
+ [[ "$AWS_PROFILE" == "default" ]]; then
+
+ unset AWS_SHARED_CREDENTIALS_FILE
+ unset AWS_CONFIG_FILE
+ fi
+
+ unset AWS_PROFILE
+ unset AWS_ACCESS_KEY_ID
+ unset AWS_SECRET_ACCESS_KEY
+ unset AWS_SESSION_TOKEN
+ unset AWS_SESSION_INIT_TIME
+ unset AWS_SESSION_DURATION
+ unset AWS_DEFAULT_REGION
+ unset AWS_DEFAULT_OUTPUT
+ unset AWS_CA_BUNDLE
+
+ # override envvar for all the subshell commands
+ export AWS_PROFILE=default
+ echo
+ else
+ echo -e "\\n\\nExecute \"source ./source-this-to-clear-AWS-envvars.sh\", and try again to proceed.\\n"
+ exit 1
+ fi
+ fi
+}
+
+yesno() {
+ # $1 is _ret
+
+ old_stty_cfg=$(stty -g)
+ stty raw -echo
+ answer=$( while ! head -c 1 | grep -i '[yn]' ;do true ;done )
+ stty "$old_stty_cfg"
+
+ if echo "$answer" | grep -iq "^n" ; then
+ _ret="no"
+ else
+ _ret="yes"
+ fi
+
+ eval "$1=${_ret}"
+}
+
+checkAWSErrors() {
+ # $1 is _ret (_is_error)
+ # $2 is exit_on_error (true/false)
+ # $3 is the AWS return (may be good or bad)
+ # $4 is the 'profile in use' if present
+ # $5 is the custom message if present;
+ # only used when $3 is positively present
+ # (such as at MFA token request)
+
+ local exit_on_error=$2
+ local aws_raw_return=$3
+ local profile_in_use
+ local custom_error
+ [[ "$4" == "" ]] && profile_in_use="selected" || profile_in_use="$4"
+ [[ "$5" == "" ]] && custom_error="" || custom_error="${5}\\n\\n"
+
+ local is_error="false"
+ if [[ "$aws_raw_return" =~ 'InvalidClientTokenId' ]]; then
+ echo -en "\\n${BIRed}${On_Black}${custom_error}The AWS Access Key ID does not exist!${Red}\\nCheck the ${profile_in_use} profile configuration including any 'AWS_*' environment variables.${Color_Off}\\n"
+ is_error="true"
+ elif [[ "$aws_raw_return" =~ 'SignatureDoesNotMatch' ]]; then
+ echo -en "\\n${BIRed}${On_Black}${custom_error}The Secret Access Key does not match the Access Key ID!${Red}\\nCheck the ${profile_in_use} profile configuration including any 'AWS_*' environment variables.${Color_Off}\\n"
+ is_error="true"
+ elif [[ "$aws_raw_return" =~ 'IncompleteSignature' ]]; then
+ echo -en "\\n${BIRed}${On_Black}${custom_error}Incomplete signature!${Red}\\nCheck the Secret Access Key of the ${profile_in_use} for typos/completeness (including any 'AWS_*' environment variables).${Color_Off}\\n"
+ is_error="true"
+ elif [[ "$aws_raw_return" =~ 'MissingAuthenticationToken' ]]; then
+ echo -en "\\n${BIRed}${On_Black}${custom_error}The Secret Access Key is not present!${Red}\\nCheck the ${profile_in_use} profile configuration (including any 'AWS_*' environment variables).${Color_Off}\\n"
+ is_error="true"
+ elif [[ "$aws_raw_return" =~ 'AccessDenied' ]]; then
+ echo -en "\\n${BIRed}${On_Black}${custom_error}Access denied!${Red}\\nThe active/selected profile is not authorized for this action.\\nEither you haven't activated an authorized profile, \\nor the effective MFA IAM policy is too restrictive.${Color_Off}\\n"
+ is_error="true"
+ elif [[ "$aws_raw_return" =~ 'AuthFailure' ]]; then
+ echo -en "\\n${BIRed}${On_Black}${custom_error}Authentication failure!${Red}\\nCheck the credentials for the ${profile_in_use} profile (including any 'AWS_*' environment variables).${Color_Off}\\n"
+ is_error="true"
+ elif [[ "$aws_raw_return" =~ 'ServiceUnavailable' ]]; then
+ echo -en "\\n${BIRed}${On_Black}${custom_error}Service unavailable!${Red}\\nThis is likely a temporary problem with AWS; wait for a moment and try again.${Color_Off}\\n"
+ is_error="true"
+ elif [[ "$aws_raw_return" =~ 'Throttling' ]]; then
+ echo -en "\\n${BIRed}${On_Black}${custom_error}Too many requests in too short amount of time!${Red}\\nWait for a few moments and try again.${Color_Off}\\n"
+ is_error="true"
+ elif [[ "$aws_raw_return" =~ 'InvalidAction' ]] ||
+ [[ "$aws_raw_return" =~ 'InvalidQueryParameter' ]] ||
+ [[ "$aws_raw_return" =~ 'MalformedQueryString' ]] ||
+ [[ "$aws_raw_return" =~ 'MissingAction' ]] ||
+ [[ "$aws_raw_return" =~ 'ValidationError' ]] ||
+ [[ "$aws_raw_return" =~ 'MissingParameter' ]] ||
+ [[ "$aws_raw_return" =~ 'InvalidParameterValue' ]]; then
+
+ echo -en "\\n${BIRed}${On_Black}${custom_error}AWS did not understand the request.${Red}\\nThis should never occur with this script. Maybe there was a glitch in\\nthe matrix (maybe the AWS API changed)?\\nRun the script with the '--debug' switch to see the exact error.${Color_Off}\\n"
+ is_error="true"
+ elif [[ "$aws_raw_return" =~ 'InternalFailure' ]]; then
+ echo -en "\\n${BIRed}${On_Black}${custom_error}An unspecified error occurred!${Red}\\n\"Internal Server Error 500\". Sorry I don't have more detail.${Color_Off}\\n"
+ is_error="true"
+ elif [[ "$aws_raw_return" =~ 'error occurred' ]]; then
+ echo -e "${BIRed}${On_Black}${custom_error}An unspecified error occurred!${Red}\\nCheck the ${profile_in_use} profile (including any 'AWS_*' environment variables).\\nRun the script with the '--debug' switch to see the exact error.${Color_Off}\\n"
+ is_error="true"
+ fi
+
+ if [[ "$is_error" == "true" && "$exit_on_error" == "true" ]]; then
+ exit 1
+ elif [[ "$is_error" == "true" ]]; then
+ result="true"
+ else
+ result="false"
+ fi
+
+ eval "$1=$result"
+}
+
+print_mfa_notice() {
+ echo -e "\\n\
+To disable/detach a vMFAd from the profile, you must either have\\n\
+an active MFA session established with it, or use an admin profile\\n\
+that is authorized to remove the MFA for the given profile. Use the\\n\
+'awscli-mfa.sh' script to establish an MFA session for the profile\\n\
+(or select/activate an MFA session if one exists already), then run\\n\
+this script again."
+
+ echo -e "\\n\
+If you do not have possession of the vMFAd (in your GA/Authy app) for\\n\
+the profile whose vMFAd you wish to disable, please send a request to\\n\
+ops to do so. Or, if you have admin credentials for AWS, first activate\\n\
+them with the 'awscli-mfa.sh' script, then run this script again.\\n"
+}
+
+getAccountAlias() {
+ # $1 is _ret (returns the index)
+ # $2 is the profile_ident
+
+ local local_profile_ident=$2
+
+ if [[ "$local_profile_ident" != "" ]]; then
+ profile_param="--profile $local_profile_ident"
+ else
+ profile_param=""
+ fi
+
+ # get the account alias (if any) for the user/profile
+ account_alias_result="$(aws iam list-account-aliases $profile_param --output text --query 'AccountAliases' 2>&1)"
+ [[ "$DEBUG" == "true" ]] && echo -e "\\n${Cyan}${On_Black}result for: 'aws iam list-account-aliases $profile_param --query 'AccountAliases' --output text':\\n${ICyan}${account_alias_result}${Color_Off}\\n\\n"
+
+ if [[ "$account_alias_result" =~ 'error occurred' ]]; then
+ # no access to list account aliases for this profile or other error
+ result=""
+ else
+ result="$account_alias_result"
+ fi
+
+ eval "$1=$result"
+}
+
+## PREREQUISITES CHECK
+
+# is AWS CLI installed?
+if ! exists aws ; then
+ printf "\\n******************************************************************************************************************************\\n\
+This script requires the AWS CLI. See the details here: http://docs.aws.amazon.com/cli/latest/userguide/cli-install-macos.html\\n\
+******************************************************************************************************************************\\n\\n"
+ exit 1
+fi
+
+filexit="false"
+# check for ~/.aws directory, and ~/.aws/{config|credentials} files
+# # if the custom config defs aren't in effect
+if [[ "$AWS_CONFIG_FILE" == "" ]] &&
+ [[ "$AWS_SHARED_CREDENTIALS_FILE" == "" ]] &&
+ [ ! -d ~/.aws ]; then
+
+ echo
+ echo -e "${BIRed}${On_Black}AWSCLI configuration directory '~/.aws' is not present.${Color_Off}\\nMake sure it exists, and that you have at least one profile configured\\nusing the 'config' and 'credentials' files within that directory."
+ filexit="true"
+fi
+
+# SUPPORT CUSTOM CONFIG FILE SET WITH ENVVAR
+if [[ "$AWS_CONFIG_FILE" != "" ]] &&
+ [ -f "$AWS_CONFIG_FILE" ]; then
+
+ active_config_file=$AWS_CONFIG_FILE
+ echo
+ echo -e "${BIWhite}${On_Black}** NOTE: A custom configuration file defined with AWS_CONFIG_FILE envvar in effect: '$AWS_CONFIG_FILE'${Color_Off}"
+
+elif [[ "$AWS_CONFIG_FILE" != "" ]] &&
+ [ ! -f "$AWS_CONFIG_FILE" ]; then
+
+ echo
+ echo -e "${BIRed}${On_Black}The custom config file defined with AWS_CONFIG_FILE envvar, '$AWS_CONFIG_FILE', is not present.${Color_Off}\\nMake sure it is present or purge the envvar.\\nSee http://docs.aws.amazon.com/cli/latest/userguide/cli-config-files.html for details on how to set them up."
+ filexit="true"
+
+elif [ -f "$CONFFILE" ]; then
+ active_config_file="$CONFFILE"
+else
+ echo
+ echo -e "${BIRed}${On_Black}AWSCLI configuration file '$CONFFILE' was not found.${Color_Off}\\nMake sure it and '$CREDFILE' files exist.\\nSee http://docs.aws.amazon.com/cli/latest/userguide/cli-config-files.html for details on how to set them up."
+ filexit="true"
+fi
+
+# SUPPORT CUSTOM CREDENTIALS FILE SET WITH ENVVAR
+if [[ "$AWS_SHARED_CREDENTIALS_FILE" != "" ]] &&
+ [ -f "$AWS_SHARED_CREDENTIALS_FILE" ]; then
+
+ active_credentials_file=$AWS_SHARED_CREDENTIALS_FILE
+ echo
+ echo -e "${BIWhite}${On_Black}** NOTE: A custom credentials file defined with AWS_SHARED_CREDENTIALS_FILE envvar in effect: '$AWS_SHARED_CREDENTIALS_FILE'${Color_Off}"
+
+elif [[ "$AWS_SHARED_CREDENTIALS_FILE" != "" ]] &&
+ [ ! -f "$AWS_SHARED_CREDENTIALS_FILE" ]; then
+
+ echo
+ echo -e "${BIRed}${On_Black}The custom credentials file defined with AWS_SHARED_CREDENTIALS_FILE envvar, '$AWS_SHARED_CREDENTIALS_FILE', is not present.${Color_Off}\\nMake sure it is present or purge the envvar.\\nSee http://docs.aws.amazon.com/cli/latest/userguide/cli-config-files.html for details on how to set them up."
+ filexit="true"
+
+elif [ -f "$CREDFILE" ]; then
+ active_credentials_file="$CREDFILE"
+else
+ echo
+ echo -e "${BIRed}${On_Black}AWSCLI credentials file '$CREDFILE' was not found.${Color_Off}\\nMake sure it and '$CONFFILE' files exist.\\nSee http://docs.aws.amazon.com/cli/latest/userguide/cli-config-files.html for details on how to set them up."
+ filexit="true"
+fi
+
+if [[ "$filexit" == "true" ]]; then
+ echo
+ exit 1
+fi
+
+CONFFILE="$active_config_file"
+CREDFILE="$active_credentials_file"
+
+# read the credentials file and make sure that at least one profile is configured
+ONEPROFILE="false"
+while IFS='' read -r line || [[ -n "$line" ]]; do
+ [[ "$line" =~ ^\[(.*)\].* ]] &&
+ profile_ident="${BASH_REMATCH[1]}"
+
+ if [[ "$profile_ident" != "" ]]; then
+ ONEPROFILE="true"
+ fi
+done < "$CREDFILE"
+
+if [[ "$ONEPROFILE" == "false" ]]; then
+ echo
+ echo -e "${BIRed}${On_Black}NO CONFIGURED AWS PROFILES FOUND.${Color_Off}\\nPlease make sure you have '$CONFFILE' (profile configurations),\\nand '$CREDFILE' (profile credentials) files, and at least\\none configured profile. For more info, see AWS CLI documentation at:\\nhttp://docs.aws.amazon.com/cli/latest/userguide/cli-config-files.html"
+ echo
+
+else
+
+ # Check OS for some supported platforms
+ OS="$(uname)"
+ case $OS in
+ 'Linux')
+ OS='Linux'
+ ;;
+ 'Darwin')
+ OS='macOS'
+ ;;
+ *)
+ OS='unknown'
+ echo
+ echo "** NOTE: THIS SCRIPT HAS NOT BEEN TESTED ON YOUR CURRENT PLATFORM."
+ echo
+ ;;
+ esac
+
+ # make sure ~/.aws/credentials has a linefeed in the end
+ c=$(tail -c 1 "$CREDFILE")
+ if [[ "$c" != "" ]]; then
+ echo "" >> "$CREDFILE"
+ fi
+
+ # make sure ~/.aws/config has a linefeed in the end
+ c=$(tail -c 1 "$CONFFILE")
+ if [[ "$c" != "" ]]; then
+ echo "" >> "$CONFFILE"
+ fi
+
+ ## FUNCTIONAL PREREQS PASSED; PROCEED WITH EXPIRED SESSION CHECK
+ ## AMD CUSTOM CONFIGURATION/PROPERTY READ-IN
+
+ # define profiles arrays, variables
+ declare -a profiles_ident
+ declare -a profiles_type
+ declare -a profiles_key_id
+ declare -a profiles_secret_key
+ declare -a profiles_session_token
+ declare -a profiles_session_init_time
+ profiles_iterator=0
+ profiles_init=0
+
+ # ugly hack to relate different values because
+ # macOS *still* does not provide bash 4.x by default,
+ # so associative arrays aren't available
+ # NOTE: this pass is quick as no aws calls are done
+ while IFS='' read -r line || [[ -n "$line" ]]; do
+ if [[ "$line" =~ ^\[(.*)\].* ]]; then
+ _ret="${BASH_REMATCH[1]}"
+
+ if [[ $profiles_init -eq 0 ]]; then
+ profiles_ident[$profiles_iterator]=$_ret
+ profiles_init=1
+ fi
+
+ if [[ "$_ret" != "" ]] &&
+ [[ ! "$_ret" =~ -mfasession$ ]]; then
+
+ profiles_type[$profiles_iterator]="profile"
+ else
+ profiles_type[$profiles_iterator]="session"
+ fi
+
+ if [[ "${profiles_ident[$profiles_iterator]}" != "$_ret" ]]; then
+ ((profiles_iterator++))
+ profiles_ident[$profiles_iterator]=$_ret
+ fi
+ fi
+
+ [[ "$line" =~ ^aws_access_key_id[[:space:]]*=[[:space:]]*(.*)$ ]] &&
+ profiles_key_id[$profiles_iterator]="${BASH_REMATCH[1]}"
+
+ [[ "$line" =~ ^aws_secret_access_key[[:space:]]*=[[:space:]]*(.*)$ ]] &&
+ profiles_secret_key[$profiles_iterator]="${BASH_REMATCH[1]}"
+
+ [[ "$line" =~ ^aws_session_token[[:space:]]*=[[:space:]]*(.*)$ ]] &&
+ profiles_session_token[$profiles_iterator]="${BASH_REMATCH[1]}"
+
+ [[ "$line" =~ ^aws_session_init_time[[:space:]]*=[[:space:]]*(.*)$ ]] &&
+ profiles_session_init_time[$profiles_iterator]=${BASH_REMATCH[1]}
+
+ done < "$CREDFILE"
+
+ # init arrays to hold ident<->mfasec detail
+ declare -a confs_ident
+ declare -a confs_region
+ declare -a confs_output
+ declare -a confs_mfasec
+ confs_init=0
+ confs_iterator=0
+
+ # read in the config file params
+ while IFS='' read -r line || [[ -n "$line" ]]; do
+
+ if [[ "$line" =~ ^\[[[:space:]]*profile[[:space:]]*(.*)[[:space:]]*\].* ]]; then
+ _ret="${BASH_REMATCH[1]}"
+
+ if [[ $confs_init -eq 0 ]]; then
+ confs_ident[$confs_iterator]=$_ret
+ confs_init=1
+ elif [[ "${confs_ident[$confs_iterator]}" != "$_ret" ]]; then
+ ((confs_iterator++))
+ confs_ident[$confs_iterator]=$_ret
+ fi
+ fi
+
+ [[ "$line" =~ ^[[:space:]]*region[[:space:]]*=[[:space:]]*(.*)$ ]] &&
+ confs_region[$confs_iterator]=${BASH_REMATCH[1]}
+
+ [[ "$line" =~ ^[[:space:]]*output[[:space:]]*=[[:space:]]*(.*)$ ]] &&
+ confs_output[$confs_iterator]=${BASH_REMATCH[1]}
+
+ [[ "$line" =~ ^[[:space:]]*mfasec[[:space:]]*=[[:space:]]*(.*)$ ]] &&
+ confs_mfasec[$confs_iterator]=${BASH_REMATCH[1]}
+
+ done < "$CONFFILE"
+
+ # make sure environment has either no config or a functional config
+ # before we proceed
+ checkEnvSession
+
+ # get default region and output format
+ # (since at least one profile should exist at this point, and one should be selected)
+ default_region=$(aws configure get region --profile default)
+ [[ "$DEBUG" == "true" ]] && echo -e "\\n${Cyan}${On_Black}result for 'aws configure get region --profile default':\\n${ICyan}${default_region}${Color_Off}\\n\\n"
+
+ default_output=$(aws configure get output --profile default)
+ [[ "$DEBUG" == "true" ]] && echo -e "\\n${Cyan}${On_Black}result for 'aws configure get output --profile default':\\n${ICyan}${default_output}${Color_Off}\\n\\n"
+
+ if [[ "$default_region" == "" ]]; then
+ echo
+ echo -e "${BIWhite}${On_Black}THE DEFAULT REGION HAS NOT BEEN CONFIGURED.${Color_Off}\\nPlease set the default region in '$CONFFILE', for example like so:\\naws configure set region \"us-east-1\""
+ echo
+ exit 1
+ fi
+
+ if [[ "$default_output" == "" ]]; then
+ aws configure set output "table"
+ fi
+
+ echo
+
+ if [[ "$AWS_ACCESS_KEY_ID" != "" ]]; then
+ current_aws_access_key_id="${AWS_ACCESS_KEY_ID}"
+ else
+ current_aws_access_key_id="$(aws configure get aws_access_key_id)"
+ [[ "$DEBUG" == "true" ]] && echo -e "\\n${Cyan}result for: 'aws configure get aws_access_key_id':\\n${ICyan}${current_aws_access_key_id}${Color_Off}\\n\\n"
+ fi
+
+ idxLookup idx profiles_key_id[@] "$current_aws_access_key_id"
+
+ if [[ $idx != "" ]]; then
+ currently_selected_profile_ident="${profiles_ident[$idx]}"
+ currently_selected_profile_ident_printable="'${profiles_ident[$idx]}'"
+ else
+ if [[ "${PRECHECK_AWS_PROFILE}" != "" ]]; then
+ currently_selected_profile_ident="${PRECHECK_AWS_PROFILE}"
+ currently_selected_profile_ident_printable="'${PRECHECK_AWS_PROFILE}' [transient]"
+ else
+ currently_selected_profile_ident=""
+ currently_selected_profile_ident_printable="transient/unknown"
+ fi
+ fi
+
+ process_user_arn="$(aws sts get-caller-identity --query 'Arn' --output text 2>&1)"
+ [[ "$DEBUG" == "true" ]] && echo -e "\\n${Cyan}${On_Black}result for: 'aws sts get-caller-identity --query 'Arn' --output text':\\n${ICyan}${process_user_arn}${Color_Off}\\n\\n"
+
+ if [[ "$process_user_arn" =~ 'error occurred' ]]; then
+ continue_maybe "invalid"
+
+ currently_selected_profile_ident_printable="'default'"
+ process_user_arn="$(aws sts get-caller-identity --query 'Arn' --output text 2>&1)"
+ [[ "$DEBUG" == "true" ]] && echo -e "\\n${Cyan}${On_Black}result for: 'aws sts get-caller-identity --query 'Arn' --output text' \\(after profile reset\\):\\n${ICyan}${process_user_arn}${Color_Off}\\n\\n"
+
+ [[ "$process_user_arn" =~ ([^/]+)$ ]] &&
+ process_username="${BASH_REMATCH[1]}"
+ fi
+
+ # this bails out on errors
+ checkAWSErrors _is_error "true" "$process_user_arn" "$currently_selected_profile_ident_printable"
+
+ # we didn't bail out; continuing...
+ # get the actual username and user account
+ # (username may be different from the arbitrary profile ident)
+ if [[ "$process_user_arn" =~ ([[:digit:]]+):user.*/([^/]+)$ ]]; then
+ profile_user_acc="${BASH_REMATCH[1]}"
+ process_username="${BASH_REMATCH[2]}"
+ fi
+
+ getAccountAlias _ret
+ if [[ "${_ret}" != "" ]]; then
+ account_alias_if_any="@${_ret}"
+ else
+ account_alias_if_any="@${profile_user_acc}"
+ fi
+
+ # we didn't bail out; continuing...
+ echo "Executing this script as the AWS/IAM user $process_username $account_alias_if_any (profile $currently_selected_profile_ident_printable)."
+
+ echo
+
+ # declare the arrays for credentials loop
+ declare -a cred_profiles
+ declare -a cred_profile_status
+ declare -a cred_profile_user
+ declare -a cred_profile_arn
+ declare -a cred_profile_account_alias
+ declare -a profile_region
+ declare -a profile_output
+ declare -a mfa_profiles
+ declare -a mfa_arns
+ declare -a mfa_profile_status
+ declare -a mfa_mfasec
+ cred_profilecounter=0
+
+ echo -ne "${BIWhite}${On_Black}Please wait"
+
+ # read the credentials file
+ while IFS='' read -r line || [[ -n "$line" ]]; do
+
+ [[ "$line" =~ ^\[(.*)\].* ]] &&
+ profile_ident="${BASH_REMATCH[1]}"
+
+ # transfer possible MFA mfasec from config array
+ idxLookup idx confs_ident[@] "$profile_ident"
+ if [[ $idx != "" ]]; then
+ mfa_mfasec[$cred_profilecounter]=${confs_mfasec[$idx]}
+ fi
+
+ # only process if profile identifier is present,
+ # and if it's not a mfasession profile
+ # (mfasession profiles have '-mfasession' postfix)
+ if [[ "$profile_ident" != "" ]] &&
+ [[ ! "$profile_ident" =~ -mfasession$ ]]; then
+
+ # store this profile ident
+ cred_profiles[$cred_profilecounter]="$profile_ident"
+
+ # store this profile region and output format
+ profile_region[$cred_profilecounter]=$(aws configure get region --profile "$profile_ident")
+ [[ "$DEBUG" == "true" ]] && echo -e "\\n${Cyan}${On_Black}result for: 'aws configure get region --profile \"$profile_ident\"':\\n${ICyan}${profile_region[$cred_profilecounter]}${Color_Off}\\n\\n"
+ profile_output[$cred_profilecounter]=$(aws configure get output --profile "$profile_ident")
+ [[ "$DEBUG" == "true" ]] && echo -e "\\n${Cyan}${On_Black}result for: 'aws configure get output --profile \"$profile_ident\"':\\n${ICyan}${profile_output[$cred_profilecounter]}${Color_Off}\\n\\n"
+
+ # get the user ARN; this should be always
+ # available for valid profiles
+ user_arn="$(aws sts get-caller-identity --profile "$profile_ident" --query 'Arn' --output text 2>&1)"
+ [[ "$DEBUG" == "true" ]] && echo -e "\\n${Cyan}${On_Black}result for: 'aws sts get-caller-identity --profile \"$profile_ident\" --query 'Arn' --output text':\\n${ICyan}${user_arn}${Color_Off}\\n\\n"
+
+ if [[ "$user_arn" =~ ^arn:aws ]]; then
+ cred_profile_arn[$cred_profilecounter]=$user_arn
+ else
+ # must be a bad profile
+ cred_profile_arn[$cred_profilecounter]=""
+ fi
+
+ # get the actual username
+ # (may be different from the arbitrary profile ident)
+ [[ "$user_arn" =~ ([^/]+)$ ]] &&
+ profile_username="${BASH_REMATCH[1]}"
+ if [[ "$profile_username" =~ 'error occurred' ]]; then
+ cred_profile_user[$cred_profilecounter]=""
+ else
+ cred_profile_user[$cred_profilecounter]="$profile_username"
+ fi
+
+ # get the account alias (if any) for the user/profile
+ getAccountAlias _ret "$profile_ident"
+ cred_profile_account_alias[$cred_profilecounter]="${_ret}"
+
+ # find the MFA session for the current profile if one exists ("There can be only one")
+ # (profile with profilename + "-mfasession" postfix)
+ while IFS='' read -r line || [[ -n "$line" ]]; do
+ [[ "$line" =~ \[(${profile_ident}-mfasession)\]$ ]] &&
+ mfa_profile_ident="${BASH_REMATCH[1]}"
+ done < "$CREDFILE"
+ mfa_profiles[$cred_profilecounter]="$mfa_profile_ident"
+
+ # check to see if this profile has access currently
+ # (this is not 100% as it depends on the defined IAM access;
+ # however if MFA enforcement is set, this should produce
+ # a reasonably reliable result)
+ profile_check="$(aws iam get-user --profile "$profile_ident" --query 'User.Arn' --output text 2>&1)"
+ [[ "$DEBUG" == "true" ]] && echo -e "\\n${Cyan}${On_Black}result for: 'aws iam get-user --profile \"$profile_ident\" --query 'User.Arn' --output text':\\n${ICyan}${profile_check}${Color_Off}\\n\\n"
+
+ if [[ "$profile_check" =~ ^arn:aws ]]; then
+ cred_profile_status[$cred_profilecounter]="OK"
+ else
+ cred_profile_status[$cred_profilecounter]="LIMITED"
+ fi
+
+ # get MFA ARN if available
+ # (obviously not available if a MFA device
+ # isn't configured for the profile)
+ mfa_arn="$(aws iam list-mfa-devices \
+ --profile "$profile_ident" \
+ --user-name "${cred_profile_user[$cred_profilecounter]}" \
+ --output text \
+ --query 'MFADevices[].SerialNumber' 2>&1)"
+
+ [[ "$DEBUG" == "true" ]] && echo -e "\\n${Cyan}${On_Black}result for: 'aws iam list-mfa-devices --profile \"$profile_ident\" --user-name \"${cred_profile_user[$cred_profilecounter]}\" --query 'MFADevices[].SerialNumber' --output text':\\n${ICyan}${mfa_arn}${Color_Off}\\n\\n"
+
+ if [[ "$mfa_arn" =~ ^arn:aws ]]; then
+ mfa_arns[$cred_profilecounter]="$mfa_arn"
+ else
+ mfa_arns[$cred_profilecounter]=""
+ fi
+
+ # If an existing MFA profile was found, check its status
+ # (uses timestamps first if available; falls back to
+ # less reliable get-user command -- its output depends
+ # on IAM policy settings, and while it's usually accurate
+ # it's still not reliable)
+ if [[ "$mfa_profile_ident" != "" ]]; then
+
+ getInitTime _ret_timestamp "$mfa_profile_ident"
+ getDuration _ret_duration "$mfa_profile_ident"
+ getRemaining _ret_remaining "${_ret_timestamp}" "${_ret_duration}"
+
+ if [[ ${_ret_remaining} -eq 0 ]]; then
+ # session has expired
+
+ mfa_profile_status[$cred_profilecounter]="EXPIRED"
+ elif [[ ${_ret_remaining} -gt 0 ]]; then
+ # session time remains
+
+ getPrintableTimeRemaining _ret "${_ret_remaining}"
+ mfa_profile_status[$cred_profilecounter]="${_ret} remaining"
+ elif [[ ${_ret_remaining} -eq -1 ]]; then
+ # no timestamp; legacy or initialized outside of this utility
+
+ mfa_profile_check="$(aws iam get-user --profile "$mfa_profile_ident" --query 'User.Arn' --output text 2>&1)"
+
+ [[ "$DEBUG" == "true" ]] && echo -e "\\n${Cyan}${On_Black}result for: 'aws iam get-user --profile \"$mfa_profile_ident\" --query 'User.Arn' --output text':\\n${ICyan}${mfa_profile_check}${Color_Off}\\n\\n"
+
+ if [[ "$mfa_profile_check" =~ ^arn:aws ]]; then
+ mfa_profile_status[$cred_profilecounter]="OK"
+ elif [[ "$mfa_profile_check" =~ ExpiredToken ]]; then
+ mfa_profile_status[$cred_profilecounter]="EXPIRED"
+ else
+ mfa_profile_status[$cred_profilecounter]="LIMITED"
+ fi
+ fi
+ fi
+
+ ## DEBUG (enable with DEBUG="true" on top of the file)
+ if [[ "$DEBUG" == "true" ]]; then
+
+ echo
+ echo "PROFILE IDENT: $profile_ident (${cred_profile_status[$cred_profilecounter]})"
+ echo "USER ARN: ${cred_profile_arn[$cred_profilecounter]}"
+ echo "USER NAME: ${cred_profile_user[$cred_profilecounter]}"
+ echo "ACCOUNT ALIAS: ${cred_profile_account_alias[$cred_profilecounter]}"
+ echo "MFA ARN: ${mfa_arns[$cred_profilecounter]}"
+ echo "MFA SESSION CUSTOM LENGTH (MFASEC): ${mfa_mfasec[$cred_profilecounter]}"
+ if [[ "${mfa_profiles[$cred_profilecounter]}" == "" ]]; then
+ echo "MFA PROFILE IDENT:"
+ else
+ echo "MFA PROFILE IDENT: ${mfa_profiles[$cred_profilecounter]} (${mfa_profile_status[$cred_profilecounter]})"
+ fi
+ echo
+ ## END DEBUG
+ else
+ echo -n "."
+ fi
+
+ # erase variables & increase iterator for the next iteration
+ mfa_arn=""
+ user_arn=""
+ account_alias_result=""
+ profile_ident=""
+ profile_check=""
+ profile_username=""
+ mfa_profile_ident=""
+ mfa_profile_check=""
+
+ ((cred_profilecounter++))
+
+ fi
+ done < "$CREDFILE"
+ echo -e "${Color_Off}"
+
+ # select the profile (first, single profile + a possible persistent MFA session)
+ if [[ ${#cred_profiles[@]} == 1 ]]; then
+ echo
+ [[ "${cred_profile_user[0]}" != "" ]] && prcpu="${cred_profile_user[0]}" || prcpu="unknown -- a bad profile?"
+ [[ "${cred_profile_account_alias[0]}" != "" ]] && prcpaa="@${cred_profile_account_alias[0]}" || prcpaa=""
+ echo -e "${Green}${On_Black}You have one configured profile: ${BIGreen}${cred_profiles[0]} ${Green}(IAM: ${prcpu}${prcpaa})${Color_Off}"
+
+ if [[ "${mfa_arns[0]}" != "" ]]; then
+ echo -en ".. and its virtual MFA device is already enabled.\\n\\n${BIWhite}${On_Black}Do you want to disable its vMFAd? Y/N${Color_Off} "
+
+ while :
+ do
+ read -s -n 1 -r
+ if [[ $REPLY =~ ^[Yy]$ ]]; then
+ selprofile="-1"
+ break;
+ elif [[ $REPLY =~ ^[Nn]$ ]]; then
+ echo -e "\\n\\nA vMFAd not disabled/detached. Exiting.\\n"
+ exit 1
+ break;
+ fi
+ done
+ echo
+
+ else
+ echo -en ".. but it doesn't have a virtual MFA device attached/enabled.\\n\\n${BIWhite}${On_Black}Do you want to attach/enable a vMFAd? Y/N${Color_Off} "
+ while :
+ do
+ read -s -n 1 -r
+ if [[ $REPLY =~ ^[Yy]$ ]]; then
+ selprofile="-1"
+ break;
+ elif [[ $REPLY =~ ^[Nn]$ ]]; then
+ echo -e "\\n\\nvA MFAd not attached/enabled. Exiting.\\n"
+ exit 1
+ break;
+ fi
+ done
+ echo
+
+ fi
+
+ else # more than 1 profile
+
+ declare -a iter_to_profile
+
+ # create the profile selections for "no vMFAd configured" and "vMFAd enabled"
+ echo
+ echo -e "${BIWhite}${On_Red} AWS PROFILES WITH NO ATTACHED/ENABLED VIRTUAL MFA DEVICE (vMFAd): ${Color_Off}"
+ echo -e " ${BIWhite}${On_Black}Select a profile to which you want to attach/enable a vMFAd.${Color_Off}\\n A new vMFAd is created/initialized if one doesn't exist."
+ echo
+ SELECTR=0
+ ITER=1
+ for i in "${cred_profiles[@]}"
+ do
+ if [[ "${mfa_arns[$SELECTR]}" == "" ]]; then
+ # no vMFAd configured
+ [[ "${cred_profile_user[$SELECTR]}" != "" ]] && prcpu="${cred_profile_user[$SELECTR]}" || prcpu="unknown -- a bad profile?"
+ [[ "${cred_profile_account_alias[$SELECTR]}" != "" ]] && prcpaa=" @${cred_profile_account_alias[$SELECTR]}" || prcpaa=""
+ echo -en "${BIWhite}${On_Black}${ITER}: $i${Color_Off} (IAM: ${prcpu}${prcpaa})\\n\\n"
+
+ # add to the translation table for the selection
+ iter_to_profile[$ITER]=$SELECTR
+ ((ITER++))
+ fi
+ ((SELECTR++))
+ done
+
+ echo
+ echo -e "${BIWhite}${On_DGreen} AWS PROFILES WITH ACTIVE (ENABLED) VIRTUAL MFA DEVICE (vMFAd): ${Color_Off}"
+ echo -e " ${BIWhite}${On_Black}Select a profile whose vMFAd you want to disable/detach.${Color_Off}\\n Once detached, you'll have the option to delete the vMFAd.\\n NOTE: A profile must have an active MFA session to disable!"
+ echo
+ SELECTR=0
+ for i in "${cred_profiles[@]}"
+ do
+ if [[ "${mfa_arns[$SELECTR]}" != "" ]]; then
+ # vMFAd configured
+ [[ "${cred_profile_user[$SELECTR]}" != "" ]] && prcpu="${cred_profile_user[$SELECTR]}" || prcpu="unknown -- a bad profile?"
+ [[ "${cred_profile_account_alias[$SELECTR]}" != "" ]] && prcpaa=" @${cred_profile_account_alias[$SELECTR]}" || prcpaa=""
+ echo -en "${BIWhite}${On_Black}${ITER}: $i${Color_Off} (IAM: ${prcpu}${prcpaa})\\n\\n"
+ # add to the translation table for the selection
+ iter_to_profile[$ITER]=$SELECTR
+ ((ITER++))
+ fi
+ ((SELECTR++))
+ done
+
+ # prompt for profile selection
+ echo -en "\\n${BIWhite}${On_Black}SELECT A PROFILE BY THE NUMBER:${Color_Off} "
+ read -r selprofile
+
+ fi # end profile selection
+
+ # process the selection
+ if [[ "$selprofile" == "-1" ]]; then
+ selprofile="1"
+ fi
+
+ if [[ "$selprofile" != "" ]]; then
+ # capture the numeric part of the selection
+ [[ $selprofile =~ ^([[:digit:]]+) ]] &&
+ selprofile_check="${BASH_REMATCH[1]}"
+
+ if [[ "$selprofile_check" != "" ]]; then
+ # if the numeric selection was found,
+ # translate it to the array index and validate
+ profilecount=${#cred_profiles[@]}
+ if [[ $selprofile_check -gt $profilecount ||
+ $selprofile_check -lt 1 ]]; then
+
+ # a selection outside of the existing range was specified
+ echo -e "${BIRed}${On_Black}There is no profile with the ID '${selprofile}'.${Color_Off}"
+ echo
+ exit 1
+ else
+ translated_selprofile=${iter_to_profile[$selprofile]}
+ fi
+
+ # a base profile was selected (sessions are not considered)
+ if [[ $selprofile =~ ^[[:digit:]]+$ ]]; then
+ echo
+ final_selection="${cred_profiles[$translated_selprofile]}"
+
+ echo -n "Preparing to "
+ idxLookup idx cred_profiles[@] "$final_selection"
+ if [[ "${mfa_arns[$idx]}" == "" ]]; then
+ echo "enable the vMFAd for the profile..."
+ echo
+
+ selected_profile_arn=${cred_profile_arn[idx]}
+
+ if [[ "$selected_profile_arn" =~ ^arn:aws:iam::([[:digit:]]+):user.*/([^/]+)$ ]]; then
+ aws_account_id="${BASH_REMATCH[1]}"
+ aws_iam_user="${BASH_REMATCH[2]}"
+ else
+ echo -e "${BIRed}${On_Black}Could not acquire AWS account ID or current IAM user name. A bad profile? Cannot continue.${Color_Off}"
+ echo
+ exit 1
+ fi
+
+ available_user_vmfad=$(aws iam list-virtual-mfa-devices \
+ --profile "${final_selection}" \
+ --assignment-status Unassigned \
+ --output text \
+ --query 'VirtualMFADevices[?SerialNumber==`arn:aws:iam::'"${aws_account_id}"':mfa/'"${aws_iam_user}"'`].SerialNumber' 2>&1)
+
+ if [[ "$DEBUG" == "true" ]]; then
+ echo -e "\\n${Cyan}${On_Black}result for: 'aws iam list-virtual-mfa-devices --profile \"${final_selection}\" --assignment-status Unassigned --query 'VirtualMFADevices[?SerialNumber==´arn:aws:iam::${aws_account_id}:mfa/${aws_iam_user}´].SerialNumber' --output text':\\n${ICyan}${available_user_vmfad}${Color_Off}\\n\\n"
+ fi
+
+ existing_mfa_deleted="false"
+ if [[ "$available_user_vmfad" =~ 'error occurred' ]]; then
+ echo -e "${BIRed}${On_Black}Could not execute list-virtual-mfa-devices. Cannot continue.${Color_Off}"
+ echo
+ exit 1
+ elif [[ "$available_user_vmfad" != "" ]]; then
+ unassigned_vmfad_preexisted="true"
+
+ echo -e "${Green}${On_Black}Unassigned vMFAd found for the profile:\\n${BIGreen}$available_user_vmfad${Color_Off}\\n"
+ echo -en "${BIWhite}${On_Black}Do you have access to the above vMFAd on your GA/Authy device?${Color_Off}\\nNOTE: 'No' will delete the vMFAd and create a new one\\n(thus voiding a possible existing GA/Authy entry), so\\nmake your choice: ${BIWhite}${On_Black}Y/N${Color_Off} "
+
+ while :
+ do
+ read -s -n 1 -r
+ if [[ $REPLY =~ ^[Yy]$ ]]; then
+ break;
+ elif [[ $REPLY =~ ^[Nn]$ ]]; then
+ mfa_deletion_result=$(aws iam delete-virtual-mfa-device \
+ --profile "${final_selection}" \
+ --serial-number "${available_user_vmfad}" 2>&1)
+
+ if [[ "$DEBUG" == "true" ]]; then
+ echo -e "\\n${Cyan}${On_Black}result for: 'aws iam delete-virtual-mfa-device --profile \"${final_selection}\" --serial-number \"${available_user_vmfad}\"':\\n${ICyan}${mfa_deletion_result}${Color_Off}\\n\\n"
+ fi
+
+ # this bails out on errors
+ checkAWSErrors _is_error "true" "$mfa_deletion_result" "$final_selection" "Could not delete the inaccessible vMFAd. Cannot continue!"
+
+ # we didn't bail out; continuing...
+ echo -e "\\n\\nThe old vMFAd has been deleted."
+ existing_mfa_deleted="true"
+ break;
+ fi
+ done
+ fi
+
+ if [[ "$available_user_vmfad" == "" ]] ||
+ [[ "$existing_mfa_deleted" == "true" ]]; then
+ # no vMFAd was found, create new..
+
+ unassigned_vmfad_preexisted="false"
+
+ qr_file_name="${final_selection} vMFAd QRCode.png"
+
+ if [[ "$OS" == "macOS" ]]; then
+ qr_file_target="on your DESKTOP"
+ qr_with_path="${HOME}/Desktop/${qr_file_name}"
+ elif [[ -d $HOME/Desktop ]]; then
+ qr_file_target="on your DESKTOP"
+ qr_with_path="${HOME}/Desktop/${qr_file_name}"
+ else
+ qr_file_target="in your HOME DIRECTORY ($HOME)"
+ qr_with_path="${HOME}/${qr_file_name}"
+ fi
+
+ echo
+ echo "No available vMFAd found; creating new..."
+ echo
+ vmfad_creation_status=$(aws iam create-virtual-mfa-device \
+ --profile "${final_selection}" \
+ --virtual-mfa-device-name "${aws_iam_user}" \
+ --outfile "${qr_with_path}" \
+ --bootstrap-method QRCodePNG 2>&1)
+
+ if [[ "$DEBUG" == "true" ]]; then
+ echo -e "\\n${Cyan}${On_Black}result for: 'aws iam create-virtual-mfa-device --profile \"${final_selection}\" --virtual-mfa-device-name \"${aws_iam_user}\" --outfile \"${qr_with_path}\" --bootstrap-method QRCodePNG':\\n${ICyan}${vmfad_creation_status}${Color_Off}\\n\\n"
+ fi
+
+ # this bails out on errors
+ checkAWSErrors _is_error "true" "$vmfad_creation_status" "$final_selection" "Could not execute create-virtual-mfa-device. No virtual MFA device to enable. Cannot continue!"
+
+ # we didn't bail out; continuing...
+ echo -e "${BIGreen}${On_Black}A new vMFAd has been created. ${BIWhite}${On_Black}Please scan\\nthe QRCode with Authy to add the vMFAd on\\nyour portable device.${Color_Off}\\n"
+ echo -e "NOTE: The QRCode file, \"${qr_file_name}\",\\nis $qr_file_target!"
+ echo
+ echo -e "${BIWhite}${On_Black}Press 'x' once you have scanned the QRCode to proceed.${Color_Off}"
+ while :
+ do
+ read -s -n 1 -r
+ if [[ $REPLY =~ ^[Xx]$ ]]; then
+ break;
+ fi
+ done
+
+ echo
+ echo -en "NOTE: Anyone who gains possession of the QRCode file\\n can initialize the vMFDd like you just did, so\\n optimally it should not be kept around.\\n\\n${BIWhite}${On_Black}Do you want to delete the QRCode securely? Y/N${Color_Off} "
+
+ while :
+ do
+ read -s -n 1 -r
+ if [[ $REPLY =~ ^[Yy]$ ]]; then
+ rm -fP "${qr_with_path}"
+ echo
+ echo -e "${BIWhite}${On_Black}QRCode file deleted securely.${Color_Off}"
+ break;
+ elif [[ $REPLY =~ ^[Nn]$ ]]; then
+ echo
+ echo -e "${BIWhite}${On_Black}You chose not to delete the vMFAd initializer QRCode;\\nplease store it securely as if it were a password!${Color_Off}"
+ break;
+ fi
+ done
+ echo
+
+ available_user_vmfad=$(aws iam list-virtual-mfa-devices \
+ --profile "${final_selection}" \
+ --assignment-status Unassigned \
+ --output text \
+ --query 'VirtualMFADevices[?SerialNumber==`arn:aws:iam::'"${aws_account_id}"':mfa/'"${aws_iam_user}"'`].SerialNumber' 2>&1)
+
+ if [[ "$DEBUG" == "true" ]]; then
+ echo -e "\\n${Cyan}${On_Black}result for: 'aws iam list-virtual-mfa-devices --profile \"${final_selection}\" --assignment-status Unassigned --query 'VirtualMFADevices[?SerialNumber==´arn:aws:iam::${aws_account_id}:mfa/${aws_iam_user}´].SerialNumber' --output text':\\n${ICyan}${available_user_vmfad}${Color_Off}\\n\\n"
+ fi
+
+ # this bails out on errors
+ checkAWSErrors _is_error "true" "$available_user_vmfad" "$final_selection" "Could not execute list-virtual-mfa-devices. Cannot continue!"
+
+ # we didn't bail out; continuing...
+ fi
+
+ if [[ "$available_user_vmfad" == "" ]]; then
+ # no vMFAd existed, none could be created
+ echo -e "\\n\\n${BIRed}${On_Black}No virtual MFA device to enable. Cannot continue.${Color_Off}"
+ exit 1
+ else
+ [[ "$unassigned_vmfad_preexisted" == "true" ]] && vmfad_source="existing" || vmfad_source="newly created"
+ echo -e "\\n\\nEnabling the $vmfad_source virtual MFA device:\\n$available_user_vmfad\\n"
+ fi
+
+ echo
+ echo -e "${BIWhite}${On_Black}Please enter two consecutively generated authcodes from your\\nGA/Authy app for this profile.${Color_Off} Enter the two six-digit codes\\nseparated by a space (e.g. 123456 456789), then press enter\\nto complete the process.\\n"
+
+ while :
+ do
+ read -p ">>> " -r authcodes
+ if [[ $authcodes =~ ^([[:digit:]][[:digit:]][[:digit:]][[:digit:]][[:digit:]][[:digit:]])[[:space:]]+([[:digit:]][[:digit:]][[:digit:]][[:digit:]][[:digit:]][[:digit:]])$ ]]; then
+ authcode1="${BASH_REMATCH[1]}"
+ authcode2="${BASH_REMATCH[2]}"
+ break;
+ else
+ echo -e "${BIRed}${On_Black}Bad authcodes.${Color_Off} Please enter two consecutively generated six-digit numbers separated by a space."
+ fi
+ done
+
+ echo
+
+ vmfad_enablement_status=$(aws iam enable-mfa-device \
+ --profile "${final_selection}" \
+ --user-name "${aws_iam_user}" \
+ --serial-number "${available_user_vmfad}" \
+ --authentication-code-1 "${authcode1}" \
+ --authentication-code-2 "${authcode2}" 2>&1)
+
+ if [[ "$DEBUG" == "true" ]]; then
+ echo -e "\\n${Cyan}${On_Black}result for: 'aws iam enable-mfa-device --profile \"${final_selection}\" --user-name \"${aws_iam_user}\" --serial-number \"${available_user_vmfad}\" --authentication-code-1 \"${authcode1}\" --authentication-code-2 \"${authcode2}\"':\\n${ICyan}${vmfad_enablement_status}${Color_Off}\\n\\n"
+ fi
+
+ # this bails out on errors
+ checkAWSErrors _is_error "true" "$vmfad_enablement_status" "$final_selection" "Could not enable vMFAd. Cannot continue.\\n${Red}Mistyped authcodes, or wrong/old vMFAd?"
+
+ # we didn't bail out; continuing...
+ echo -e "${BIGreen}${On_Black}vMFAd successfully enabled for the profile '${final_selection}' ${Green}(IAM user name '$aws_iam_user').${Color_Off}"
+ echo -e "${BIGreen}${On_Black}You can now use the 'awscli-mfa.sh' script to start an MFA session for this profile!${Color_Off}"
+ echo
+
+ else
+ echo -e "disable the vMFAd for the profile...\\n"
+
+ transient_mfa_profile_check="$(aws sts get-caller-identity --profile "${final_selection}" --query 'Arn' --output text 2>&1)"
+ [[ "$DEBUG" == "true" ]] && echo -e "\\n${Cyan}${On_Black}result for: 'aws sts get-caller-identity --profile \"${final_selection}\" --query 'Arn' --output text':\\n${ICyan}${transient_mfa_profile_check}${Color_Off}\\n\\n"
+
+ # this bails out on errors
+ checkAWSErrors _is_error "true" "$transient_mfa_profile_check" "transient/unknown" "Could not acquire AWS account ID or current IAM user name. A bad profile? Cannot continue!"
+
+ # we didn't bail out; continuing...
+ if [[ "$transient_mfa_profile_check" =~ ^arn:aws:iam::([[:digit:]]+):user.*/([^/]+)$ ]]; then
+ aws_account_id="${BASH_REMATCH[1]}" # this AWS account
+ aws_iam_user="${BASH_REMATCH[2]}" # IAM user of the (hopefully :-) active MFA session
+ else
+ # .. but so does this, just in case to make sure script exits
+ # if there is nothing to work with
+ echo -e "${BIRed}${On_Black}Could not acquire AWS account ID or current IAM user name. A bad profile? Cannot continue.${Color_Off}\\n"
+ echo
+ exit 1
+ fi
+
+ _ret_remaining="undefined"
+ # First checking the envvars
+ if ( [[ "$PRECHECK_AWS_PROFILE" == "" ]] ||
+ [[ ! "$PRECHECK_AWS_PROFILE" =~ -mfasession$ ]] ) &&
+
+ [[ "$PRECHECK_AWS_SESSION_TOKEN" == "" ]] &&
+ [[ "$PRECHECK_AWS_SESSION_INIT_TIME" == "" ]] &&
+ [[ "$PRECHECK_AWS_SESSION_DURATION" == "" ]]; then
+ # this is an authorized (?) base profile
+
+ if [[ "$currently_selected_profile_ident" == ^${final_selection}$ ]]; then
+ # self w/o MFA
+ for_other_profiles=""
+ else
+ # another base profile
+ for_other_profiles=",\\nand for IAM users other than itself"
+ fi
+
+ echo -en "${BIWhite}${On_Black}A base profile ${currently_selected_profile_ident_printable} (IAM: ${process_username} ${account_alias_if_any})\\nis currently in effect instead of an MFA session for the profile\\nwhose vMFAd you want to disable. Do you want to attempt to disable\\nthe vMFAd with the selected profile (the selected profile must have\\nthe authority to disable a vMFAd without an active MFA session${for_other_profiles}? Y/N${Color_Off} "
+
+ while :
+ do
+ read -s -n 1 -r
+ if [[ $REPLY =~ ^[Yy]$ ]]; then
+ break;
+ elif [[ $REPLY =~ ^[Nn]$ ]]; then
+ echo -e "\\n\\nThe vMFAd not disabled/detached. Exiting.\\n"
+ exit 1
+ break;
+ fi
+ done
+ echo
+
+ elif [[ "$PRECHECK_AWS_PROFILE" =~ ^${final_selection}-mfasession$ ]] &&
+ [[ "$PRECHECK_AWS_SESSION_TOKEN" != "" ]] &&
+ [[ "$PRECHECK_AWS_SESSION_INIT_TIME" != "" ]] &&
+ [[ "$PRECHECK_AWS_SESSION_DURATION" != "" ]]; then
+ # this is a valid in-env MFA profile (an MFA session for the profile being disabled)
+
+ getRemaining _ret_remaining "$PRECHECK_AWS_SESSION_INIT_TIME" "$PRECHECK_AWS_SESSION_DURATION"
+
+ elif [[ "$PRECHECK_AWS_PROFILE" =~ ^${final_selection}-mfasession$ ]] &&
+ [[ "$profiles_idx" != "" ]]; then
+ # this is a valid selected persistent MFA profile
+
+ # find the selected persistent MFA profile's init time if one exists
+ profile_time=${profiles_session_init_time[$profiles_idx]}
+
+ # if the duration for the current profile is not set
+ # (as is usually the case with the mfaprofiles), use
+ # the parent/base profile's duration
+ if [[ "$profile_time" != "" ]]; then
+ getDuration parent_duration "$PRECHECK_AWS_PROFILE"
+ getRemaining _ret_remaining "$profile_time" "$parent_duration"
+ fi
+
+ elif [[ "$PRECHECK_AWS_PROFILE" == "" ]] &&
+ [[ "$PRECHECK_AWS_SESSION_TOKEN" != "" ]] &&
+ [[ "$PRECHECK_AWS_SESSION_INIT_TIME" != "" ]] &&
+ [[ "$PRECHECK_AWS_SESSION_DURATION" != "" ]]; then
+ # this is an unknown/transient in-env profile, the base profile is in $currently_selected_profile_ident
+
+ getRemaining _ret_remaining "$PRECHECK_AWS_SESSION_INIT_TIME" "$PRECHECK_AWS_SESSION_DURATION"
+
+ if [[ ${_ret_remaining} -gt 300 ]]; then
+ # this is an unknown in-env MFA session with at least 5 minutes remaining
+
+ echo -e "The effective profile is an unnamed in-env MFA session with at least 5 minutes remaining.\\n"
+ echo -en "${BIWhite}${On_Black}You are executing this with an MFA session for a profile (${currently_selected_profile_ident_printable})\\nother than the one whose vMFAd you're trying to disable.\\nThe effective MFA profile must have the authority to disable\\nvMFAd for IAM users other than itself. Do you want to proceed? Y/N "
+ yesno _ret
+ if [[ "$_ret" == "no" ]]; then
+ echo -e "${Color_Off}\\n\\nThe vMFAd not disabled/detached. Exiting.\\n"
+ exit 1
+ fi
+
+ else
+ echo -e "${BIRed}${On_Black}You tried to execute this with an unnamed in-env MFA session (${currently_selected_profile_ident_printable})\\nthat is near expiration or that has expired. Cannot continue.${Color_Off}\\n"
+ print_mfa_notice
+ exit 1
+ fi
+
+ elif [[ "$currently_selected_profile_ident" != ^${final_selection}-mfasession$ ]] &&
+ [[ "$currently_selected_profile_ident" =~ -mfasession$ ]]; then
+ # some other profile's mfasession is in effect
+
+ if [[ "$PRECHECK_AWS_SESSION_TOKEN" != "" ]] &&
+ [[ "$PRECHECK_AWS_SESSION_INIT_TIME" != "" ]] &&
+ [[ "$PRECHECK_AWS_SESSION_DURATION" != "" ]]; then
+
+ # this is an in-env MFA session (AWS_PROFILE needs not
+ # be checked because the MFA session name is obviously
+ # known and so MFA_PROFILE MUST BE SET)
+
+ getRemaining _ret_remaining "$PRECHECK_AWS_SESSION_INIT_TIME" "$PRECHECK_AWS_SESSION_DURATION"
+ else
+ # this is a persistent MFA session (we know this
+ # because the name of the profile is known, hence
+ # an in-env MFA session must be named with AWS_PROFILE,
+ # or AWS_PROFILE must refere to a persistent profile)
+
+ # find the selected persistent MFA profile's init time if one exists
+ profile_time=${profiles_session_init_time[$profiles_idx]}
+
+ # since the duration for the current profile is not set
+ # (as is the case with the mfaprofiles), use the parent/base
+ # profile's duration
+ if [[ "$profile_time" != "" ]]; then
+ getDuration parent_duration "$currently_selected_profile_ident"
+ getRemaining _ret_remaining "$profile_time" "$parent_duration"
+ else
+ _ret_remaining=0
+ fi
+
+ fi
+
+ echo -en "${BIWhite}${On_Black}An MFA session for a profile (${currently_selected_profile_ident_printable})\\nother than the one whose vMFAd you're trying to disable is in effect.\\nThe selected MFA profile must have the authority to disable vMFAd for\\nIAM users other than itself. Do you want to proceed? Y/N "
+ yesno _ret
+ if [[ "$_ret" == "no" ]]; then
+ echo -e "${Color_Off}\\n\\nThe vMFAd not disabled/detached. Exiting.\\n"
+ exit 1
+ fi
+ fi
+
+ # deactivation process
+ if [[ "${_ret_remaining}" != "undefined" && ${_ret_remaining} -gt 120 || # at least 120 seconds of the session remains
+ "${_ret_remaining}" == "undefined" ]]; then # .. or we try with a base profile
+ # the profile is not defined below because an active MFA session or an otherwise authorized profile must be selected/active
+
+ vmfad_deactivation_result=$(aws iam deactivate-mfa-device \
+ --user-name "${aws_iam_user}" \
+ --serial-number "arn:aws:iam::${aws_account_id}:mfa/${aws_iam_user}" 2>&1)
+
+ [[ "$DEBUG" == "true" ]] && echo -e "\\n${Cyan}${On_Black}result for: 'aws iam deactivate-mfa-device --profile \"${final_selection}\" --user-name \"${aws_iam_user}\" --serial-number \"arn:aws:iam::${aws_account_id}:mfa/${aws_iam_user}\"':\\n${ICyan}${vmfad_deactivation_result}${Color_Off}\\n\\n"
+
+ # this bails out on errors
+ checkAWSErrors _is_error "false" "$vmfad_deactivation_result" "$final_selection" "Could not disable/detach vMFAd for the profile '${final_selection}'. Cannot continue!"
+
+ if [[ "${_is_error}" == "true" ]]; then
+ print_mfa_notice
+ exit 1
+ fi
+
+ # we didn't bail out; continuing...
+ echo
+ echo -e "${BIGreen}${On_Black}\\nvMFAd disabled/detached for the profile '${final_selection}'.${Color_Off}"
+ echo
+
+ echo -en "${BIWhite}${On_Black}Do you want to ${BIRed}DELETE${BIWhite} the disabled/detached vMFAd? Y/N${Color_Off} "
+ while :
+ do
+ read -s -n 1 -r
+ if [[ $REPLY =~ ^[Yy]$ ]]; then
+ vmfad_delete_result=$(aws iam delete-virtual-mfa-device \
+ --profile "${final_selection}" \
+ --serial-number "arn:aws:iam::${aws_account_id}:mfa/${aws_iam_user}")
+
+ [[ "$DEBUG" == "true" ]] && echo -e "\\n${Cyan}${On_Black}result for: 'aws iam delete-virtual-mfa-device --profile \"${final_selection}\" --serial-number \"arn:aws:iam::${aws_account_id}:mfa/${aws_iam_user}\"':\\n${ICyan}${vmfad_delete_result}${Color_Off}\\n\\n"
+
+ # this bails out on errors
+ checkAWSErrors _is_error "true" "$vmfad_delete_result" "$final_selection" "Could not delete vMFAd for the profile '${final_selection}'. Cannot continue!"
+
+ # we didn't bail out; continuing...
+ echo -e "\\n${BIGreen}${On_Black}vMFAd deleted for the profile '${final_selection}'.${Color_Off}"
+ echo
+ echo "To set up a new vMFAd, run this script again."
+ echo
+ break;
+ elif [[ $REPLY =~ ^[Nn]$ ]]; then
+ echo -e "\\n\\n${BIWhite}${On_Black}The following vMFAd was disabled/detached, but not deleted:${Color_Off}\\narn:aws:iam::${aws_account_id}:mfa/${aws_iam_user}\\n\\nNOTE: Detached vMFAd's may be automatically deleted after some time.\\n"
+ exit 1
+ break;
+ fi
+ done
+
+ else
+ echo -e "\\n${BIRed}${On_Black}The MFA session for the profile \"${final_selection}\" has expired.${Color_Off}\\n"
+ print_mfa_notice
+ echo
+ exit 1
+ fi
+ exit 1
+ fi
+ else
+ # non-acceptable characters were present in the selection
+ echo -e "${BIRed}${On_Black}There is no profile '${selprofile}'.${Color_Off}"
+ echo
+ exit 1
+ fi
+ else
+ # no numeric part in selection
+ echo -e "${BIRed}${On_Black}There is no profile '${selprofile}'.${Color_Off}"
+ echo
+ exit 1
+ fi
+ else
+ # empty selection
+ echo -e "${BIRed}${On_Black}There is no profile '${selprofile}'.${Color_Off}"
+ echo
+ exit 1
+ fi
+fi
diff --git a/awscli-mfa/example-MFA-enforcement-policy.txt b/awscli-mfa/example-MFA-enforcement-policy.txt
new file mode 100644
index 0000000..d3f83f1
--- /dev/null
+++ b/awscli-mfa/example-MFA-enforcement-policy.txt
@@ -0,0 +1,162 @@
+{
+ "Version": "2012-10-17",
+ "Statement": [
+ {
+ "Sid": "AllowAllUsersToListAccountAliases",
+ "Effect": "Allow",
+ "Action": [
+ "iam:ListAccountAliases"
+ ],
+ "Resource": [
+ "*"
+ ]
+ },
+ {
+ "Sid": "AllowAllUsersToListTheAvailbleMFADevices",
+ "Effect": "Allow",
+ "Action": [
+ "iam:ListVirtualMFADevices"
+ ],
+ "Resource": [
+ "arn:aws:iam::REPLACE-WITH-YOUR-AWS-ACCOUNT-ID:mfa/*"
+ ]
+ },
+ {
+ "Sid": "AllowAllUsersToListAccounts",
+ "Effect": "Allow",
+ "Action": [
+ "iam:GetAccountPasswordPolicy",
+ "iam:ListUsers"
+ ],
+ "Resource": [
+ "arn:aws:iam::REPLACE-WITH-YOUR-AWS-ACCOUNT-ID:user/*"
+ ]
+ },
+ {
+ "Sid": "AllowAllUsersToGetRole",
+ "Effect": "Allow",
+ "Action": [
+ "iam:GetRole"
+ ],
+ "Resource": [
+ "arn:aws:iam::REPLACE-WITH-YOUR-AWS-ACCOUNT-ID:role/*"
+ ]
+ },
+ {
+ "Sid": "AllowIndividualUserToSeeTheirAccountInformationAndCreateAccessKey",
+ "Effect": "Allow",
+ "Action": [
+ "iam:ChangePassword",
+ "iam:CreateLoginProfile",
+ "iam:DeleteLoginProfile",
+ "iam:GetAccountPasswordPolicy",
+ "iam:GetAccountSummary",
+ "iam:GetLoginProfile",
+ "iam:UpdateLoginProfile"
+ ],
+ "Resource": [
+ "arn:aws:iam::REPLACE-WITH-YOUR-AWS-ACCOUNT-ID:user/${aws:username}"
+ ]
+ },
+ {
+ "Sid": "AllowIndividualUserToListTheirMFA",
+ "Effect": "Allow",
+ "Action": [
+ "iam:ListMFADevices"
+ ],
+ "Resource": [
+ "arn:aws:iam::REPLACE-WITH-YOUR-AWS-ACCOUNT-ID:mfa/${aws:username}",
+ "arn:aws:iam::REPLACE-WITH-YOUR-AWS-ACCOUNT-ID:user/${aws:username}"
+ ]
+ },
+ {
+ "Sid": "AllowIndividualUserToManageTheirMFA",
+ "Effect": "Allow",
+ "Action": [
+ "iam:CreateVirtualMFADevice",
+ "iam:DeleteVirtualMFADevice",
+ "iam:EnableMFADevice",
+ "iam:ResyncMFADevice"
+ ],
+ "Resource": [
+ "arn:aws:iam::REPLACE-WITH-YOUR-AWS-ACCOUNT-ID:mfa/${aws:username}",
+ "arn:aws:iam::REPLACE-WITH-YOUR-AWS-ACCOUNT-ID:user/${aws:username}"
+ ]
+ },
+ {
+ "Sid": "DenyEverythingExceptForBelowUnlessMFAd",
+ "Effect": "Deny",
+ "NotAction": [
+ "iam:ChangePassword",
+ "iam:CreateAccessKey",
+ "iam:CreateLoginProfile",
+ "iam:CreateVirtualMFADevice",
+ "iam:DeleteLoginProfile",
+ "iam:DeleteVirtualMFADevice",
+ "iam:EnableMFADevice",
+ "iam:GetAccountPasswordPolicy",
+ "iam:GetAccountSummary",
+ "iam:GetLoginProfile",
+ "iam:GetRole",
+ "iam:ListAccessKeys",
+ "iam:ListAccountAliases",
+ "iam:ListMFADevices",
+ "iam:ListUsers",
+ "iam:ListVirtualMFADevices",
+ "iam:ResyncMFADevice",
+ "iam:UpdateLoginProfile"
+ ],
+ "Resource": "*",
+ "Condition": {
+ "NumericGreaterThanIfExists": {
+ "aws:MultiFactorAuthAge": "32400"
+ }
+ }
+ },
+ {
+ "Sid": "AllowBelowWhenMFAd",
+ "Effect": "Allow",
+ "Action": [
+ "iam:GetUser",
+ "iam:DeactivateMFADevice"
+ ],
+ "Resource": [
+ "arn:aws:iam::REPLACE-WITH-YOUR-AWS-ACCOUNT-ID:mfa/${aws:username}",
+ "arn:aws:iam::REPLACE-WITH-YOUR-AWS-ACCOUNT-ID:user/${aws:username}"
+ ],
+ "Condition": {
+ "NumericLessThanIfExists": {
+ "aws:MultiFactorAuthAge": "32400"
+ }
+ }
+ },
+ {
+ "Sid": "DenyIamAccessToOtherAccountsUnlessMFAd",
+ "Effect": "Deny",
+ "Action": [
+ "iam:ChangePassword",
+ "iam:CreateAccessKey",
+ "iam:CreateLoginProfile",
+ "iam:CreateVirtualMFADevice",
+ "iam:DeactivateMFADevice",
+ "iam:DeleteLoginProfile",
+ "iam:DeleteVirtualMFADevice",
+ "iam:EnableMFADevice",
+ "iam:GetAccountPasswordPolicy",
+ "iam:GetLoginProfile",
+ "iam:ListAccessKeys",
+ "iam:ResyncMFADevice",
+ "iam:UpdateLoginProfile"
+ ],
+ "NotResource": [
+ "arn:aws:iam::REPLACE-WITH-YOUR-AWS-ACCOUNT-ID:mfa/${aws:username}",
+ "arn:aws:iam::REPLACE-WITH-YOUR-AWS-ACCOUNT-ID:user/${aws:username}"
+ ],
+ "Condition": {
+ "NumericGreaterThanIfExists": {
+ "aws:MultiFactorAuthAge": "32400"
+ }
+ }
+ }
+ ]
+}
diff --git a/awscli-mfa/mfastatus.sh b/awscli-mfa/mfastatus.sh
new file mode 100755
index 0000000..ad8b8b8
--- /dev/null
+++ b/awscli-mfa/mfastatus.sh
@@ -0,0 +1,601 @@
+#!/usr/bin/env bash
+
+# Set the global session length in seconds below; note that
+# this only sets the client-side duration for the MFA session
+# token! The maximum length of a valid session is enforced by
+# the IAM policy, and is unaffected by this value (if this
+# duration is set to a longer value than the enforcing value
+# in the IAM policy, the token will stop working before it
+# expires on the client side). Matching this value with the
+# enforcing IAM policy provides you with accurate detail
+# about how long a token will continue to be valid.
+#
+# THIS VALUE CAN BE OPTIONALLY OVERRIDDEN PER EACH PROFILE
+# BY ADDING A "mfasec" ENTRY FOR THE PROFILE IN ~/.aws/config
+#
+# The valid session lengths are from 900 seconds (15 minutes)
+# to 129600 seconds (36 hours); currently set (below) to
+# 32400 seconds, or 9 hours.
+#
+# **NOTE: THIS SHOULD MATCH THE SETTING IN THE
+# awscli-mfa.sh SCRIPT!
+MFA_SESSION_LENGTH_IN_SECONDS=32400
+
+# Define the standard locations for the AWS credentials and
+# config files; these can be statically overridden with
+# AWS_SHARED_CREDENTIALS_FILE and AWS_CONFIG_FILE envvars
+# (this script will override these envvars only if the
+# "[default]" profile in the defined custom file(s) is
+# defunct, thus reverting to the below default locations).
+CONFFILE=~/.aws/config
+CREDFILE=~/.aws/credentials
+
+# COLOR DEFINITIONS ==========================================================
+
+# Reset
+Color_Off='\033[0m' # Text Reset
+
+# Regular Colors
+Black='\033[0;30m' # Black
+Red='\033[0;31m' # Red
+Green='\033[0;32m' # Green
+Yellow='\033[0;33m' # Yellow
+Blue='\033[0;34m' # Blue
+Purple='\033[0;35m' # Purple
+Cyan='\033[0;36m' # Cyan
+White='\033[0;37m' # White
+
+# Bold
+BBlack='\033[1;30m' # Black
+BRed='\033[1;31m' # Red
+BGreen='\033[1;32m' # Green
+BYellow='\033[1;33m' # Yellow
+BBlue='\033[1;34m' # Blue
+BPurple='\033[1;35m' # Purple
+BCyan='\033[1;36m' # Cyan
+BWhite='\033[1;37m' # White
+
+# Underline
+UBlack='\033[4;30m' # Black
+URed='\033[4;31m' # Red
+UGreen='\033[4;32m' # Green
+UYellow='\033[4;33m' # Yellow
+UBlue='\033[4;34m' # Blue
+UPurple='\033[4;35m' # Purple
+UCyan='\033[4;36m' # Cyan
+UWhite='\033[4;37m' # White
+
+# Background
+On_Black='\033[40m' # Black
+On_Red='\033[41m' # Red
+On_Green='\033[42m' # Green
+On_Yellow='\033[43m' # Yellow
+On_Blue='\033[44m' # Blue
+On_Purple='\033[45m' # Purple
+On_Cyan='\033[46m' # Cyan
+On_White='\033[47m' # White
+
+# High Intensity
+IBlack='\033[0;90m' # Black
+IRed='\033[0;91m' # Red
+IGreen='\033[0;92m' # Green
+IYellow='\033[0;93m' # Yellow
+IBlue='\033[0;94m' # Blue
+IPurple='\033[0;95m' # Purple
+ICyan='\033[0;96m' # Cyan
+IWhite='\033[0;97m' # White
+
+# Bold High Intensity
+BIBlack='\033[1;90m' # Black
+BIRed='\033[1;91m' # Red
+BIGreen='\033[1;92m' # Green
+BIYellow='\033[1;93m' # Yellow
+BIBlue='\033[1;94m' # Blue
+BIPurple='\033[1;95m' # Purple
+BICyan='\033[1;96m' # Cyan
+BIWhite='\033[1;97m' # White
+
+# High Intensity backgrounds
+On_IBlack='\033[0;100m' # Black
+On_IRed='\033[0;101m' # Red
+On_IGreen='\033[0;102m' # Green
+On_IYellow='\033[0;103m' # Yellow
+On_IBlue='\033[0;104m' # Blue
+On_IPurple='\033[0;105m' # Purple
+On_ICyan='\033[0;106m' # Cyan
+On_IWhite='\033[0;107m' # White
+
+
+# FUNCTIONS ==================================================================
+
+# workaround function for lack of
+# macOS bash's assoc arrays
+idxLookup() {
+ # $1 is _ret (returns the index)
+ # $2 is the array
+ # $3 is the item to be looked up in the array
+
+ declare -a arr=("${!2}")
+ local key=$3
+ local result=""
+ local i
+ local maxIndex
+
+ maxIndex=${#arr[@]}
+ ((maxIndex--))
+
+ for (( i=0; i<=maxIndex; i++ ))
+ do
+ if [[ "${arr[$i]}" == "$key" ]]; then
+ result=$i
+ break
+ fi
+ done
+
+ eval "$1=$result"
+}
+
+getDuration() {
+ # $1 is _ret
+ # $2 is the profile ident
+
+ local this_profile_ident=$2
+ local this_duration
+
+ # use parent profile ident if this is an MFA session
+ [[ "$this_profile_ident" =~ ^(.*)-mfasession$ ]] &&
+ this_profile_ident="${BASH_REMATCH[1]}"
+
+ # look up possible custom duration for the parent profile
+ idxLookup idx confs_ident[@] "$this_profile_ident"
+
+ [[ $idx != "" && "${confs_mfasec[$idx]}" != "" ]] &&
+ this_duration=${confs_mfasec[$idx]} ||
+ this_duration=$MFA_SESSION_LENGTH_IN_SECONDS
+
+ eval "$1=${this_duration}"
+}
+
+# Returns remaining seconds for the given timestamp;
+# if the custom duration is not provided, the global
+# duration setting is used). In the result
+# 0 indicates expired, -1 indicates NaN input
+getRemaining() {
+ # $1 is _ret
+ # $2 is the timestamp
+ # $3 is the duration
+
+ local timestamp=$2
+ local duration=$3
+ local this_time
+ this_time=$(date +%s)
+ local remaining=0
+
+ [[ "${duration}" == "" ]] &&
+ duration=$MFA_SESSION_LENGTH_IN_SECONDS
+
+ if [ ! -z "${timestamp##*[!0-9]*}" ]; then
+ ((session_end=timestamp+duration))
+ if [[ $session_end -gt $this_time ]]; then
+ ((remaining=session_end-this_time))
+ else
+ remaining=0
+ fi
+ else
+ remaining=-1
+ fi
+ eval "$1=${remaining}"
+}
+
+# return printable output for given 'remaining' timestamp
+# (must be pre-incremented with duration,
+# such as getRemaining() output)
+getPrintableTimeRemaining() {
+ # $1 is _ret
+ # $2 is the timestamp
+
+ local timestamp=$2
+
+ case $timestamp in
+ -1)
+ response="N/A"
+ ;;
+ 0)
+ response="EXPIRED"
+ ;;
+ *)
+ response=$(printf '%02dh:%02dm:%02ds' $((timestamp/3600)) $((timestamp%3600/60)) $((timestamp%60)))
+ ;;
+ esac
+ eval "$1=${response}"
+}
+
+sessionData() {
+ idxLookup idx profiles_key_id[@] "$AWS_ACCESS_KEY_ID"
+ in_env_only="false"
+ if [[ "$idx" == "" ]]; then
+
+ if [[ "${AWS_PROFILE}" != "" ]]; then
+ idxLookup name_idx profiles_ident[@] "${AWS_PROFILE}"
+ if [[ "$name_idx" != "" ]]; then
+ matched="not same as the similarly named persistent session"
+ else
+ matched="an in-env session [only]"
+ in_env_only="true"
+ fi
+ else
+ matched="an in-env session [only]"
+ in_env_only="true"
+ fi
+ else
+ if [[ "${AWS_PROFILE}" != "" ]]; then
+ matched="same as the similarly named persistent session below"
+ else
+ matched="same as the persistent session '${profiles_ident[$idx]}'"
+ fi
+ fi
+
+ for_iam=""
+ bad_profile="false"
+ if [[ "$in_env_only" == "true" ]]; then
+ env_iam_check="$(aws sts get-caller-identity --output text --query 'Arn' 2>&1)"
+
+ if [[ "$env_iam_check" =~ ^arn:aws:iam::([[:digit:]]+):user.*/([^/]+)$ ]]; then
+ aws_account_id="${BASH_REMATCH[1]}" # this AWS account
+ aws_iam_user="${BASH_REMATCH[2]}" # IAM user of the (hopefully :-) active MFA session
+ for_iam=" for IAM user '$aws_iam_user'"
+ else
+ bad_profile="true"
+ fi
+ fi
+
+ [[ "${AWS_PROFILE}" == "" ]] && AWS_PROFILE="[unnamed]"
+
+ if [[ "$bad_profile" == "false" ]]; then
+ echo -e "${Green}AWS PROFILE IN THE ENVIRONMENT: ${BIGreen}${AWS_PROFILE} ${Green}\\n (${matched}${for_iam})${Color_Off}"
+ else
+ echo -e "${Red}AWS PROFILE IN THE ENVIRONMENT: ${BIRed}${AWS_PROFILE} -- a bad profile?${Color_Off}"
+ fi
+
+ if [[ "$AWS_SESSION_INIT_TIME" != "" ]]; then
+
+ # use the global default if the duration is not set for the env session
+ [[ "${AWS_SESSION_DURATION}" == "" ]] &&
+ AWS_SESSION_DURATION=$MFA_SESSION_LENGTH_IN_SECONDS
+
+ getRemaining _ret_remaining "$AWS_SESSION_INIT_TIME" "$AWS_SESSION_DURATION"
+ getPrintableTimeRemaining _ret ${_ret_remaining}
+ if [ "${_ret}" = "EXPIRED" ]; then
+ echo -e " ${Red}THE MFA SESSION EXPIRED; ${BIRed}YOU SHOULD PURGE THE ENV BY EXECUTING 'source ./source-this-to-clear-AWS-envvars.sh'${Color_Off}"
+ else
+ echo -e " ${Green}MFA SESSION REMAINING TO EXPIRATION: ${BIGreen}${_ret}${Color_Off}"
+ fi
+ fi
+}
+
+repeatr() {
+ # $1 is the repeat_char
+ # $2 is the base_length
+ # $3 is the variable_repeat_length
+
+ local repeat_char=$1
+ local base_length=$2
+ local variable_repeat_length=$3
+
+ ((repeat_length=base_length+variable_repeat_length))
+
+ printf "$repeat_char"'%.s' $(eval "echo {1.."$((repeat_length))"}");
+}
+
+getAccountAlias() {
+ # $1 is _ret (returns the index)
+ # $2 is the profile_ident
+
+ local local_profile_ident=$2
+
+ if [[ "$local_profile_ident" != "" ]]; then
+ profile_param="--profile $local_profile_ident"
+ else
+ profile_param=""
+ fi
+
+ # get the account alias (if any) for the user/profile
+ account_alias_result="$(aws iam list-account-aliases $profile_param --output text --query 'AccountAliases' 2>&1)"
+ [[ "$DEBUG" == "true" ]] && echo -e "\\n${Cyan}${On_Black}result for: 'aws iam list-account-aliases $profile_param --query 'AccountAliases' --output text':\\n${ICyan}${account_alias_result}${Color_Off}\\n\\n"
+
+ if [[ "$account_alias_result" =~ 'error occurred' ]]; then
+ # no access to list account aliases for this profile or other error
+ result=""
+ else
+ result="$account_alias_result"
+ fi
+
+ eval "$1=$result"
+}
+
+# -- end functions --
+
+## PREREQUISITES CHECK
+
+filexit="false"
+# check for ~/.aws directory, and ~/.aws/{config|credentials} files
+# if the custom config defs aren't in effect
+if [[ "$AWS_CONFIG_FILE" == "" ]] &&
+ [[ "$AWS_SHARED_CREDENTIALS_FILE" == "" ]] &&
+ [ ! -d ~/.aws ]; then
+
+ echo
+ echo -e "${BIRed}AWSCLI configuration directory '~/.aws' is not present.${Color_Off}\\nMake sure it exists, and that you have at least one profile configured\\nusing the 'config' and 'credentials' files within that directory."
+ filexit="true"
+fi
+
+# SUPPORT CUSTOM CONFIG FILE SET WITH ENVVAR
+if [[ "$AWS_CONFIG_FILE" != "" ]] &&
+ [ -f "$AWS_CONFIG_FILE" ]; then
+
+ active_config_file=$AWS_CONFIG_FILE
+ echo
+ echo -e "${BIWhite}${On_Black}** NOTE: A custom configuration file defined with AWS_CONFIG_FILE envvar in effect: '$AWS_CONFIG_FILE'${Color_Off}"
+
+elif [[ "$AWS_CONFIG_FILE" != "" ]] &&
+ [ ! -f "$AWS_CONFIG_FILE" ]; then
+
+ echo
+ echo -e "${BIRed}${On_Black}The custom config file defined with AWS_CONFIG_FILE envvar, '$AWS_CONFIG_FILE', is not present.${Color_Off}\\nMake sure it is present or purge the envvar.\\nSee http://docs.aws.amazon.com/cli/latest/userguide/cli-config-files.html for details on how to set them up."
+ filexit="true"
+
+elif [ -f "$CONFFILE" ]; then
+ active_config_file="$CONFFILE"
+else
+ echo
+ echo -e "${BIRed}${On_Black}AWSCLI configuration file '$CONFFILE' was not found.${Color_Off}\\nMake sure it and '$CREDFILE' files exist.\\nSee http://docs.aws.amazon.com/cli/latest/userguide/cli-config-files.html for details on how to set them up."
+ filexit="true"
+fi
+
+# SUPPORT CUSTOM CREDENTIALS FILE SET WITH ENVVAR
+if [[ "$AWS_SHARED_CREDENTIALS_FILE" != "" ]] &&
+ [ -f "$AWS_SHARED_CREDENTIALS_FILE" ]; then
+
+ active_credentials_file=$AWS_SHARED_CREDENTIALS_FILE
+ echo
+ echo -e "${BIWhite}${On_Black}** NOTE: A custom credentials file defined with AWS_SHARED_CREDENTIALS_FILE envvar in effect: '$AWS_SHARED_CREDENTIALS_FILE'${Color_Off}"
+
+elif [[ "$AWS_SHARED_CREDENTIALS_FILE" != "" ]] &&
+ [ ! -f "$AWS_SHARED_CREDENTIALS_FILE" ]; then
+
+ echo
+ echo -e "${BIRed}${On_Black}The custom credentials file defined with AWS_SHARED_CREDENTIALS_FILE envvar, '$AWS_SHARED_CREDENTIALS_FILE', is not present.${Color_Off}\\nMake sure it is present or purge the envvar.\\nSee http://docs.aws.amazon.com/cli/latest/userguide/cli-config-files.html for details on how to set them up."
+ filexit="true"
+
+elif [ -f "$CREDFILE" ]; then
+ active_credentials_file="$CREDFILE"
+else
+ echo
+ echo -e "${BIRed}${On_Black}AWSCLI credentials file '${CREDFILE}' was not found.${Color_Off}\\nMake sure it and '${CONFFILE}' files exist.\\nSee http://docs.aws.amazon.com/cli/latest/userguide/cli-config-files.html for details on how to set them up."
+ filexit="true"
+fi
+
+if [[ "$filexit" == "true" ]]; then
+ echo
+ exit 1
+fi
+
+CONFFILE="$active_config_file"
+CREDFILE="$active_credentials_file"
+
+
+# COLLECT AWS_SESSION DATA FROM THE ENVIRONMENT
+AWS_PROFILE=$(env | grep AWS_PROFILE)
+[[ "$AWS_PROFILE" =~ ^AWS_PROFILE[[:space:]]*=[[:space:]]*(.*)$ ]] &&
+ AWS_PROFILE="${BASH_REMATCH[1]}"
+
+AWS_ACCESS_KEY_ID=$(env | grep AWS_ACCESS_KEY_ID)
+[[ "$AWS_ACCESS_KEY_ID" =~ ^AWS_ACCESS_KEY_ID[[:space:]]*=[[:space:]]*(.*)$ ]] &&
+ AWS_ACCESS_KEY_ID="${BASH_REMATCH[1]}"
+
+AWS_SESSION_TOKEN=$(env | grep AWS_SESSION_TOKEN)
+[[ "$AWS_SESSION_TOKEN" =~ ^AWS_SESSION_TOKEN[[:space:]]*=[[:space:]]*(.*)$ ]] &&
+ AWS_SESSION_TOKEN="${BASH_REMATCH[1]}"
+
+AWS_SESSION_INIT_TIME=$(env | grep AWS_SESSION_INIT_TIME)
+[[ "$AWS_SESSION_INIT_TIME" =~ ^AWS_SESSION_INIT_TIME[[:space:]]*=[[:space:]]*(.*)$ ]] &&
+ AWS_SESSION_INIT_TIME="${BASH_REMATCH[1]}"
+
+AWS_SESSION_DURATION=$(env | grep AWS_SESSION_DURATION)
+[[ "$AWS_SESSION_DURATION" =~ ^AWS_SESSION_DURATION[[:space:]]*=[[:space:]]*(.*)$ ]] &&
+ AWS_SESSION_DURATION="${BASH_REMATCH[1]}"
+
+IN_ENV_SESSION_TIME=0
+if [[ "$AWS_SESSION_INIT_TIME" != "" ]]; then
+
+ [[ "${AWS_SESSION_DURATION}" == "" ]] &&
+ AWS_SESSION_DURATION=$MFA_SESSION_LENGTH_IN_SECONDS
+
+ ((IN_ENV_SESSION_TIME=AWS_SESSION_INIT_TIME+AWS_SESSION_DURATION))
+fi
+
+# COLLECT AWS CONFIG DATA FROM $CONFFILE
+
+# init arrays to hold ident<->mfasec detail
+declare -a confs_ident
+declare -a confs_mfasec
+confs_iterator=0
+
+# read the config file for the optional MFA length param (mfasec)
+while IFS='' read -r line || [[ -n "$line" ]]; do
+
+ [[ "$line" =~ ^\[[[:space:]]*profile[[:space:]]*(.*)[[:space:]]*\].* ]] &&
+ this_conf_ident=${BASH_REMATCH[1]}
+
+ [[ "$line" =~ ^[[:space:]]*mfasec[[:space:]]*=[[:space:]]*(.*)$ ]] &&
+ this_conf_mfasec=${BASH_REMATCH[1]}
+
+ if [[ "$this_conf_mfasec" != "" ]]; then
+ confs_ident[$confs_iterator]=$this_conf_ident
+ confs_mfasec[$confs_iterator]=$this_conf_mfasec
+
+ ((confs_iterator++))
+ fi
+
+ this_conf_mfasec=""
+
+done < "$CONFFILE"
+
+
+# COLLECT AWS_SESSION DATA FROM $CREDFILE
+
+# define profiles arrays
+declare -a profiles_ident
+declare -a profiles_type
+declare -a profiles_key_id
+declare -a profiles_session_token
+declare -a profiles_session_init_time
+declare -a profiles_mfa_mfasec
+profiles_iterator=0
+profiles_init=0
+
+while IFS='' read -r line || [[ -n "$line" ]]; do
+
+ if [[ "$line" =~ ^\[(.*)\].* ]]; then
+ _ret=${BASH_REMATCH[1]}
+
+ if [[ $profiles_init -eq 0 ]]; then
+ profiles_ident[$profiles_iterator]=$_ret
+ profiles_init=1
+ fi
+
+ if [[ "${profiles_ident[$profiles_iterator]}" != "$_ret" ]]; then
+ ((profiles_iterator++))
+ profiles_ident[$profiles_iterator]="$_ret"
+ fi
+
+ # transfer possible MFA mfasec from config array
+ idxLookup idx confs_ident[@] "${_ret}"
+ if [[ $idx != "" ]]; then
+ profiles_mfa_mfasec[$profiles_iterator]=${confs_mfasec[$idx]}
+ fi
+
+ if [[ "$_ret" != "" ]] &&
+ [[ ! "$_ret" =~ -mfasession$ ]]; then
+
+ profiles_type[$profiles_iterator]="profile"
+ else
+ profiles_type[$profiles_iterator]="session"
+ fi
+ fi
+
+ [[ "$line" =~ ^aws_access_key_id[[:space:]]*=[[:space:]]*(.*)$ ]] &&
+ profiles_key_id[$profiles_iterator]="${BASH_REMATCH[1]}"
+
+ [[ "$line" =~ ^aws_session_token[[:space:]]*=[[:space:]]*(.*)$ ]] &&
+ profiles_session_token[$profiles_iterator]="${BASH_REMATCH[1]}"
+
+ [[ "$line" =~ ^aws_session_init_time[[:space:]]*=[[:space:]]*(.*)$ ]] &&
+ profiles_session_init_time[$profiles_iterator]="${BASH_REMATCH[1]}"
+
+done < "$CREDFILE"
+
+## PRESENTATION
+
+echo
+echo -e "${BIWhite}${On_Black}ENVIRONMENT${Color_Off}"
+echo -e "==========="
+echo
+
+if [[ "$AWS_PROFILE" != "" ]]; then
+ if [[ "$AWS_ACCESS_KEY_ID" != "" ]]; then
+ sessionData
+ else
+ idxLookup name_idx profiles_ident[@] "${AWS_PROFILE}"
+ if [[ "$name_idx" != "" ]]; then
+ profile_type=${profiles_type[$name_idx]}
+ else
+ profile_type=""
+ fi
+
+ if [[ "${profile_type}" == "profile" ]]; then
+ echo -e "${Green}${On_Black}ENVVAR 'AWS_PROFILE' SELECTING A BASE PROFILE (not an MFA session): ${BIGreen}${AWS_PROFILE}${Color_Off}"
+ elif [[ "${profile_type}" == "session" ]]; then
+ echo -e "${Green}${On_Black}ENVVAR 'AWS_PROFILE' SELECTING A PERSISTENT MFA SESSION (as below): ${BIGreen}${AWS_PROFILE}${Color_Off}"
+ else
+ echo -e "${BIRed}${On_Black}INVALID ENVIRONMENT CONFIGURATION!\\nExecute ${Red}source ./source-this-to-clear-AWS-envvars.sh${BIRed} to clear the environment.\\n${Color_Off}"
+ fi
+ fi
+else
+ if [[ "$AWS_ACCESS_KEY_ID" != "" ]]; then
+ sessionData
+
+ else
+ echo -e "No AWS profile variables present in the environment;\\nusing the default base profile."
+ fi
+fi
+
+echo
+echo
+echo -e "${BIWhite}${On_Black}PERSISTENT MFA SESSIONS (in $CREDFILE)${Color_Off}"
+repeatr "=" 29 ${#CREDFILE}
+echo -e "${Color_Off}"
+echo
+
+maxIndex=${#profiles_ident[@]}
+((maxIndex--))
+
+live_session_counter=0
+bad_profile="false"
+for_iam=""
+for (( z=0; z<=maxIndex; z++ ))
+do
+ if [[ "${profiles_type[$z]}" == "session" ]]; then
+
+ profile_iam_check="$(aws --profile "${profiles_ident[$z]}" sts get-caller-identity --output text --query 'Arn' 2>&1)"
+
+ if [[ "$profile_iam_check" =~ ^arn:aws:iam::([[:digit:]]+):user.*/([^/]+)$ ]]; then
+ aws_account_id="${BASH_REMATCH[1]}" # this AWS account
+ aws_iam_user="${BASH_REMATCH[2]}" # IAM user of the (hopefully :-) active MFA session
+ for_iam="$aws_iam_user"
+
+ getAccountAlias _ret
+ if [[ "${_ret}" != "" ]]; then
+ account_alias_if_any=" @ ${_ret}"
+ else
+ account_alias_if_any=" @ ${aws_acount_id}"
+ fi
+
+ else
+ for_iam="unknown -- a bad profile?"
+ bad_profile="true"
+ fi
+
+ if [[ "$bad_profile" == "false" ]]; then
+ echo -e "${Green}MFA SESSION IDENT: ${BIGreen}${profiles_ident[$z]} ${Green}(IAM user: '${for_iam}${account_alias_if_any}')${Color_Off}"
+ else
+ echo -e "${Green}MFA SESSION IDENT: ${BIGreen}${profiles_ident[$z]} ${Red}(IAM user: '${for_iam}${account_alias_if_any}')${Color_Off}"
+ fi
+
+ if [[ "${profiles_session_init_time[$z]}" != "" ]]; then
+ getDuration _ret_duration "${profiles_ident[$z]}"
+ getRemaining _ret_remaining "${profiles_session_init_time[$z]}" "${_ret_duration}"
+ getPrintableTimeRemaining _ret "${_ret_remaining}"
+ if [ "${_ret}" = "EXPIRED" ]; then
+ echo -e " ${Red}**MFA SESSION EXPIRED**${Color_Off}"
+ else
+ ((live_session_counter++))
+ echo -e " ${Green}MFA SESSION REMAINING TO EXPIRATION: ${BIGreen}${_ret}${Color_Off}"
+ fi
+ else
+ echo -e " ${Yellow}No recorded init time (legacy or external init?)${Color_Off}"
+ fi
+ echo
+ fi
+
+ bad_profile="false"
+ _ret=""
+ _ret_duration=""
+ _ret_remaining=""
+done
+
+if [[ $live_session_counter -eq 0 ]]; then
+ echo -e "No current active persistent MFA sessions.\\n\\n"
+fi
+
+echo -e "\\nNOTE: Execute 'awscli-mfa.sh' to renew/start a new MFA session,\\n or to select (switch to) an existing active MFA session.\\n\\n"
diff --git a/awscli-mfa/source-this-to-clear-AWS-envvars.sh b/awscli-mfa/source-this-to-clear-AWS-envvars.sh
new file mode 100644
index 0000000..9d9a636
--- /dev/null
+++ b/awscli-mfa/source-this-to-clear-AWS-envvars.sh
@@ -0,0 +1,21 @@
+#!/bin/bash
+
+if [ "$0" = "$BASH_SOURCE" ]; then
+ echo
+ echo "You must source this script to clear the AWS environment variables, like so:"
+ echo
+ echo "source ./source-to-clear-AWS-envvars.sh"
+ echo
+fi
+
+unset AWS_ACCESS_KEY_ID
+unset AWS_SECRET_ACCESS_KEY
+unset AWS_SESSION_TOKEN
+unset AWS_SESSION_INIT_TIME
+unset AWS_SESSION_DURATION
+unset AWS_DEFAULT_REGION
+unset AWS_DEFAULT_OUTPUT
+unset AWS_PROFILE
+unset AWS_CA_BUNDLE
+unset AWS_SHARED_CREDENTIALS_FILE
+unset AWS_CONFIG_FILE
diff --git a/awscli-mfa/test.sh b/awscli-mfa/test.sh
new file mode 100755
index 0000000..0397fde
--- /dev/null
+++ b/awscli-mfa/test.sh
@@ -0,0 +1,28 @@
+#!/bin/bash
+
+currently_selected_profile_ident="blahblah-mfasession"
+final_selection="jotainmuuta"
+
+declare -a testarray
+
+testarray[2]='hey'
+testarray[5]='lala'
+
+echo "this is testarray 2: ${testarray[2]}"
+echo "this is testarray 5: ${testarray[5]}"
+echo "this is testarray 3: ${testarray[3]}"
+
+profile_ident="asfasdfasdf-mfasession"
+
+ if [[ "$profile_ident" != "" ]] &&
+ [[ ! "$profile_ident" =~ -mfasession$ ]] &&
+ [[ ! "$profile_ident" =~ -rolesession$ ]] ; then
+
+ echo "joujoujou"
+
+ fi
+
+echo > testfile
+echo "blabla" >> testfile
+echo -e "\\n\\n" >> testfile
+echo "blabla again" >> testfile
diff --git a/delete-sagemaker-notebooks.sh b/delete-sagemaker-notebooks.sh
new file mode 100755
index 0000000..edbeeca
--- /dev/null
+++ b/delete-sagemaker-notebooks.sh
@@ -0,0 +1,145 @@
+#!/usr/bin/env bash
+
+# Script to search and delete Amazon SageMaker Notebook instances across all enabled regions
+# Uses AWS CLI to manage SageMaker notebooks
+# NOTE: This script assumes all notebook instances are already stopped
+
+set -e
+
+## PREREQUISITES CHECK
+
+# `exists` for commands
+exists() {
+ command -v "$1" >/dev/null 2>&1
+}
+
+# is AWS CLI installed?
+if ! exists aws ; then
+ printf "\n******************************************************************************************************************************\n\
+This script requires the AWS CLI. See the details here: https://docs.aws.amazon.com/cli/latest/userguide/getting-started-install.html\n\
+******************************************************************************************************************************\n\n"
+ exit 1
+fi
+
+# Check if AWS credentials are configured
+if ! aws sts get-caller-identity &>/dev/null; then
+ echo
+ echo "ERROR: AWS credentials are not configured or are invalid."
+ echo "Please configure your credentials using 'aws configure' or ensure your AWS environment variables are set."
+ echo
+ exit 1
+fi
+
+echo "==========================================="
+echo "SageMaker Notebook Instances Deletion Tool"
+echo "==========================================="
+echo
+
+# Get AWS account information
+ACCOUNT_ID=$(aws sts get-caller-identity --query Account --output text)
+USER_ARN=$(aws sts get-caller-identity --query Arn --output text)
+echo "Running as: $USER_ARN"
+echo "Account ID: $ACCOUNT_ID"
+echo
+
+# Function to get only enabled regions (excludes non-opted-in regions)
+get_enabled_regions() {
+ aws ec2 describe-regions --all-regions --query "Regions[?OptInStatus=='opt-in-not-required' || OptInStatus=='opted-in'].RegionName" --output text
+}
+
+# Function to list SageMaker notebook instances in a region
+list_notebooks_in_region() {
+ local region=$1
+ aws sagemaker list-notebook-instances --region "$region" --query "NotebookInstances[].NotebookInstanceName" --output text 2>/dev/null || echo ""
+}
+
+# Function to delete a notebook instance
+delete_notebook() {
+ local region=$1
+ local notebook_name=$2
+ echo " → Deleting notebook instance: $notebook_name"
+ if aws sagemaker delete-notebook-instance --region "$region" --notebook-instance-name "$notebook_name" 2>/dev/null; then
+ echo " ✓ Notebook deleted: $notebook_name"
+ return 0
+ else
+ echo " ✗ Failed to delete notebook: $notebook_name"
+ return 1
+ fi
+}
+
+# Main execution
+echo "Retrieving enabled AWS regions..."
+REGIONS=$(get_enabled_regions)
+
+if [ -z "$REGIONS" ]; then
+ echo "ERROR: Could not retrieve AWS regions. Please check your AWS CLI configuration."
+ exit 1
+fi
+
+REGION_COUNT=$(echo "$REGIONS" | wc -w)
+echo "Found $REGION_COUNT enabled region(s) to scan"
+echo
+
+# Track statistics
+TOTAL_NOTEBOOKS=0
+DELETED_NOTEBOOKS=0
+FAILED_DELETIONS=0
+
+# Scan each region
+for region in $REGIONS; do
+ echo "Scanning region: $region"
+
+ # List notebooks in the region
+ notebooks=$(list_notebooks_in_region "$region")
+
+ if [ -z "$notebooks" ]; then
+ echo " No notebook instances found in $region"
+ echo
+ continue
+ fi
+
+ # Convert to array
+ notebook_array=($notebooks)
+ notebook_count=${#notebook_array[@]}
+ TOTAL_NOTEBOOKS=$((TOTAL_NOTEBOOKS + notebook_count))
+
+ echo " Found $notebook_count notebook instance(s) in $region"
+
+ # Process each notebook
+ for notebook in $notebook_array; do
+ echo " Processing: $notebook"
+
+ # Delete the notebook (assumes it's already stopped)
+ if delete_notebook "$region" "$notebook"; then
+ DELETED_NOTEBOOKS=$((DELETED_NOTEBOOKS + 1))
+ else
+ echo " ⚠ Note: If deletion failed because the notebook is not stopped, stop it first and retry"
+ FAILED_DELETIONS=$((FAILED_DELETIONS + 1))
+ fi
+ done
+
+ echo
+done
+
+# Summary
+echo "==========================================="
+echo "Deletion Summary"
+echo "==========================================="
+echo "Total notebook instances found: $TOTAL_NOTEBOOKS"
+echo "Successfully deleted: $DELETED_NOTEBOOKS"
+echo "Failed deletions: $FAILED_DELETIONS"
+echo "==========================================="
+echo
+
+if [ $TOTAL_NOTEBOOKS -eq 0 ]; then
+ echo "No SageMaker notebook instances were found in any region."
+elif [ $DELETED_NOTEBOOKS -eq $TOTAL_NOTEBOOKS ]; then
+ echo "All notebook instances were successfully deleted!"
+ exit 0
+elif [ $DELETED_NOTEBOOKS -gt 0 ]; then
+ echo "Some notebook instances were deleted, but there were failures."
+ exit 1
+else
+ echo "No notebook instances could be deleted."
+ exit 1
+fi