From b5fa1eb625f0058ecd75adf39884165224e8b31d Mon Sep 17 00:00:00 2001 From: Ville Walveranta Date: Sat, 17 Feb 2018 03:08:37 -0600 Subject: [PATCH 01/71] Significant awscli-mfa.sh script improvements (preparing for CLI AWS_SESSION_TOKEN use). --- awscli-mfa.sh | 354 ++++++++++++++++++++++++++++++++++---------------- 1 file changed, 240 insertions(+), 114 deletions(-) diff --git a/awscli-mfa.sh b/awscli-mfa.sh index bdf4419..2525222 100755 --- a/awscli-mfa.sh +++ b/awscli-mfa.sh @@ -1,5 +1,9 @@ #!/bin/bash +DEBUG="false" +# uncomment below to enable the debug output +#DEBUG="true" + # Set the session length in seconds below; # note that this only sets the client-side # validity of the MFA session token; @@ -51,8 +55,10 @@ elif [ ! -f ~/.aws/credentials ]; then exit 1 fi +# defined the standard location of the AWS credentials file CREDFILE=~/.aws/credentials -# check that at least one profile is configured + +# 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" =~ ^\[(.*)\].* ]] && @@ -63,39 +69,39 @@ while IFS='' read -r line || [[ -n "$line" ]]; do fi done < $CREDFILE - -if [[ "$ONEPROFILE" = "false" ]]; then +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 + # 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 the arrays declare -a cred_profiles declare -a cred_profile_status declare -a cred_profile_user @@ -109,29 +115,36 @@ else echo "Please wait..." + # read the credentials file 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 + # 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 --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 + # get the user ARN; this should be always + # available for valid profiles 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 + # must be a bad profile cred_profile_arn[$cred_profilecounter]="" fi - # get the actual username (may be different from the arbitrary profile ident) + # get the actual username + # (may be different from the arbitrary profile ident) [[ "$user_arn" =~ ([^/]+)$ ]] && profile_username="${BASH_REMATCH[1]}" if [[ "$profile_username" =~ error ]]; then @@ -140,15 +153,18 @@ else cred_profile_user[$cred_profilecounter]="$profile_username" fi - # find existing MFA sessions for the current profile + # find the existing MFA sessions for the current profile + # (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 - # (this is not 100% as it depends on IAM access) + # 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 reliable result) 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" @@ -157,6 +173,8 @@ else fi # get MFA ARN if available + # (obviously not available if a MFA device + # isn't configured for the profile) 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" @@ -165,7 +183,9 @@ else fi # if existing MFA profile was found, check its status - # (this is not 100% as it depends on IAM access) + # (this is not 100% as it depends on the defined IAM access; + # however if MFA enforcement is set, this should produce + # a reliable result) 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 @@ -177,17 +197,21 @@ else 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 + ## DEBUG (enable with DEBUG="true" on top of the file) + if [ "$DEBUG" == "true" ]; then + + 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 + fi + ## END DEBUG # erase variables & increase iterator for the next iteration mfa_arn="" @@ -203,7 +227,7 @@ else fi done < $CREDFILE - # create profile selections + # create the profile selections echo "AVAILABLE AWS PROFILES:" echo SELECTR=0 @@ -218,8 +242,8 @@ else echo "${ITER}: $i (${cred_profile_user[$SELECTR]}${mfa_notify})" - if [ "${mfa_profile_status[$SELECTR]}" = "OK" ] || - [ "${mfa_profile_status[$SELECTR]}" = "LIMITED" ]; then + 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 @@ -228,13 +252,19 @@ else let SELECTR=${SELECTR}+1 done + # this is used to trigger MFA request for a MFA profile + active_mfa="false" + + # this is used to print MFA questions/details + mfaprofile="false" + # 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 + # capture the numeric part of the selection [[ $selprofile =~ ^([[:digit:]]+) ]] && selprofile_check="${BASH_REMATCH[1]}" if [ "$selprofile_check" != "" ]; then @@ -256,15 +286,21 @@ else [[ $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" != "" && - ( "${mfa_profile_status[$actual_selprofile]}" = "OK" || - "${mfa_profile_status[$actual_selprofile]}" = "LIMITED" ) ]]; then - + ( "${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]}" + # this is used to print MFA questions/details + mfaprofile="true" + + active_mfa="true" + elif [[ "$selprofile_mfa_check" != "" && - "${mfa_profile_status[$actual_selprofile]}" = "" ]]; then + "${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 @@ -296,11 +332,15 @@ else exit 1 fi - if [ "${mfa_arns[$actual_selprofile]}" != "" ]; then - mfaprofile="true" - # prompt for the MFA code since MFA has been configured for this profile + if [[ "${mfa_arns[$actual_selprofile]}" != "" && + "$active_mfa" == "false" ]]; then + + # prompt for the MFA code since MFA has been configured for this profile (MFA ARN is avialable), + # and since the selection is not an active MFA 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." + echo -e "Enter the current MFA one time pass code for profile '${cred_profiles[$actual_selprofile]}' to start/renew the MFA session," + echo "or leave empty (just press [ENTER]) to use the selected profile without the MFA." + while : do read mfacode @@ -312,23 +352,27 @@ else fi done - else + elif [[ "$active_mfa" == "false" ]]; then + # no MFA configured (no MFA ARN); print a notice + + # this is used to print MFA questions/details mfaprofile="false" + + # reset entered MFA code (just to be safe) 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) + if [[ "$mfacode" != "" ]]; then + # init an MFA session (request an 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" + echo "NOW GETTING THE 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 \ @@ -342,62 +386,80 @@ else echo "Could not initialize the requested MFA session." echo exit 1 - fi + else + # this is used to print MFA questions/details + mfaprofile="true" + + ## DEBUG + if [ "$DEBUG" == "true" ]; then + 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" + fi + ## END DEBUG + + # 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 format for the region (have they been 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 format 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 -## 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')." + `aws --profile $AWS_2AUTH_PROFILE configure set region "${set_new_region}"` 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 - 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')." + `aws --profile $AWS_2AUTH_PROFILE configure set output "${set_new_output}"` 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" + # 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 + elif [[ "$active_mfa" == "false" ]]; then + + # this is used to print MFA questions/details + mfaprofile="false" 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) +# todo : get base profile region, output if not set for the MFA profile + + AWS_ACCESS_KEY_ID=$(aws --profile $final_selection configure get aws_access_key_id) + AWS_SECRET_ACCESS_KEY=$(aws --profile $final_selection configure get aws_secret_access_key) + AWS_SESSION_TOKEN=$(aws --profile $final_selection configure get aws_session_token) + + echo + echo "========================================================================" echo - if [[ "$mfaprofile" = "true" && "$mfacode" != "" ]]; then + if [[ "$mfaprofile" == "true" ]]; then echo "MFA profile name: '${final_selection}'" echo else @@ -408,28 +470,92 @@ else 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:" + + # print env export secrets? + secrets_out="false" + read -p "Do you want to export the selected profile's secrets to the environment (for s3cmd, etc)? - y[N] " -n 1 -r + if [[ $REPLY =~ ^[Yy]$ ]]; then + secrets_out="true" + fi + echo + echo + + if [[ "$OS" == "macOS" ]]; then + + echo "Execute the following in Terminal to activate the selected profile" + echo "(it's already on your clipboard; just paste it and press [ENTER]):" echo echo "export AWS_PROFILE=${final_selection}" + + if [[ "$secrets_out" == "false" ]]; then + echo "unset AWS_ACCESS_KEY_ID" + echo "unset AWS_SECRET_ACCESS_KEY" + echo "unset AWS_SESSION_TOKEN" + echo -n "export AWS_PROFILE=${final_selection}; unset AWS_ACCESS_KEY_ID; unset AWS_SECRET_ACCESS_KEY; unset AWS_SESSION_TOKEN" | pbcopy + else + echo "export AWS_ACCESS_KEY_ID=${AWS_ACCESS_KEY_ID}" + echo "export AWS_SECRET_ACCESS_KEY=${AWS_SECRET_ACCESS_KEY}" + if [[ "$mfaprofile" == "true" ]]; then + echo "export AWS_SESSION_TOKEN=${AWS_SESSION_TOKEN}" + echo -n "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_SESSION_TOKEN=${AWS_SESSION_TOKEN}" | pbcopy + else + echo "unset AWS_SESSION_TOKEN" + echo -n "export AWS_PROFILE=${final_selection}; export AWS_ACCESS_KEY_ID=${AWS_ACCESS_KEY_ID}; export AWS_SECRET_ACCESS_KEY=${AWS_SECRET_ACCESS_KEY}; unset AWS_SESSION_TOKEN" | pbcopy + echo + fi + fi 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 "NOTE: Make sure to set/unset the environment with the new values as instructed above!" + + elif [ "$OS" == "Linux" ]; then + echo "Execute the following on the command line to activate this profile for the 'aws', 's3cmd', etc. commands." + echo "NOTE: Even if you only use a named profile ('AWS_PROFILE'), it's important to execute all of the export/unset" + echo " commands to make sure previously set environment variables won't override the selected configuration." 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])" + echo "export AWS_ACCESS_KEY_ID=${AWS_ACCESS_KEY_ID}" + echo "export AWS_SECRET_ACCESS_KEY=${AWS_SECRET_ACCESS_KEY}" + if [[ "$mfaprofile" == "true" ]]; then + echo "export AWS_SESSION_TOKEN=${AWS_SESSION_TOKEN}" + if exists xclip ; then + echo -n "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_SESSION_TOKEN=${AWS_SESSION_TOKEN}" | xclip -i + echo "(xclip found; the activation command is now on your X PRIMARY clipboard -- just paste on the command line, and press [ENTER])" + fi else + echo "unset AWS_SESSION_TOKEN" + if exists xclip ; then + echo -n "export AWS_PROFILE=${final_selection}; export AWS_ACCESS_KEY_ID=${AWS_ACCESS_KEY_ID}; export AWS_SECRET_ACCESS_KEY=${AWS_SECRET_ACCESS_KEY}; unset AWS_SESSION_TOKEN" | xclip -i + echo "(xclip found; the activation command is now on your X PRIMARY clipboard -- just paste on the command line, and press [ENTER])" + fi + fi + if ! exists xclip ; then + echo 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}" + echo ".. or execute the following to use named profile only, clearning any previoiusly set configuration variables:" + echo + echo "export AWS_PROFILE=${final_selection}; unset AWS_ACCESS_KEY_ID; unset AWS_SECRET_ACCESS_KEY; unset AWS_SESSION_TOKEN" + echo + else # not macOS, not Linux, so some other weird OS like Windows.. + echo "Execute the following on the command line to activate this profile for the 'aws', 's3cmd', etc. commands." + echo "NOTE: Even if you only use a named profile ('AWS_PROFILE'), it's important to execute all of the export/unset" + echo " commands to make sure previously set environment variables won't override the selected configuration." + echo + 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} \\" + if [[ "$mfaprofile" == "true" ]]; then + echo "export AWS_SESSION_TOKEN=${AWS_SESSION_TOKEN}" + else + echo "unset AWS_SESSION_TOKEN" + fi + echo + echo "..or execute the following to use named profile only, clearning any previoiusly set configuration variables:" + echo + echo "export AWS_PROFILE=${final_selection}; unset AWS_ACCESS_KEY_ID; unset AWS_SECRET_ACCESS_KEY; unset AWS_SESSION_TOKEN" + echo + fi echo From 2557db6f2f3007a5396038b7fb43477673fc717b Mon Sep 17 00:00:00 2001 From: Ville Walveranta Date: Sat, 17 Feb 2018 16:45:50 -0600 Subject: [PATCH 02/71] Finalized the awscli-mfa changes for CLI MFA use; added a sourceabl 'clear-aws.sh' that will clear set AWS environment varialbles. --- awscli-mfa.sh | 117 ++++++++++++++++++++++++++++++-------------------- clear-aws.sh | 6 +++ 2 files changed, 76 insertions(+), 47 deletions(-) create mode 100644 clear-aws.sh diff --git a/awscli-mfa.sh b/awscli-mfa.sh index 2525222..8e1ffe5 100755 --- a/awscli-mfa.sh +++ b/awscli-mfa.sh @@ -76,6 +76,21 @@ if [[ "$ONEPROFILE" == "false" ]]; then else + # get default region and output format (since at least one profile should exist at this point) + default_region=$(aws --profile default configure get region) + default_output=$(aws --profile default configure get output) + + if [[ "$default_region" == "" ]]; then + echo + echo -e "DEFAULT REGION HAS NOT BEEN CONFIGURED.\nPlease set the default region in '~/.aws/config', 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 + # Check OS for some supported platforms OS="`uname`" case $OS in @@ -113,7 +128,7 @@ else declare -a mfa_profile_status cred_profilecounter=0 - echo "Please wait..." + echo -n "Please wait" # read the credentials file while IFS='' read -r line || [[ -n "$line" ]]; do @@ -210,8 +225,10 @@ else echo "MFA PROFILE IDENT: ${mfa_profiles[$cred_profilecounter]} (${mfa_profile_status[$cred_profilecounter]})" fi echo - fi ## END DEBUG + else + echo -n "." + fi # erase variables & increase iterator for the next iteration mfa_arn="" @@ -228,6 +245,8 @@ else done < $CREDFILE # create the profile selections + echo + echo echo "AVAILABLE AWS PROFILES:" echo SELECTR=0 @@ -252,10 +271,10 @@ else let SELECTR=${SELECTR}+1 done - # this is used to trigger MFA request for a MFA profile + # this is used to determine whether to trigger a MFA request for a MFA profile active_mfa="false" - # this is used to print MFA questions/details + # this is used to determine whether to print MFA questions/details mfaprofile="false" # prompt for profile selection @@ -291,13 +310,18 @@ else ( "${mfa_profile_status[$actual_selprofile]}" == "OK" || "${mfa_profile_status[$actual_selprofile]}" == "LIMITED" ) ]]; then - echo "SELECTED MFA PROFILE: ${mfa_profiles[$actual_selprofile]}" + # get the parent profile name + # transpose selection (starting from 1) to array index (starting from 0) + mfa_parent_profile_ident="${cred_profiles[$actual_selprofile]}" + final_selection="${mfa_profiles[$actual_selprofile]}" + echo "SELECTED MFA PROFILE: ${final_selection} (for base profile '${mfa_parent_profile_ident}')" - # this is used to print MFA questions/details - mfaprofile="true" + # this is used to determine whether to print MFA questions/details + mfaprofile="true" - active_mfa="true" + # this is used to determine whether to trigger a MFA request for a MFA profile + active_mfa="true" elif [[ "$selprofile_mfa_check" != "" && "${mfa_profile_status[$actual_selprofile]}" == "" ]]; then @@ -332,11 +356,11 @@ else exit 1 fi + # this is a MFA request (a MFA ARN exists but MFA is not active) if [[ "${mfa_arns[$actual_selprofile]}" != "" && "$active_mfa" == "false" ]]; then - # prompt for the MFA code since MFA has been configured for this profile (MFA ARN is avialable), - # and since the selection is not an active MFA profile + # prompt for the MFA code echo echo -e "Enter the current MFA one time pass code for profile '${cred_profiles[$actual_selprofile]}' to start/renew the MFA session," echo "or leave empty (just press [ENTER]) to use the selected profile without the MFA." @@ -352,10 +376,9 @@ else fi done - elif [[ "$active_mfa" == "false" ]]; then - # no MFA configured (no MFA ARN); print a notice + elif [[ "$active_mfa" == "false" ]]; then # no MFA configured (no MFA ARN); print a notice - # this is used to print MFA questions/details + # this is used to determine whether to print MFA questions/details mfaprofile="false" # reset entered MFA code (just to be safe) @@ -387,7 +410,7 @@ else echo exit 1 else - # this is used to print MFA questions/details + # this is used to determine whether to print MFA questions/details mfaprofile="true" ## DEBUG @@ -403,36 +426,6 @@ else `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 format for the region (have they been 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 format 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 @@ -442,15 +435,45 @@ else elif [[ "$active_mfa" == "false" ]]; then - # this is used to print MFA questions/details + # this is used to determine whether to print MFA questions/details mfaprofile="false" fi - # get region and output format for display (even when not entering MFA code) + # get region and output format for the selected profile get_region=$(aws --profile $final_selection configure get region) get_output=$(aws --profile $final_selection configure get output) -# todo : get base profile region, output if not set for the MFA profile + # 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 [[ "${get_region}" == "" ]]; then + # retrieve parent profile region if an MFA profie + if [[ "${profile_region[$actual_selprofile]}" != "" && + "${mfaprofile}" == "true" ]]; then + set_new_region=${profile_region[$actual_selprofile]} + echo "Region had not been configured for the selected MFA profile; it has been set to same as the parent profile ('$set_new_region')." + fi + if [[ "${set_new_region}" == "" ]]; then + set_new_region=${default_region} + echo "Region had not been configured for the selected profile; it has been set to the default region ('${default_region}')." + fi + + `aws --profile $final_selection configure set region "${set_new_region}"` + fi + + if [ "${get_output}" == "" ]; then + # retrieve parent profile output format if an MFA profile + if [[ "${profile_output[$actual_selprofile]}" != "" && + "${mfaprofile}" == "true" ]]; then + set_new_output=${profile_output[$actual_selprofile]} + echo "Output format had not been configured for the selected MFA profile; 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 "Output format had not been configured for the selected profile; it has been set to the default output format ('${default_output}')." + fi + + `aws --profile $final_selection configure set output "${set_new_output}"` + fi AWS_ACCESS_KEY_ID=$(aws --profile $final_selection configure get aws_access_key_id) AWS_SECRET_ACCESS_KEY=$(aws --profile $final_selection configure get aws_secret_access_key) diff --git a/clear-aws.sh b/clear-aws.sh new file mode 100644 index 0000000..630c95d --- /dev/null +++ b/clear-aws.sh @@ -0,0 +1,6 @@ +#!/bin/bash + +unset AWS_PROFILE +unset AWS_ACCESS_KEY_ID +unset AWS_SECRET_ACCESS_KEY +unset AWS_SESSION_TOKEN From 8c6cc9e8ab0a0759264cb4748f6254c755b15e75 Mon Sep 17 00:00:00 2001 From: Ville Walveranta Date: Sat, 17 Feb 2018 21:42:52 -0600 Subject: [PATCH 03/71] Formatting fixes, typo corrections, added environment clearing script. --- awscli-mfa.sh | 164 +++++++++++++++++---------------- clear-aws.sh | 6 -- source-to-clear-AWS-envvars.sh | 14 +++ 3 files changed, 100 insertions(+), 84 deletions(-) delete mode 100644 clear-aws.sh create mode 100644 source-to-clear-AWS-envvars.sh diff --git a/awscli-mfa.sh b/awscli-mfa.sh index 8e1ffe5..7e033c9 100755 --- a/awscli-mfa.sh +++ b/awscli-mfa.sh @@ -138,19 +138,19 @@ else # 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 + if [[ "$profile_ident" != "" ]] && + ! [[ "$profile_ident" =~ -mfasession$ ]]; then - # store this profile ident - cred_profiles[$cred_profilecounter]=$profile_ident + # store this profile ident + cred_profiles[$cred_profilecounter]=$profile_ident - # store this profile region and output format - profile_region[$cred_profilecounter]=$(aws --profile $profile_ident configure get region) - profile_output[$cred_profilecounter]=$(aws --profile $profile_ident configure get output) + # store this profile region and output format + profile_region[$cred_profilecounter]=$(aws --profile $profile_ident configure get region) + profile_output[$cred_profilecounter]=$(aws --profile $profile_ident configure get output) - # get the user ARN; this should be always - # available for valid profiles - user_arn="$(aws sts get-caller-identity --profile $profile_ident --output text --query 'Arn' 2>&1)" + # get the user ARN; this should be always + # available for valid profiles + 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 @@ -160,8 +160,8 @@ else # get the actual username # (may be different from the arbitrary profile ident) - [[ "$user_arn" =~ ([^/]+)$ ]] && - profile_username="${BASH_REMATCH[1]}" + [[ "$user_arn" =~ ([^/]+)$ ]] && + profile_username="${BASH_REMATCH[1]}" if [[ "$profile_username" =~ error ]]; then cred_profile_user[$cred_profilecounter]="" else @@ -233,15 +233,15 @@ else # erase variables & increase iterator for the next iteration mfa_arn="" user_arn="" - profile_ident="" - profile_check="" - profile_username="" - mfa_profile_ident="" - mfa_profile_check="" + profile_ident="" + profile_check="" + profile_username="" + mfa_profile_ident="" + mfa_profile_check="" cred_profilecounter=$(($cred_profilecounter+1)) - fi + fi done < $CREDFILE # create the profile selections @@ -262,7 +262,7 @@ else echo "${ITER}: $i (${cred_profile_user[$SELECTR]}${mfa_notify})" if [[ "${mfa_profile_status[$SELECTR]}" == "OK" ]] || - [[ "${mfa_profile_status[$SELECTR]}" == "LIMITED" ]]; then + [[ "${mfa_profile_status[$SELECTR]}" == "LIMITED" ]]; then echo "${ITER}m: $i MFA profile in ${mfa_profile_status[$SELECTR]} status" fi @@ -282,33 +282,33 @@ else read -r selprofile # process the selection - if [ "$selprofile" != "" ]; then + 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 this is an MFA profile, it must be in OK or LIMITED status to select - if [[ "$selprofile_mfa_check" != "" && - ( "${mfa_profile_status[$actual_selprofile]}" == "OK" || - "${mfa_profile_status[$actual_selprofile]}" == "LIMITED" ) ]]; then + [[ $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 this is an MFA profile, it must be in OK or LIMITED status to select + if [[ "$selprofile_mfa_check" != "" && + ( "${mfa_profile_status[$actual_selprofile]}" == "OK" || + "${mfa_profile_status[$actual_selprofile]}" == "LIMITED" ) ]]; then # get the parent profile name # transpose selection (starting from 1) to array index (starting from 0) @@ -320,45 +320,45 @@ else # 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 + # this is used to determine whether to trigger a MFA request for a MFA profile active_mfa="true" - 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 + 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 + 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 + 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 - # this is a MFA request (a MFA ARN exists but MFA is not active) + # this is an MFA request (an MFA ARN exists but the MFA is not active) if [[ "${mfa_arns[$actual_selprofile]}" != "" && - "$active_mfa" == "false" ]]; then + "$active_mfa" == "false" ]]; then # prompt for the MFA code echo @@ -367,7 +367,7 @@ else while : do - read mfacode + read mfacode if ! [[ "$mfacode" =~ ^$ || "$mfacode" =~ [0-9]{6} ]]; then echo "The MFA code must be exactly six digits, or blank to bypass." continue @@ -427,7 +427,7 @@ else `aws --profile $AWS_2AUTH_PROFILE configure set aws_session_token "$AWS_SESSION_TOKEN"` # Make sure the final selection profile name has '-mfasession' suffix - # (it's not present when going from base profile to MFA profile) + # (it's not present when going from a base profile to an MFA profile) if ! [[ "$final_selection" =~ -mfasession$ ]]; then final_selection="${final_selection}-mfasession" fi @@ -463,7 +463,7 @@ else if [ "${get_output}" == "" ]; then # retrieve parent profile output format if an MFA profile if [[ "${profile_output[$actual_selprofile]}" != "" && - "${mfaprofile}" == "true" ]]; then + "${mfaprofile}" == "true" ]]; then set_new_output=${profile_output[$actual_selprofile]} echo "Output format had not been configured for the selected MFA profile; it has been set to same as the parent profile ('$set_new_output')." fi @@ -499,9 +499,9 @@ else read -p "Do you want to export the selected profile's secrets to the environment (for s3cmd, etc)? - y[N] " -n 1 -r if [[ $REPLY =~ ^[Yy]$ ]]; then secrets_out="true" - fi - echo - echo + fi + echo + echo if [[ "$OS" == "macOS" ]]; then @@ -528,7 +528,10 @@ else fi fi echo - echo "NOTE: Make sure to set/unset the environment with the new values as instructed above!" + echo "NOTE: Make sure to set/unset the environment with the new values as instructed above to make sure no conflicting profile/secret remains in the envrionment!" + echo + echo -e "To conveniently remove any AWS profile/secret information from the environment, simply source the attached script, like so:\n'source ./source-to-clear-AWS-envvars.sh'" + echo elif [ "$OS" == "Linux" ]; then echo "Execute the following on the command line to activate this profile for the 'aws', 's3cmd', etc. commands." @@ -560,6 +563,9 @@ else echo echo "export AWS_PROFILE=${final_selection}; unset AWS_ACCESS_KEY_ID; unset AWS_SECRET_ACCESS_KEY; unset AWS_SESSION_TOKEN" echo + echo -e "To conveniently remove any AWS profile/secret information from the environment, simply source the attached script, like so:\n'source ./source-to-clear-AWS-envvars.sh'" + echo + else # not macOS, not Linux, so some other weird OS like Windows.. echo "Execute the following on the command line to activate this profile for the 'aws', 's3cmd', etc. commands." echo "NOTE: Even if you only use a named profile ('AWS_PROFILE'), it's important to execute all of the export/unset" @@ -578,6 +584,8 @@ else echo echo "export AWS_PROFILE=${final_selection}; unset AWS_ACCESS_KEY_ID; unset AWS_SECRET_ACCESS_KEY; unset AWS_SESSION_TOKEN" echo + echo -e "To conveniently remove any AWS profile/secret information from the environment, simply source the attached script, like so:\nsource ./source-to-clear-AWS-envvars.sh" + echo fi echo diff --git a/clear-aws.sh b/clear-aws.sh deleted file mode 100644 index 630c95d..0000000 --- a/clear-aws.sh +++ /dev/null @@ -1,6 +0,0 @@ -#!/bin/bash - -unset AWS_PROFILE -unset AWS_ACCESS_KEY_ID -unset AWS_SECRET_ACCESS_KEY -unset AWS_SESSION_TOKEN diff --git a/source-to-clear-AWS-envvars.sh b/source-to-clear-AWS-envvars.sh new file mode 100644 index 0000000..0875c1a --- /dev/null +++ b/source-to-clear-AWS-envvars.sh @@ -0,0 +1,14 @@ +#!/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 ./clear-aws.sh" + echo +fi + +unset AWS_PROFILE +unset AWS_ACCESS_KEY_ID +unset AWS_SECRET_ACCESS_KEY +unset AWS_SESSION_TOKEN From d0451c5819e09b68c59afaf367452709dc3cbb0f Mon Sep 17 00:00:00 2001 From: Ville Walveranta Date: Sun, 18 Feb 2018 14:50:34 -0600 Subject: [PATCH 04/71] Bug fix: awscli-mfa.sh --- awscli-mfa.sh | 2 ++ 1 file changed, 2 insertions(+) diff --git a/awscli-mfa.sh b/awscli-mfa.sh index 7e033c9..2365ee9 100755 --- a/awscli-mfa.sh +++ b/awscli-mfa.sh @@ -457,6 +457,7 @@ else echo "Region had not been configured for the selected profile; it has been set to the default region ('${default_region}')." fi + get_region="${set_new_region}" `aws --profile $final_selection configure set region "${set_new_region}"` fi @@ -472,6 +473,7 @@ else echo "Output format had not been configured for the selected profile; it has been set to the default output format ('${default_output}')." fi + get_output="${set_new_output}" `aws --profile $final_selection configure set output "${set_new_output}"` fi From 4aa8540eba2c51ad20fc6a11f139c8a26cc98e97 Mon Sep 17 00:00:00 2001 From: Ville Walveranta Date: Sun, 18 Feb 2018 15:14:26 -0600 Subject: [PATCH 05/71] Documentation update. File reorganization. --- README.md | 4 +++- awscli-mfa.sh => awscli-mfa/awscli-mfa.sh | 0 .../source-to-clear-AWS-envvars.sh | 0 3 files changed, 3 insertions(+), 1 deletion(-) rename awscli-mfa.sh => awscli-mfa/awscli-mfa.sh (100%) rename source-to-clear-AWS-envvars.sh => awscli-mfa/source-to-clear-AWS-envvars.sh (100%) diff --git a/README.md b/README.md index 09564fc..084385d 100644 --- a/README.md +++ b/README.md @@ -5,7 +5,9 @@ This repository contains non-proprietary (MIT license) utility scripts for use w * **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.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/). +* **awscli-mfa/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/). + +* **awscli-mfa/source-to-clear-AWS-envvars.sh** - Simple sourceable script that removes any AWS secrets/settings that may have been set in the local environment by `awscli-mfa.sh` script (above). * **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/awscli-mfa.sh b/awscli-mfa/awscli-mfa.sh similarity index 100% rename from awscli-mfa.sh rename to awscli-mfa/awscli-mfa.sh diff --git a/source-to-clear-AWS-envvars.sh b/awscli-mfa/source-to-clear-AWS-envvars.sh similarity index 100% rename from source-to-clear-AWS-envvars.sh rename to awscli-mfa/source-to-clear-AWS-envvars.sh From 196cd27820150cfa501586839a488aef85cabef7 Mon Sep 17 00:00:00 2001 From: Ville Walveranta Date: Mon, 19 Feb 2018 02:32:14 -0600 Subject: [PATCH 06/71] aws-iam-rotate-keys.sh script reformat/cleanup and minor corrections. --- aws-iam-rotate-keys.sh | 997 ++++++++++++++++++++------------------- awscli-mfa/awscli-mfa.sh | 2 +- 2 files changed, 503 insertions(+), 496 deletions(-) diff --git a/aws-iam-rotate-keys.sh b/aws-iam-rotate-keys.sh index e7ffc73..77ec0f0 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,525 @@ 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=(`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 (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 + 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/awscli-mfa.sh b/awscli-mfa/awscli-mfa.sh index 2365ee9..7224c07 100755 --- a/awscli-mfa/awscli-mfa.sh +++ b/awscli-mfa/awscli-mfa.sh @@ -1,4 +1,4 @@ -#!/bin/bash +#!/usr/bin/env bash DEBUG="false" # uncomment below to enable the debug output From e7bc19d1009f279bb83acf01f6409f16d0e7a2a4 Mon Sep 17 00:00:00 2001 From: Ville Walveranta Date: Mon, 19 Feb 2018 14:23:44 -0600 Subject: [PATCH 07/71] Added error handling for explicit deny (restrictive policy) conditions. --- aws-iam-rotate-keys.sh | 31 ++++++++++++++++++++++++++----- 1 file changed, 26 insertions(+), 5 deletions(-) diff --git a/aws-iam-rotate-keys.sh b/aws-iam-rotate-keys.sh index 77ec0f0..b195f37 100755 --- a/aws-iam-rotate-keys.sh +++ b/aws-iam-rotate-keys.sh @@ -132,14 +132,26 @@ else 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}'`) + 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" == "Inactive" ]]; then + 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 @@ -147,10 +159,14 @@ else 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}" + 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 ${key_status_array[$keypos]} is $(((`date -d "$TODAY" "+%s"` - `date -d "$kcd" "+%s"`)/86400)) days old\n${key_status_accumulator}" + key_status_accumulator=" ${statusword} ${key_status_array[$keypos]}\n Restrictive policy in effect.\n" fi else let "s_no++" @@ -234,6 +250,11 @@ else 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]}" From 1f532eecb8bc1b9061696b3bcafbc78bdabfda8c Mon Sep 17 00:00:00 2001 From: Ville Walveranta Date: Mon, 19 Feb 2018 14:23:44 -0600 Subject: [PATCH 08/71] Added error handling for explicit deny (restrictive policy) conditions in aws-iam-rotate-keys.sh. --- aws-iam-rotate-keys.sh | 31 ++++++++++++++++++++++++++----- 1 file changed, 26 insertions(+), 5 deletions(-) diff --git a/aws-iam-rotate-keys.sh b/aws-iam-rotate-keys.sh index 77ec0f0..b195f37 100755 --- a/aws-iam-rotate-keys.sh +++ b/aws-iam-rotate-keys.sh @@ -132,14 +132,26 @@ else 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}'`) + 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" == "Inactive" ]]; then + 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 @@ -147,10 +159,14 @@ else 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}" + 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 ${key_status_array[$keypos]} is $(((`date -d "$TODAY" "+%s"` - `date -d "$kcd" "+%s"`)/86400)) days old\n${key_status_accumulator}" + key_status_accumulator=" ${statusword} ${key_status_array[$keypos]}\n Restrictive policy in effect.\n" fi else let "s_no++" @@ -234,6 +250,11 @@ else 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]}" From ce460afc7c8eb8a46fb63a977bf028fbcbf7749c Mon Sep 17 00:00:00 2001 From: Ville Walveranta Date: Sun, 25 Feb 2018 20:26:28 -0600 Subject: [PATCH 09/71] Added all AWS ennvars to source-to-clear-AWS-envvars.sh script. --- awscli-mfa/source-to-clear-AWS-envvars.sh | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/awscli-mfa/source-to-clear-AWS-envvars.sh b/awscli-mfa/source-to-clear-AWS-envvars.sh index 0875c1a..df16a56 100644 --- a/awscli-mfa/source-to-clear-AWS-envvars.sh +++ b/awscli-mfa/source-to-clear-AWS-envvars.sh @@ -8,7 +8,12 @@ if [ "$0" = "$BASH_SOURCE" ]; then echo fi -unset AWS_PROFILE unset AWS_ACCESS_KEY_ID unset AWS_SECRET_ACCESS_KEY unset AWS_SESSION_TOKEN +unset AWS_DEFAULT_REGION +unset AWS_DEFAULT_OUTPUT +unset AWS_PROFILE +unset AWS_CA_BUNDLE +unset AWS_SHARED_CREDENTIALS_FILE +unset AWS_CONFIG_FILE From f7b196d0c8b80a2eb3e1753f19a695ee046aa43d Mon Sep 17 00:00:00 2001 From: Ville Walveranta Date: Thu, 8 Mar 2018 19:39:26 -0600 Subject: [PATCH 10/71] Changed awscli-mfa.sh to use 'list-mfa-devices' instead of 'list-virtual-mfa-devices'; UI enhancements. --- awscli-mfa/awscli-mfa.sh | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/awscli-mfa/awscli-mfa.sh b/awscli-mfa/awscli-mfa.sh index 7224c07..08bb439 100755 --- a/awscli-mfa/awscli-mfa.sh +++ b/awscli-mfa/awscli-mfa.sh @@ -116,6 +116,19 @@ else ## PREREQS PASSED; PROCEED.. + echo + process_user_arn="$(aws sts get-caller-identity --output text --query 'Arn' 2>&1)" + + [[ "$process_user_arn" =~ ([^/]+)$ ]] && + process_username="${BASH_REMATCH[1]}" + if [[ "$process_username" =~ error ]]; then + echo "Default/selected profile is not functional; the script may not work as expected." + echo "Check the Default profile in your '~/.aws/credentials' file, as well as any 'AWS_' environment variables!" + else + echo "Running this process as user \"$process_username\"." + fi + echo + # declare the arrays declare -a cred_profiles declare -a cred_profile_status @@ -190,7 +203,7 @@ else # get MFA ARN if available # (obviously not available if a MFA device # isn't configured for the profile) - mfa_arn="$(aws iam list-virtual-mfa-devices --profile $profile_ident --output text --query "VirtualMFADevices[?User.Arn=='${user_arn}'].SerialNumber" 2>&1)" + mfa_arn="$(aws iam list-mfa-devices --profile $profile_ident --user-name ${cred_profile_user[$cred_profilecounter]} --output text --query "MFADevices[].SerialNumber" 2>&1)" if [[ "$mfa_arn" =~ ^arn:aws ]]; then mfa_arns[$cred_profilecounter]="$mfa_arn" else From 34e549acf1b08c9fa0bf778a285ac219845ad910 Mon Sep 17 00:00:00 2001 From: Ville Walveranta Date: Thu, 8 Mar 2018 19:45:28 -0600 Subject: [PATCH 11/71] Added sample MFA enforcement policy that works with the script. --- awscli-mfa/example-MFA-enforcement-policy.txt | 131 ++++++++++++++++++ 1 file changed, 131 insertions(+) create mode 100644 awscli-mfa/example-MFA-enforcement-policy.txt diff --git a/awscli-mfa/example-MFA-enforcement-policy.txt b/awscli-mfa/example-MFA-enforcement-policy.txt new file mode 100644 index 0000000..9175e48 --- /dev/null +++ b/awscli-mfa/example-MFA-enforcement-policy.txt @@ -0,0 +1,131 @@ +{ + "Version": "2012-10-17", + "Statement": [ + { + "Sid": "AllowAllUsersToListAccounts", + "Effect": "Allow", + "Action": [ + "iam:ListAccountAliases", + "iam:ListUsers" + ], + "Resource": [ + "arn:aws:iam::YOUR-AWS-ACCOUNT-ID-WITHOUT-HYPHENS:user/*" + ] + }, + { + "Sid": "AllowIndividualUserToSeeTheirAccountInformationAndCreateAccessKey", + "Effect": "Allow", + "Action": [ + "iam:ChangePassword", + "iam:CreateLoginProfile", + "iam:DeleteLoginProfile", + "iam:GetAccountPasswordPolicy", + "iam:GetAccountSummary", + "iam:GetLoginProfile", + "iam:UpdateLoginProfile" + ], + "Resource": [ + "arn:aws:iam::YOUR-AWS-ACCOUNT-ID-WITHOUT-HYPHENS:user/${aws:username}" + ] + }, + { + "Sid": "AllowIndividualUserToListTheirMFA", + "Effect": "Allow", + "Action": [ + "iam:ListMFADevices" + ], + "Resource": [ + "arn:aws:iam::YOUR-AWS-ACCOUNT-ID-WITHOUT-HYPHENS:mfa/${aws:username}", + "arn:aws:iam::YOUR-AWS-ACCOUNT-ID-WITHOUT-HYPHENS:user/${aws:username}" + ] + }, + { + "Sid": "AllowIndividualUserToManageTheirMFA", + "Effect": "Allow", + "Action": [ + "iam:CreateVirtualMFADevice", + "iam:DeleteVirtualMFADevice", + "iam:EnableMFADevice", + "iam:ResyncMFADevice" + ], + "Resource": [ + "arn:aws:iam::YOUR-AWS-ACCOUNT-ID-WITHOUT-HYPHENS:mfa/${aws:username}", + "arn:aws:iam::YOUR-AWS-ACCOUNT-ID-WITHOUT-HYPHENS:user/${aws:username}" + ] + }, + { + "Sid": "RequireActiveMFASessionToDeactivateMFADevice", + "Effect": "Allow", + "Action": [ + "iam:DeactivateMFADevice" + ], + "Resource": [ + "arn:aws:iam::YOUR-AWS-ACCOUNT-ID-WITHOUT-HYPHENS:mfa/${aws:username}", + "arn:aws:iam::YOUR-AWS-ACCOUNT-ID-WITHOUT-HYPHENS:user/${aws:username}" + ], + "Condition": { + "Bool": { + "aws:MultiFactorAuthPresent": true + } + } + }, + { + "Sid": "DenyEverythingExceptForBelowUnlessMFAd", + "Effect": "Deny", + "NotAction": [ + "iam:ListMFADevices", + "iam:ListUsers", + "iam:ListAccountAliases", + "iam:CreateVirtualMFADevice", + "iam:DeactivateMFADevice", + "iam:DeleteVirtualMFADevice", + "iam:EnableMFADevice", + "iam:ResyncMFADevice", + "iam:ChangePassword", + "iam:CreateLoginProfile", + "iam:DeleteLoginProfile", + "iam:GetAccountPasswordPolicy", + "iam:GetAccountSummary", + "iam:GetLoginProfile", + "iam:UpdateLoginProfile", + "iam:CreateAccessKey", + "iam:ListAccessKeys" + ], + "Resource": "*", + "Condition": { + "NumericLessThanIfExists": { + "aws:MultiFactorAuthAge": "300" + } + } + }, + { + "Sid": "DenyIamAccessToOtherAccountsUnlessMFAd", + "Effect": "Deny", + "Action": [ + "iam:ListVirtualMFADevices", + "iam:CreateVirtualMFADevice", + "iam:DeactivateMFADevice", + "iam:DeleteVirtualMFADevice", + "iam:EnableMFADevice", + "iam:ResyncMFADevice", + "iam:ChangePassword", + "iam:CreateLoginProfile", + "iam:DeleteLoginProfile", + "iam:GetAccountPasswordPolicy", + "iam:GetLoginProfile", + "iam:UpdateLoginProfile", + "iam:CreateAccessKey", + "iam:ListAccessKeys" + ], + "NotResource": [ + "arn:aws:iam::YOUR-AWS-ACCOUNT-ID-WITHOUT-HYPHENS:mfa/${aws:username}", + "arn:aws:iam::YOUR-AWS-ACCOUNT-ID-WITHOUT-HYPHENS:user/${aws:username}" + ], + "Condition": { + "NumericLessThanIfExists": { + "aws:MultiFactorAuthAge": "300" + } + } + } + ] +} From a7561c8ee272081433203952b78991752936df27 Mon Sep 17 00:00:00 2001 From: Ville Walveranta Date: Thu, 8 Mar 2018 23:27:42 -0600 Subject: [PATCH 12/71] Example MFA enforcement policy updates, awscli-mfa.sh fixes and todo comments. --- awscli-mfa/awscli-mfa.sh | 12 +++++++- awscli-mfa/example-MFA-enforcement-policy.txt | 29 ++++++++++--------- 2 files changed, 26 insertions(+), 15 deletions(-) diff --git a/awscli-mfa/awscli-mfa.sh b/awscli-mfa/awscli-mfa.sh index 08bb439..15c2a95 100755 --- a/awscli-mfa/awscli-mfa.sh +++ b/awscli-mfa/awscli-mfa.sh @@ -1,5 +1,15 @@ #!/usr/bin/env bash +# todo: detect AWS_* envvars in the environment and offer to copy the purge command +# to the clipboard before proceeding (otherwise executing as the selected profile +# which may or may not be active is using a session variable) + +# todo: resolve the "Executing this script as" user to the profile + +# todo: store the session init times if there is no other way to obtain +# the remaining session length. + + DEBUG="false" # uncomment below to enable the debug output #DEBUG="true" @@ -125,7 +135,7 @@ else echo "Default/selected profile is not functional; the script may not work as expected." echo "Check the Default profile in your '~/.aws/credentials' file, as well as any 'AWS_' environment variables!" else - echo "Running this process as user \"$process_username\"." + echo "Executing this script as the AWS/IAM user \"$process_username\"." fi echo diff --git a/awscli-mfa/example-MFA-enforcement-policy.txt b/awscli-mfa/example-MFA-enforcement-policy.txt index 9175e48..7d27804 100644 --- a/awscli-mfa/example-MFA-enforcement-policy.txt +++ b/awscli-mfa/example-MFA-enforcement-policy.txt @@ -5,11 +5,12 @@ "Sid": "AllowAllUsersToListAccounts", "Effect": "Allow", "Action": [ + "iam:GetAccountPasswordPolicy", "iam:ListAccountAliases", "iam:ListUsers" ], "Resource": [ - "arn:aws:iam::YOUR-AWS-ACCOUNT-ID-WITHOUT-HYPHENS:user/*" + "arn:aws:iam::ACCOUNT-ID-WITHOUT-HYPHENS:user/*" ] }, { @@ -25,7 +26,7 @@ "iam:UpdateLoginProfile" ], "Resource": [ - "arn:aws:iam::YOUR-AWS-ACCOUNT-ID-WITHOUT-HYPHENS:user/${aws:username}" + "arn:aws:iam::ACCOUNT-ID-WITHOUT-HYPHENS:user/${aws:username}" ] }, { @@ -35,8 +36,8 @@ "iam:ListMFADevices" ], "Resource": [ - "arn:aws:iam::YOUR-AWS-ACCOUNT-ID-WITHOUT-HYPHENS:mfa/${aws:username}", - "arn:aws:iam::YOUR-AWS-ACCOUNT-ID-WITHOUT-HYPHENS:user/${aws:username}" + "arn:aws:iam::ACCOUNT-ID-WITHOUT-HYPHENS:mfa/${aws:username}", + "arn:aws:iam::ACCOUNT-ID-WITHOUT-HYPHENS:user/${aws:username}" ] }, { @@ -49,8 +50,8 @@ "iam:ResyncMFADevice" ], "Resource": [ - "arn:aws:iam::YOUR-AWS-ACCOUNT-ID-WITHOUT-HYPHENS:mfa/${aws:username}", - "arn:aws:iam::YOUR-AWS-ACCOUNT-ID-WITHOUT-HYPHENS:user/${aws:username}" + "arn:aws:iam::ACCOUNT-ID-WITHOUT-HYPHENS:mfa/${aws:username}", + "arn:aws:iam::ACCOUNT-ID-WITHOUT-HYPHENS:user/${aws:username}" ] }, { @@ -60,8 +61,8 @@ "iam:DeactivateMFADevice" ], "Resource": [ - "arn:aws:iam::YOUR-AWS-ACCOUNT-ID-WITHOUT-HYPHENS:mfa/${aws:username}", - "arn:aws:iam::YOUR-AWS-ACCOUNT-ID-WITHOUT-HYPHENS:user/${aws:username}" + "arn:aws:iam::ACCOUNT-ID-WITHOUT-HYPHENS:mfa/${aws:username}", + "arn:aws:iam::ACCOUNT-ID-WITHOUT-HYPHENS:user/${aws:username}" ], "Condition": { "Bool": { @@ -93,8 +94,8 @@ ], "Resource": "*", "Condition": { - "NumericLessThanIfExists": { - "aws:MultiFactorAuthAge": "300" + "NumericGreaterThanIfExists": { + "aws:MultiFactorAuthAge": "900" } } }, @@ -118,12 +119,12 @@ "iam:ListAccessKeys" ], "NotResource": [ - "arn:aws:iam::YOUR-AWS-ACCOUNT-ID-WITHOUT-HYPHENS:mfa/${aws:username}", - "arn:aws:iam::YOUR-AWS-ACCOUNT-ID-WITHOUT-HYPHENS:user/${aws:username}" + "arn:aws:iam::ACCOUNT-ID-WITHOUT-HYPHENS:mfa/${aws:username}", + "arn:aws:iam::ACCOUNT-ID-WITHOUT-HYPHENS:user/${aws:username}" ], "Condition": { - "NumericLessThanIfExists": { - "aws:MultiFactorAuthAge": "300" + "NumericGreaterThanIfExists": { + "aws:MultiFactorAuthAge": "900" } } } From faab163dbab0c16cb3c661386f49903b2c6ab16a Mon Sep 17 00:00:00 2001 From: Ville Walveranta Date: Fri, 9 Mar 2018 17:57:04 -0600 Subject: [PATCH 13/71] Profile lookup based on aws_access_key_id now working; prep pro session timer ready. --- awscli-mfa/awscli-mfa.sh | 106 +++++++++++++++++++++++++++++++++++---- 1 file changed, 97 insertions(+), 9 deletions(-) diff --git a/awscli-mfa/awscli-mfa.sh b/awscli-mfa/awscli-mfa.sh index 15c2a95..7f1f871 100755 --- a/awscli-mfa/awscli-mfa.sh +++ b/awscli-mfa/awscli-mfa.sh @@ -4,8 +4,6 @@ # to the clipboard before proceeding (otherwise executing as the selected profile # which may or may not be active is using a session variable) -# todo: resolve the "Executing this script as" user to the profile - # todo: store the session init times if there is no other way to obtain # the remaining session length. @@ -25,6 +23,37 @@ DEBUG="false" # is 900 seconds. MFA_SESSION_LENGTH_IN_SECONDS=900 +# defined the standard location of the AWS credentials file +CREDFILE=~/.aws/credentials + +## 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="" + + maxIndex=${#arr[@]} + ((maxIndex--)) + + for (( i=0; i<=${maxIndex}; i++ )) + do + if [[ "${arr[$i]}" == "$key" ]]; then + result=$i + break + fi + done + + eval "$1=$result" +} + + ## PREREQUISITES CHECK # `exists` for commands @@ -65,9 +94,6 @@ elif [ ! -f ~/.aws/credentials ]; then exit 1 fi -# defined the standard location of the AWS credentials file -CREDFILE=~/.aws/credentials - # read the credentials file and make sure that at least one profile is configured ONEPROFILE="false" while IFS='' read -r line || [[ -n "$line" ]]; do @@ -86,7 +112,8 @@ if [[ "$ONEPROFILE" == "false" ]]; then else - # get default region and output format (since at least one profile should exist at this point) + # 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) default_output=$(aws --profile default configure get output) @@ -126,16 +153,77 @@ else ## PREREQS PASSED; PROCEED.. + # define profiles arrays + 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 + 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" =~ ^session_init_time[[:space:]]*=[[:space:]]*(.*)$ ]] && + profiles_session_init_time[$profiles_iterator]="${BASH_REMATCH[1]}" + + done < $CREDFILE + echo - process_user_arn="$(aws sts get-caller-identity --output text --query 'Arn' 2>&1)" + current_aws_access_key_id="$(aws configure get aws_access_key_id)" + idxLookup idx profiles_key_id[@] $current_aws_access_key_id + if [[ $idx != "" ]]; then + currently_selected_profile_ident="${profiles_ident[$idx]}" + else + currently_selected_profile_ident="unknown" + fi + + # todo: if the time is expired & env exists, prompt here for purging! + + process_user_arn="$(aws sts get-caller-identity --output text --query 'Arn' 2>&1)" [[ "$process_user_arn" =~ ([^/]+)$ ]] && process_username="${BASH_REMATCH[1]}" - if [[ "$process_username" =~ error ]]; then + if [[ "$process_username" =~ error ]] || + [[ "$currently_selected_profile_ident" == "unknown" ]]; then echo "Default/selected profile is not functional; the script may not work as expected." echo "Check the Default profile in your '~/.aws/credentials' file, as well as any 'AWS_' environment variables!" else - echo "Executing this script as the AWS/IAM user \"$process_username\"." + echo "Executing this script as the AWS/IAM user \"$process_username\" (profile \"$currently_selected_profile_ident\")." fi echo From bc9441bfd18c821754f3cab6f4c148024adde56d Mon Sep 17 00:00:00 2001 From: Ville Walveranta Date: Sat, 10 Mar 2018 15:30:04 -0600 Subject: [PATCH 14/71] Completed addInitTime in awscli-mfa.sh (as there is no way to retroactively pull MFA session init time via the CLI). --- awscli-mfa/awscli-mfa.sh | 35 ++++++++++++++++++++++++++++++++--- 1 file changed, 32 insertions(+), 3 deletions(-) diff --git a/awscli-mfa/awscli-mfa.sh b/awscli-mfa/awscli-mfa.sh index 7f1f871..6d56bff 100755 --- a/awscli-mfa/awscli-mfa.sh +++ b/awscli-mfa/awscli-mfa.sh @@ -4,9 +4,6 @@ # to the clipboard before proceeding (otherwise executing as the selected profile # which may or may not be active is using a session variable) -# todo: store the session init times if there is no other way to obtain -# the remaining session length. - DEBUG="false" # uncomment below to enable the debug output @@ -53,6 +50,28 @@ idxLookup() { eval "$1=$result" } +addInitTime() { + # $1 is the profile (ident) + + this_ident=$2 + this_time=$(date +%s) + + # find the selected profile's existing + # init time entry if one exists + idxLookup idx profiles_ident[@] $this_ident + profile_time=${profiles_session_init_time[$idx]} + + # update/add session init time + if [[ $profile_time != "" ]]; then + # time entry exists for the profile, update + sed -i '' -e "s/${profile_time}/${this_time}/g" $CREDFILE + 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}]\nsession_init_time = ${this_time}" + echo "$(awk -v var="${DATA//$'\n'/\\n}" '{sub(/'$replace_me'/,var)}1' $CREDFILE)" > $CREDFILE + fi +} ## PREREQUISITES CHECK @@ -215,6 +234,9 @@ else # todo: if the time is expired & env exists, prompt here for purging! + # if the the currently selected profile's profiles_type[$idx] == "session", check profiles_session_init_time[$idx] for expiration + # if expired and AWS envvars are present, it means that an expired MFA session is selected via envvar reference; cannot continue; offer to purge! + process_user_arn="$(aws sts get-caller-identity --output text --query 'Arn' 2>&1)" [[ "$process_user_arn" =~ ([^/]+)$ ]] && process_username="${BASH_REMATCH[1]}" @@ -312,6 +334,10 @@ else # (this is not 100% as it depends on the defined IAM access; # however if MFA enforcement is set, this should produce # a reliable result) + + # todo: 'iam get-user' should be either entirely replaced with timestamp comparison, + # or at least relegated to a secondary method since it's wholly IAM policy dependent + # and thus very random 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 @@ -532,6 +558,9 @@ else fi ## END DEBUG + # todo: this should be optional; the user might not want to make session data static; + # in such case also the 'aws_session_init_time' envvar should be set and accounted + # for in 'remaining.sh' utility script # 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"` From c7e155a1cd827ff349e2c5a3324493e44d4c8d67 Mon Sep 17 00:00:00 2001 From: Ville Walveranta Date: Sun, 11 Mar 2018 03:39:29 -0500 Subject: [PATCH 15/71] Numerous improvements to awscli-mfa.sh; started using recorded session init times, etc. --- awscli-mfa/awscli-mfa.sh | 279 ++++++++++++++++++---- awscli-mfa/source-to-clear-AWS-envvars.sh | 3 +- 2 files changed, 234 insertions(+), 48 deletions(-) diff --git a/awscli-mfa/awscli-mfa.sh b/awscli-mfa/awscli-mfa.sh index 6d56bff..bca23cd 100755 --- a/awscli-mfa/awscli-mfa.sh +++ b/awscli-mfa/awscli-mfa.sh @@ -1,9 +1,10 @@ #!/usr/bin/env bash -# todo: detect AWS_* envvars in the environment and offer to copy the purge command -# to the clipboard before proceeding (otherwise executing as the selected profile -# which may or may not be active is using a session variable) - +# todo: when initializing/selecting a new session, check the environment +# - is a profile selected via envvars? +# - is a non-static MFA session in effect (if no profile reference is +# in use, compare the aws_access_key_id to the ~/.aws/credentials); +# if so; warn of loss of the session (offer to make static?) DEBUG="false" # uncomment below to enable the debug output @@ -16,8 +17,7 @@ DEBUG="false" # is enforced in the IAM policy, and # is unaffected by this value. # -# The minimum valid session length -# is 900 seconds. +# The minimum valid session length is 900 seconds. MFA_SESSION_LENGTH_IN_SECONDS=900 # defined the standard location of the AWS credentials file @@ -25,6 +25,112 @@ CREDFILE=~/.aws/credentials ## FUNCTIONS +# `exists` for commands +exists() { + command -v "$1" >/dev/null 2>&1 +} + +checkEnvSession() { + # $1 is the check type + + local check_type=$1 + local 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_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]}" + + # makes sure that the MFA session has not expired (whether it's defined + # in the environment or in ~/.aws/credentials) + if [[ "$PRECHECK_AWS_SESSION_TOKEN" != "" ]] && + [[ "$PRECHECK_AWS_SESSION_INIT_TIME" != "" ]]; then + + getRemaining _ret $PRECHECK_AWS_SESSION_INIT_TIME + + if [[ "${_ret}" -eq 0 ]]; then + echo "THE MFA SESSION SELECTED IN THE ENVIRONMENT (${PRECHECK_AWS_PROFILE}) HAS EXPIRED. PURGE AND TRY AGAIN!" + exit 1 + fi + + elif [[ "$PRECHECK_AWS_PROFILE" =~ -mfasession$ ]]; then + # find the profile's init time entry if one exists + idxLookup idx profiles_ident[@] $PRECHECK_AWS_PROFILE + profile_time=${profiles_session_init_time[$idx]} + + if [[ "$profile_time" != "" ]]; then + getRemaining _ret $profile_time + if [[ "${_ret}" -eq 0 ]]; then + echo "THE MFA SESSION SELECTED IN THE ENVIRONMENT (${PRECHECK_AWS_PROFILE}) HAS EXPIRED. PURGE AND TRY AGAIN!" + exit 1 + fi + fi + fi + + if [[ "${AWS_PROFILE}" != "" ]] || + [[ "${AWS_ACCESS_KEY_ID}" != "" ]] || + [[ "${AWS_SECRET_ACCESS_KEY}" != "" ]] || + [[ "${AWS_SESSION_TOKEN}" != "" ]] || + [[ "${AWS_SESSION_INIT_TIME}" != "" ]] || + [[ "${AWS_DEFAULT_REGION}" != "" ]] || + [[ "${AWS_DEFAULT_OUTPUT}" != "" ]] || + [[ "${AWS_CA_BUNDLE}" != "" ]] || + [[ "${AWS_SHARED_CREDENTIALS_FILE}" != "" ]] || + [[ "${AWS_CONFIG_FILE}" != "" ]]; then + + echo + echo "** NOTE: SOME AWS_* ENVIRONMENT VARIABLES ARE CURRENTLY IN EFFECT:" + [[ "$PRECHECK_AWS_PROFILE" != "" ]] && echo "AWS_PROFILE: $PRECHECK_AWS_PROFILE" + [[ "$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_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() { @@ -50,16 +156,31 @@ idxLookup() { eval "$1=$result" } +getInitTime() { + # $1 is _ret + # $2 is the profile ident + + this_ident=$2 + + # 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}" +} + +# save the MFA session initialization timestamp +# in the session profile in ~/.aws/credentials addInitTime() { # $1 is the profile (ident) - this_ident=$2 + this_ident=$1 this_time=$(date +%s) # find the selected profile's existing # init time entry if one exists - idxLookup idx profiles_ident[@] $this_ident - profile_time=${profiles_session_init_time[$idx]} + getInitTime _ret "$this_ident" + profile_time=${_ret} # update/add session init time if [[ $profile_time != "" ]]; then @@ -68,18 +189,58 @@ addInitTime() { 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}]\nsession_init_time = ${this_time}" + 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 + profiles_session_init_time[$idx]=$this_time } -## PREREQUISITES CHECK +getRemaining() { + # $1 is _ret + # $2 is the timestamp -# `exists` for commands -exists() { - command -v "$1" >/dev/null 2>&1 + local timestamp=$2 + local this_time=$(date +%s) + local remaining=0 + + if [ ! -z "${timestamp##*[!0-9]*}" ]; then + let session_end=${timestamp}+${MFA_SESSION_LENGTH_IN_SECONDS} + if [[ $session_end -gt $this_time ]]; then + let remaining=${session_end}-${this_time} + else + remaining=0 + fi + else + remaining=-1 + fi + eval "$1=${remaining}" +} + +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}" } +## PREREQUISITES CHECK + # is AWS CLI installed? if ! exists aws ; then printf "\n******************************************************************************************************************************\n\ @@ -170,7 +331,7 @@ else echo "" >> "$CREDFILE" fi - ## PREREQS PASSED; PROCEED.. + ## FUNCTIONAL PREREQS PASSED; PROCEED WITH EXPIRED SESSION CHECK # define profiles arrays declare -a profiles_ident @@ -185,6 +346,7 @@ else # 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]} @@ -217,7 +379,7 @@ else [[ "$line" =~ ^aws_session_token[[:space:]]*=[[:space:]]*(.*)$ ]] && profiles_session_token[$profiles_iterator]="${BASH_REMATCH[1]}" - [[ "$line" =~ ^session_init_time[[:space:]]*=[[:space:]]*(.*)$ ]] && + [[ "$line" =~ ^aws_session_init_time[[:space:]]*=[[:space:]]*(.*)$ ]] && profiles_session_init_time[$profiles_iterator]="${BASH_REMATCH[1]}" done < $CREDFILE @@ -232,14 +394,13 @@ else currently_selected_profile_ident="unknown" fi - # todo: if the time is expired & env exists, prompt here for purging! - - # if the the currently selected profile's profiles_type[$idx] == "session", check profiles_session_init_time[$idx] for expiration - # if expired and AWS envvars are present, it means that an expired MFA session is selected via envvar reference; cannot continue; offer to purge! + # make sure environment doesn't have a stale session before we start + checkEnvSession "init" process_user_arn="$(aws sts get-caller-identity --output text --query 'Arn' 2>&1)" [[ "$process_user_arn" =~ ([^/]+)$ ]] && process_username="${BASH_REMATCH[1]}" + if [[ "$process_username" =~ error ]] || [[ "$currently_selected_profile_ident" == "unknown" ]]; then echo "Default/selected profile is not functional; the script may not work as expected." @@ -334,18 +495,32 @@ else # (this is not 100% as it depends on the defined IAM access; # however if MFA enforcement is set, this should produce # a reliable result) - - # todo: 'iam get-user' should be either entirely replaced with timestamp comparison, - # or at least relegated to a secondary method since it's wholly IAM policy dependent - # and thus very random + 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 + + getInitTime _ret_timestamp "$mfa_profile_ident" + getRemaining _ret_remaining ${_ret_timestamp} + + if [[ ${_ret_remaining} -eq 0 ]]; then + # session has expired + mfa_profile_status[$cred_profilecounter]="EXPIRED" - else - mfa_profile_status[$cred_profilecounter]="LIMITED" + 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 --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 fi @@ -398,9 +573,9 @@ else 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" + if [[ "${mfa_profile_status[$SELECTR]}" != "EXPIRED" && + "${mfa_profile_status[$SELECTR]}" != "" ]]; then + echo "${ITER}m: $i MFA profile (${mfa_profile_status[$SELECTR]})" fi echo @@ -444,8 +619,8 @@ else # if this is an MFA profile, it must be in OK or LIMITED status to select if [[ "$selprofile_mfa_check" != "" && - ( "${mfa_profile_status[$actual_selprofile]}" == "OK" || - "${mfa_profile_status[$actual_selprofile]}" == "LIMITED" ) ]]; then + "${mfa_profile_status[$actual_selprofile]}" != "EXPIRED" && + "${mfa_profile_status[$actual_selprofile]}" != "" ]]; then # get the parent profile name # transpose selection (starting from 1) to array index (starting from 0) @@ -565,12 +740,13 @@ else `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"` + # set init time in the static MFA profile (a custom key in ~/.aws/credentials) + addInitTime "${AWS_2AUTH_PROFILE}" # Make sure the final selection profile name has '-mfasession' suffix - # (it's not present when going from a base profile to an MFA profile) - if ! [[ "$final_selection" =~ -mfasession$ ]]; then - final_selection="${final_selection}-mfasession" - fi + # (before this assignment it's not present when going from a base profile to an MFA profile) + final_selection=$AWS_2AUTH_PROFILE + fi elif [[ "$active_mfa" == "false" ]]; then @@ -620,6 +796,8 @@ else AWS_ACCESS_KEY_ID=$(aws --profile $final_selection configure get aws_access_key_id) AWS_SECRET_ACCESS_KEY=$(aws --profile $final_selection configure get aws_secret_access_key) AWS_SESSION_TOKEN=$(aws --profile $final_selection configure get aws_session_token) + getInitTime _ret "$final_selection" + AWS_SESSION_INIT_TIME=${_ret} echo echo "========================================================================" @@ -656,23 +834,26 @@ else echo "unset AWS_ACCESS_KEY_ID" echo "unset AWS_SECRET_ACCESS_KEY" echo "unset AWS_SESSION_TOKEN" + echo "unset AWS_SESSION_INIT_TIME" echo -n "export AWS_PROFILE=${final_selection}; unset AWS_ACCESS_KEY_ID; unset AWS_SECRET_ACCESS_KEY; unset AWS_SESSION_TOKEN" | pbcopy else echo "export AWS_ACCESS_KEY_ID=${AWS_ACCESS_KEY_ID}" echo "export AWS_SECRET_ACCESS_KEY=${AWS_SECRET_ACCESS_KEY}" if [[ "$mfaprofile" == "true" ]]; then echo "export AWS_SESSION_TOKEN=${AWS_SESSION_TOKEN}" - echo -n "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_SESSION_TOKEN=${AWS_SESSION_TOKEN}" | pbcopy + echo "export AWS_SESSION_INIT_TIME=${AWS_SESSION_INIT_TIME}" + echo -n "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_SESSION_TOKEN=${AWS_SESSION_TOKEN}; export AWS_SESSION_INIT_TIME=${AWS_SESSION_INIT_TIME}" | pbcopy else echo "unset AWS_SESSION_TOKEN" - echo -n "export AWS_PROFILE=${final_selection}; export AWS_ACCESS_KEY_ID=${AWS_ACCESS_KEY_ID}; export AWS_SECRET_ACCESS_KEY=${AWS_SECRET_ACCESS_KEY}; unset AWS_SESSION_TOKEN" | pbcopy + echo "unset AWS_SESSION_INIT_TIME" + echo -n "export AWS_PROFILE=${final_selection}; export AWS_ACCESS_KEY_ID=${AWS_ACCESS_KEY_ID}; export AWS_SECRET_ACCESS_KEY=${AWS_SECRET_ACCESS_KEY}; unset AWS_SESSION_TOKEN; unset AWS_SESSION_INIT_TIME" | pbcopy echo fi fi echo echo "NOTE: Make sure to set/unset the environment with the new values as instructed above to make sure no conflicting profile/secret remains in the envrionment!" echo - echo -e "To conveniently remove any AWS profile/secret information from the environment, simply source the attached script, like so:\n'source ./source-to-clear-AWS-envvars.sh'" + echo -e "To conveniently remove any AWS profile/secret information from the environment, simply source the attached script, like so:\nsource ./source-to-clear-AWS-envvars.sh" echo elif [ "$OS" == "Linux" ]; then @@ -685,14 +866,16 @@ else echo "export AWS_SECRET_ACCESS_KEY=${AWS_SECRET_ACCESS_KEY}" if [[ "$mfaprofile" == "true" ]]; then echo "export AWS_SESSION_TOKEN=${AWS_SESSION_TOKEN}" + echo "export AWS_SESSION_INIT_TIME=${AWS_SESSION_INIT_TIME}" if exists xclip ; then - echo -n "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_SESSION_TOKEN=${AWS_SESSION_TOKEN}" | xclip -i + echo -n "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_SESSION_TOKEN=${AWS_SESSION_TOKEN}; export AWS_SESSION_INIT_TIME=${AWS_SESSION_INIT_TIME}" | xclip -i echo "(xclip found; the activation command is now on your X PRIMARY clipboard -- just paste on the command line, and press [ENTER])" fi else echo "unset AWS_SESSION_TOKEN" + echo "unset AWS_SESSION_INIT_TIME" if exists xclip ; then - echo -n "export AWS_PROFILE=${final_selection}; export AWS_ACCESS_KEY_ID=${AWS_ACCESS_KEY_ID}; export AWS_SECRET_ACCESS_KEY=${AWS_SECRET_ACCESS_KEY}; unset AWS_SESSION_TOKEN" | xclip -i + echo -n "export AWS_PROFILE=${final_selection}; export AWS_ACCESS_KEY_ID=${AWS_ACCESS_KEY_ID}; export AWS_SECRET_ACCESS_KEY=${AWS_SECRET_ACCESS_KEY}; unset AWS_SESSION_TOKEN; unset AWS_SESSION_INIT_TIME" | xclip -i echo "(xclip found; the activation command is now on your X PRIMARY clipboard -- just paste on the command line, and press [ENTER])" fi fi @@ -705,7 +888,7 @@ else echo echo "export AWS_PROFILE=${final_selection}; unset AWS_ACCESS_KEY_ID; unset AWS_SECRET_ACCESS_KEY; unset AWS_SESSION_TOKEN" echo - echo -e "To conveniently remove any AWS profile/secret information from the environment, simply source the attached script, like so:\n'source ./source-to-clear-AWS-envvars.sh'" + echo -e "To conveniently remove any AWS profile/secret information from the environment, simply source the attached script, like so:\nsource ./source-to-clear-AWS-envvars.sh" echo else # not macOS, not Linux, so some other weird OS like Windows.. @@ -718,13 +901,15 @@ else echo "export AWS_SECRET_ACCESS_KEY=${AWS_SECRET_ACCESS_KEY} \\" if [[ "$mfaprofile" == "true" ]]; then echo "export AWS_SESSION_TOKEN=${AWS_SESSION_TOKEN}" + echo "export AWS_SESSION_INIT_TIME=${AWS_SESSION_INIT_TIME}" else echo "unset AWS_SESSION_TOKEN" + echo "unset AWS_SESSION_INIT_TIME" fi echo - echo "..or execute the following to use named profile only, clearning any previoiusly set configuration variables:" + echo "..or execute the following to use named profile only, clearing any previously set configuration variables:" echo - echo "export AWS_PROFILE=${final_selection}; unset AWS_ACCESS_KEY_ID; unset AWS_SECRET_ACCESS_KEY; unset AWS_SESSION_TOKEN" + echo "export AWS_PROFILE=${final_selection}; unset AWS_ACCESS_KEY_ID; unset AWS_SECRET_ACCESS_KEY; unset AWS_SESSION_TOKEN; unset AWS_SESSION_INIT_TIME" echo echo -e "To conveniently remove any AWS profile/secret information from the environment, simply source the attached script, like so:\nsource ./source-to-clear-AWS-envvars.sh" echo diff --git a/awscli-mfa/source-to-clear-AWS-envvars.sh b/awscli-mfa/source-to-clear-AWS-envvars.sh index df16a56..fb3ccf8 100644 --- a/awscli-mfa/source-to-clear-AWS-envvars.sh +++ b/awscli-mfa/source-to-clear-AWS-envvars.sh @@ -4,13 +4,14 @@ if [ "$0" = "$BASH_SOURCE" ]; then echo echo "You must source this script to clear the AWS environment variables, like so:" echo - echo "source ./clear-aws.sh" + 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_DEFAULT_REGION unset AWS_DEFAULT_OUTPUT unset AWS_PROFILE From 29f584395e822f5d64b728f3c294a061f1c303bc Mon Sep 17 00:00:00 2001 From: Ville Walveranta Date: Sun, 11 Mar 2018 22:19:34 -0500 Subject: [PATCH 16/71] Completed changes to awscli-mfa.sh; the script now handles MFA session expiration times internally (and much more). --- awscli-mfa/awscli-mfa.sh | 271 ++++++++++++------ awscli-mfa/example-MFA-enforcement-policy.txt | 4 +- 2 files changed, 178 insertions(+), 97 deletions(-) diff --git a/awscli-mfa/awscli-mfa.sh b/awscli-mfa/awscli-mfa.sh index bca23cd..a27866d 100755 --- a/awscli-mfa/awscli-mfa.sh +++ b/awscli-mfa/awscli-mfa.sh @@ -1,24 +1,28 @@ #!/usr/bin/env bash -# todo: when initializing/selecting a new session, check the environment -# - is a profile selected via envvars? -# - is a non-static MFA session in effect (if no profile reference is -# in use, compare the aws_access_key_id to the ~/.aws/credentials); -# if so; warn of loss of the session (offer to make static?) +# todo: support different session lengths (different +# AWS accounts may have different maximum +# allowed session lengths) DEBUG="false" # uncomment below to enable the debug output #DEBUG="true" -# 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. +# Set the 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. # -# The minimum valid session length is 900 seconds. -MFA_SESSION_LENGTH_IN_SECONDS=900 +# 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 # defined the standard location of the AWS credentials file CREDFILE=~/.aws/credentials @@ -30,6 +34,7 @@ exists() { command -v "$1" >/dev/null 2>&1 } +# precheck envvars for existing/stale session definitions checkEnvSession() { # $1 is the check type @@ -83,11 +88,7 @@ checkEnvSession() { [[ "$PRECHECK_AWS_SESSION_INIT_TIME" != "" ]]; then getRemaining _ret $PRECHECK_AWS_SESSION_INIT_TIME - - if [[ "${_ret}" -eq 0 ]]; then - echo "THE MFA SESSION SELECTED IN THE ENVIRONMENT (${PRECHECK_AWS_PROFILE}) HAS EXPIRED. PURGE AND TRY AGAIN!" - exit 1 - fi + [[ "${_ret}" -eq 0 ]] && continue_maybe elif [[ "$PRECHECK_AWS_PROFILE" =~ -mfasession$ ]]; then # find the profile's init time entry if one exists @@ -96,13 +97,12 @@ checkEnvSession() { if [[ "$profile_time" != "" ]]; then getRemaining _ret $profile_time - if [[ "${_ret}" -eq 0 ]]; then - echo "THE MFA SESSION SELECTED IN THE ENVIRONMENT (${PRECHECK_AWS_PROFILE}) HAS EXPIRED. PURGE AND TRY AGAIN!" - exit 1 - fi + [[ "${_ret}" -eq 0 ]] && continue_maybe fi fi + # detect and print informative notice of + # effective AWS envvars if [[ "${AWS_PROFILE}" != "" ]] || [[ "${AWS_ACCESS_KEY_ID}" != "" ]] || [[ "${AWS_SECRET_ACCESS_KEY}" != "" ]] || @@ -156,6 +156,7 @@ idxLookup() { eval "$1=$result" } +# return the MFA session init time for the given profile getInitTime() { # $1 is _ret # $2 is the profile ident @@ -198,6 +199,9 @@ addInitTime() { profiles_session_init_time[$idx]=$this_time } +# return remaining seconds for the given timestamp; +# uses the MFA_SESSION_LENGTH_IN_SECONDS global var; +# 0 indicates expired, -1 indicates NaN input getRemaining() { # $1 is _ret # $2 is the timestamp @@ -219,6 +223,9 @@ getRemaining() { eval "$1=${remaining}" } +# return printable output for given 'remaining' timestamp +# (must be pre-incremented with MFA_SESSION_LENGTH_IN_SECONDS, +# such as getRemaining() output) getPrintableTimeRemaining() { # $1 is _ret # $2 is the timestamp @@ -239,6 +246,30 @@ getPrintableTimeRemaining() { eval "$1=${response}" } +continue_maybe() { + echo -e "THE MFA SESSION SELECTED IN THE ENVIRONMENT (${PRECHECK_AWS_PROFILE}) HAS EXPIRED.\n" + read -s -p "Do you want to continue with the default profile? - [Y]n " -n 1 -r + if [[ $REPLY =~ ^[Yy]$ ]] || + [[ $REPLY == "" ]]; then + + unset AWS_PROFILE + unset AWS_ACCESS_KEY_ID + unset AWS_SECRET_ACCESS_KEY + unset AWS_SESSION_TOKEN + unset AWS_SESSION_INIT_TIME + unset AWS_DEFAULT_REGION + unset AWS_DEFAULT_OUTPUT + unset AWS_CA_BUNDLE + unset AWS_SHARED_CREDENTIALS_FILE + unset AWS_CONFIG_FILE + +# use_profile='--profile default' + else + echo -e "\n\nExecute \"source ./source-to-clear-AWS-envvars.sh\", and try again to proceed.\n" + exit 1 + fi +} + ## PREREQUISITES CHECK # is AWS CLI installed? @@ -333,15 +364,17 @@ else ## FUNCTIONAL PREREQS PASSED; PROCEED WITH EXPIRED SESSION CHECK - # define profiles arrays + # 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 + persistent_MFA="false" profiles_iterator=0 profiles_init=0 + use_profile="" # ugly hack to relate different values because # macOS *still* does not provide bash 4.x by default, @@ -384,6 +417,9 @@ else done < $CREDFILE + # make sure environment doesn't have a stale session before we start + checkEnvSession "init" + echo current_aws_access_key_id="$(aws configure get aws_access_key_id)" @@ -394,10 +430,7 @@ else currently_selected_profile_ident="unknown" fi - # make sure environment doesn't have a stale session before we start - checkEnvSession "init" - - process_user_arn="$(aws sts get-caller-identity --output text --query 'Arn' 2>&1)" + process_user_arn="$(aws $use_profile sts get-caller-identity --output text --query 'Arn' 2>&1)" [[ "$process_user_arn" =~ ([^/]+)$ ]] && process_username="${BASH_REMATCH[1]}" @@ -406,6 +439,7 @@ else echo "Default/selected profile is not functional; the script may not work as expected." echo "Check the Default profile in your '~/.aws/credentials' file, as well as any 'AWS_' environment variables!" else + echo echo "Executing this script as the AWS/IAM user \"$process_username\" (profile \"$currently_selected_profile_ident\")." fi echo @@ -462,7 +496,7 @@ else cred_profile_user[$cred_profilecounter]="$profile_username" fi - # find the existing MFA sessions for the current profile + # 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)\]$ ]] && @@ -473,7 +507,7 @@ else # 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 reliable result) + # a reasonably reliable result) 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" @@ -491,11 +525,11 @@ else mfa_arns[$cred_profilecounter]="" fi - # if existing MFA profile was found, check its status - # (this is not 100% as it depends on the defined IAM access; - # however if MFA enforcement is set, this should produce - # a reliable result) - + # 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" @@ -704,16 +738,14 @@ else 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 "NOW GETTING THE 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 \ + --duration $MFA_SESSION_LENGTH_IN_SECONDS \ --serial-number $ARN_OF_MFA \ - --token-code $MFA_TOKEN_CODE \ + --token-code $mfacode \ --output text | awk '{ print $2, $4, $5 }') if [ -z "$AWS_ACCESS_KEY_ID" ]; then @@ -725,28 +757,40 @@ else # this is used to determine whether to print MFA questions/details mfaprofile="true" + # optioanlly set the persistent (~/.aws/credentials entries): + # aws_access_key_id, aws_secret_access_key, and aws_session_token + # for the MFA profile + echo + getPrintableTimeRemaining _ret ${MFA_SESSION_LENGTH_IN_SECONDS} + validity_period=${_ret} + echo -e "Make this MFA session persistent (saved in ~/.aws/credentials)\nso that you can return to it during its validity period (${validity_period})?" + read -s -p "If you answer 'No', only the envvars will be used? [Y]n " -n 1 -r + if [[ $REPLY =~ ^[Yy]$ ]] || + [[ $REPLY == "" ]]; then + + persistent_MFA="true" + `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"` + # set init time in the static MFA profile (a custom key in ~/.aws/credentials) + addInitTime "${AWS_2AUTH_PROFILE}" + else + # for non-persistent (envvars only), set the init time + AWS_SESSION_INIT_TIME=$(date +%s) + fi + + # 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 + ## DEBUG if [ "$DEBUG" == "true" ]; then 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" fi - ## END DEBUG - - # todo: this should be optional; the user might not want to make session data static; - # in such case also the 'aws_session_init_time' envvar should be set and accounted - # for in 'remaining.sh' utility script - # 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"` - # set init time in the static MFA profile (a custom key in ~/.aws/credentials) - addInitTime "${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 - + ## END DEBUG fi elif [[ "$active_mfa" == "false" ]]; then @@ -756,12 +800,13 @@ else fi # get region and output format for the selected profile - get_region=$(aws --profile $final_selection configure get region) - get_output=$(aws --profile $final_selection configure get output) + AWS_DEFAULT_REGION=$(aws --profile $final_selection configure get region) + AWS_DEFAULT_OUTPUT=$(aws --profile $final_selection configure get output) # 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 [[ "${get_region}" == "" ]]; then + # 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 [[ "${profile_region[$actual_selprofile]}" != "" && "${mfaprofile}" == "true" ]]; then @@ -773,11 +818,15 @@ else echo "Region had not been configured for the selected profile; it has been set to the default region ('${default_region}')." fi - get_region="${set_new_region}" - `aws --profile $final_selection configure set region "${set_new_region}"` + AWS_DEFAULT_REGION="${set_new_region}" + if [[ "$mfacode" == "" ]] || + ( [[ "$mfacode" != "" ]] && [[ "$persistent_MFA" == "true" ]] ); then + + `aws --profile $final_selection configure set region "${set_new_region}"` + fi fi - if [ "${get_output}" == "" ]; then + if [ "${AWS_DEFAULT_OUTPUT}" == "" ]; then # retrieve parent profile output format if an MFA profile if [[ "${profile_output[$actual_selprofile]}" != "" && "${mfaprofile}" == "true" ]]; then @@ -789,15 +838,25 @@ else echo "Output format had not been configured for the selected profile; it has been set to the default output format ('${default_output}')." fi - get_output="${set_new_output}" - `aws --profile $final_selection configure set output "${set_new_output}"` + AWS_DEFAULT_OUTPUT="${set_new_output}" + if [[ "$mfacode" == "" ]] || + ( [[ "$mfacode" != "" ]] && [[ "$persistent_MFA" == "true" ]] ); then + + `aws --profile $final_selection configure set output "${set_new_output}"` + fi fi - AWS_ACCESS_KEY_ID=$(aws --profile $final_selection configure get aws_access_key_id) - AWS_SECRET_ACCESS_KEY=$(aws --profile $final_selection configure get aws_secret_access_key) - AWS_SESSION_TOKEN=$(aws --profile $final_selection configure get aws_session_token) - getInitTime _ret "$final_selection" - AWS_SESSION_INIT_TIME=${_ret} + 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 --profile $final_selection configure get aws_access_key_id) + AWS_SECRET_ACCESS_KEY=$(aws --profile $final_selection configure get aws_secret_access_key) + + if [[ "$mfaprofile" == "true" ]]; then # this is a persistent MFA profile (a subset of [[ "$mfacode" == "" ]]) + AWS_SESSION_TOKEN=$(aws --profile $final_selection configure get aws_session_token) + getInitTime _ret "$final_selection" + AWS_SESSION_INIT_TIME=${_ret} + fi + fi echo echo "========================================================================" @@ -810,18 +869,33 @@ else 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 "Region is set to: $AWS_DEFAULT_REGION" + echo "Output format is set to: $AWS_DEFAULT_OUTPUT" echo - # print env export secrets? - secrets_out="false" - read -p "Do you want to export the selected profile's secrets to the environment (for s3cmd, etc)? - y[N] " -n 1 -r - if [[ $REPLY =~ ^[Yy]$ ]]; then + 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 "Do you want to export the selected profile's secrets to the environment (for s3cmd, etc)? - y[N] " -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 - echo - echo + + if [[ "$mfacode" != "" ]] && [[ "$persistent_MFA" == "false" ]]; then + echo "*** THIS IS A NON-PERSISTENT MFA SESSION; YOU *MUST* EXPORT THE BELOW ENVVARS TO ACTIVATE ***" + echo + fi if [[ "$OS" == "macOS" ]]; then @@ -833,27 +907,31 @@ else if [[ "$secrets_out" == "false" ]]; then echo "unset AWS_ACCESS_KEY_ID" echo "unset AWS_SECRET_ACCESS_KEY" - echo "unset AWS_SESSION_TOKEN" + echo "unset AWS_DEFAULT_REGION" + echo "unset AWS_DEFAULT_OUTPUT" echo "unset AWS_SESSION_INIT_TIME" - echo -n "export AWS_PROFILE=${final_selection}; unset AWS_ACCESS_KEY_ID; unset AWS_SECRET_ACCESS_KEY; unset AWS_SESSION_TOKEN" | pbcopy + echo "unset AWS_SESSION_TOKEN" + echo -n "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_DEFAULT_REGION; unset AWS_DEFAULT_OUTPUT" | pbcopy 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_TOKEN=${AWS_SESSION_TOKEN}" echo "export AWS_SESSION_INIT_TIME=${AWS_SESSION_INIT_TIME}" - echo -n "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_SESSION_TOKEN=${AWS_SESSION_TOKEN}; export AWS_SESSION_INIT_TIME=${AWS_SESSION_INIT_TIME}" | pbcopy + echo "export AWS_SESSION_TOKEN=${AWS_SESSION_TOKEN}" + echo -n "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_TOKEN=${AWS_SESSION_TOKEN}; export AWS_SESSION_INIT_TIME=${AWS_SESSION_INIT_TIME}" | pbcopy else - echo "unset AWS_SESSION_TOKEN" echo "unset AWS_SESSION_INIT_TIME" - echo -n "export AWS_PROFILE=${final_selection}; export AWS_ACCESS_KEY_ID=${AWS_ACCESS_KEY_ID}; export AWS_SECRET_ACCESS_KEY=${AWS_SECRET_ACCESS_KEY}; unset AWS_SESSION_TOKEN; unset AWS_SESSION_INIT_TIME" | pbcopy + echo "unset AWS_SESSION_TOKEN" + echo -n "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_TOKEN; unset AWS_SESSION_INIT_TIME" | pbcopy echo fi fi echo - echo "NOTE: Make sure to set/unset the environment with the new values as instructed above to make sure no conflicting profile/secret remains in the envrionment!" + echo "NOTE: Make sure to set/unset all the new values as instructed above to make sure no conflicting profile/secrets remain in the envrionment!" echo - echo -e "To conveniently remove any AWS profile/secret information from the environment, simply source the attached script, like so:\nsource ./source-to-clear-AWS-envvars.sh" + echo -e "To conveniently remove any AWS profile/secrets information from the environment, simply source the attached script, like so:\nsource ./source-to-clear-AWS-envvars.sh" echo elif [ "$OS" == "Linux" ]; then @@ -864,18 +942,20 @@ 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_TOKEN=${AWS_SESSION_TOKEN}" echo "export AWS_SESSION_INIT_TIME=${AWS_SESSION_INIT_TIME}" + echo "export AWS_SESSION_TOKEN=${AWS_SESSION_TOKEN}" if exists xclip ; then - echo -n "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_SESSION_TOKEN=${AWS_SESSION_TOKEN}; export AWS_SESSION_INIT_TIME=${AWS_SESSION_INIT_TIME}" | xclip -i + echo -n "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_TOKEN=${AWS_SESSION_TOKEN}; export AWS_SESSION_INIT_TIME=${AWS_SESSION_INIT_TIME}" | xclip -i echo "(xclip found; the activation command is now on your X PRIMARY clipboard -- just paste on the command line, and press [ENTER])" fi else - echo "unset AWS_SESSION_TOKEN" echo "unset AWS_SESSION_INIT_TIME" + echo "unset AWS_SESSION_TOKEN" if exists xclip ; then - echo -n "export AWS_PROFILE=${final_selection}; export AWS_ACCESS_KEY_ID=${AWS_ACCESS_KEY_ID}; export AWS_SECRET_ACCESS_KEY=${AWS_SECRET_ACCESS_KEY}; unset AWS_SESSION_TOKEN; unset AWS_SESSION_INIT_TIME" | xclip -i + echo -n "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_TOKEN; unset AWS_SESSION_INIT_TIME" | xclip -i echo "(xclip found; the activation command is now on your X PRIMARY clipboard -- just paste on the command line, and press [ENTER])" fi fi @@ -886,9 +966,9 @@ else echo echo ".. or execute the following to use named profile only, clearning any previoiusly set configuration variables:" echo - echo "export AWS_PROFILE=${final_selection}; unset AWS_ACCESS_KEY_ID; unset AWS_SECRET_ACCESS_KEY; unset AWS_SESSION_TOKEN" + echo "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_DEFAULT_REGION; unset AWS_DEFAULT_OUTPUT" echo - echo -e "To conveniently remove any AWS profile/secret information from the environment, simply source the attached script, like so:\nsource ./source-to-clear-AWS-envvars.sh" + echo -e "To conveniently remove any AWS profile/secrets information from the environment, simply source the attached script, like so:\nsource ./source-to-clear-AWS-envvars.sh" echo else # not macOS, not Linux, so some other weird OS like Windows.. @@ -899,22 +979,23 @@ 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_TOKEN=${AWS_SESSION_TOKEN}" echo "export AWS_SESSION_INIT_TIME=${AWS_SESSION_INIT_TIME}" + echo "export AWS_SESSION_TOKEN=${AWS_SESSION_TOKEN}" else - echo "unset AWS_SESSION_TOKEN" echo "unset AWS_SESSION_INIT_TIME" + echo "unset AWS_SESSION_TOKEN" fi echo echo "..or execute the following to use named profile only, clearing any previously set configuration variables:" echo - echo "export AWS_PROFILE=${final_selection}; unset AWS_ACCESS_KEY_ID; unset AWS_SECRET_ACCESS_KEY; unset AWS_SESSION_TOKEN; unset AWS_SESSION_INIT_TIME" + echo "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_DEFAULT_REGION; unset AWS_DEFAULT_OUTPUT" echo - echo -e "To conveniently remove any AWS profile/secret information from the environment, simply source the attached script, like so:\nsource ./source-to-clear-AWS-envvars.sh" + echo -e "To conveniently remove any AWS profile/secrets information from the environment, simply source the attached script, like so:\nsource ./source-to-clear-AWS-envvars.sh" echo fi echo - fi diff --git a/awscli-mfa/example-MFA-enforcement-policy.txt b/awscli-mfa/example-MFA-enforcement-policy.txt index 7d27804..6662d57 100644 --- a/awscli-mfa/example-MFA-enforcement-policy.txt +++ b/awscli-mfa/example-MFA-enforcement-policy.txt @@ -95,7 +95,7 @@ "Resource": "*", "Condition": { "NumericGreaterThanIfExists": { - "aws:MultiFactorAuthAge": "900" + "aws:MultiFactorAuthAge": "32400" } } }, @@ -124,7 +124,7 @@ ], "Condition": { "NumericGreaterThanIfExists": { - "aws:MultiFactorAuthAge": "900" + "aws:MultiFactorAuthAge": "32400" } } } From 8aa0ed51e0c25532ee6ba0cc9cbd3dcf2d3ae503 Mon Sep 17 00:00:00 2001 From: Ville Walveranta Date: Mon, 12 Mar 2018 03:15:16 -0500 Subject: [PATCH 17/71] Finalized remaining.sh status/utility script for awscli-mfa.sh --- awscli-mfa/remaining.sh | 274 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 274 insertions(+) create mode 100755 awscli-mfa/remaining.sh diff --git a/awscli-mfa/remaining.sh b/awscli-mfa/remaining.sh new file mode 100755 index 0000000..e4a8068 --- /dev/null +++ b/awscli-mfa/remaining.sh @@ -0,0 +1,274 @@ +#!/bin/bash + +# Set the 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. +# +# 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 + +# defined the standard location of the AWS credentials file +CREDFILE=~/.aws/credentials + + +# 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="" + + maxIndex=${#arr[@]} + ((maxIndex--)) + + for (( i=0; i<=${maxIndex}; i++ )) + do + if [[ "${arr[$i]}" == "$key" ]]; then + result=$i + break + fi + done + + eval "$1=$result" +} + +# return remaining seconds for the given timestamp; +# uses the MFA_SESSION_LENGTH_IN_SECONDS global var; +# 0 indicates expired, -1 indicates NaN input +getRemaining() { + # $1 is _ret + # $2 is the timestamp + + local timestamp=$2 + local this_time=$(date +%s) + local remaining=0 + + if [ ! -z "${timestamp##*[!0-9]*}" ]; then + let session_end=${timestamp}+${MFA_SESSION_LENGTH_IN_SECONDS} + if [[ $session_end -gt $this_time ]]; then + let 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 MFA_SESSION_LENGTH_IN_SECONDS, +# 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 + if [ "$idx" = "" ]; then + if [[ ${AWS_PROFILE} != "" ]]; then + matched="(not the persistent session)" + else + matched="(not a persistent session)" + fi + else + if [[ ${AWS_PROFILE} != "" ]]; then + matched="(same as the persistent session)" + else + matched="(same as the persistent session \"${profiles_ident[$idx]}\")" + fi + fi + + [[ ${AWS_PROFILE} == "" ]] && AWS_PROFILE="[unnamed]" + + echo "AWS_PROFILE IN THE ENVIRONMENT: ${AWS_PROFILE} ${matched}" + + if [[ "$AWS_SESSION_INIT_TIME" != "" ]]; then + + getRemaining _ret_remaining $AWS_SESSION_INIT_TIME + getPrintableTimeRemaining _ret ${_ret_remaining} + if [ "${_ret}" = "EXPIRED" ]; then + echo " MFA SESSION EXPIRED; YOU SHOULD PURGE THE ENV BY EXECUTING 'source ./source-to-clear-AWS-envvars.sh'" + else + echo " MFA SESSION REMAINING: ${_ret}" + fi + fi +} + +# -- end functions -- + + +# 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]}" + +IN_ENV_SESSION_TIME=0 +if [[ "$AWS_SESSION_INIT_TIME" != "" ]]; then + let IN_ENV_SESSION_TIME=${AWS_SESSION_INIT_TIME}+${MFA_SESSION_LENGTH_IN_SECONDS} + ENV_TIME="true" +else + ENV_TIME="false" +fi + +# COLLECT AWS_SESSION DATA FROM ~/.aws/credentials + +# 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 +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 + + 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]}" + +echo +echo + +done < $CREDFILE + + +# lookup AWS_PROFILE, AWS_ACCESS_KEY_ID in ~/.aws/credentials +# -> profile is not found or if AWS_ACCESS_KEY_ID is not in credentials, +# this is an env-only profile (differentiate with TOKEN). For MFA +# sessions calculate remaining time (suggest/provide purge commands +# if expired) + +# calculate the remaining session times for the MFA sessions also in +# the ~/.aws/credentials file + +echo +echo "ENVIRONMENT" +echo "-----------" +echo + +if [[ "$AWS_PROFILE" != "" ]]; then + if [[ "$AWS_ACCESS_KEY_ID" != "" ]]; then + sessionData + else + echo "AWS_PROFILE SELECTING A PERSISTENT PROFILE: ${AWS_PROFILE}" + fi +else + if [[ "$AWS_ACCESS_KEY_ID" != "" ]]; then + sessionData + else + echo "NO AWS PROFILE PRESENT IN THE ENVIRONMENT" + fi +fi + +echo +echo +echo "PERSISTENT MFA SESSIONS (in ~/.aws/credentials)" +echo "-----------------------------------------------" +echo + +maxIndex=${#profiles_ident[@]} +((maxIndex--)) + +live_session_counter=0 + +for (( i=0; i<=${maxIndex}; i++ )) +do + if [[ "${profiles_type[$i]}" == "session" ]]; then + echo "MFA SESSION IDENT: ${profiles_ident[$i]}" + if [[ "${profiles_session_init_time[$i]}" != "" ]]; then + getRemaining _ret_remaining ${profiles_session_init_time[$i]} + getPrintableTimeRemaining _ret ${_ret_remaining} + if [ "${_ret}" = "EXPIRED" ]; then + echo " MFA SESSION EXPIRED" + else + ((live_session_counter++)) + echo " MFA SESSION REMAINING: ${_ret}" + fi + else + echo " no recorded init time (legacy or external init?)" + fi + echo + fi +done + +echo + +if [[ "$live_session_counter" -gt 0 ]]; then + echo "** Execute awscli-mfa.sh to select an active MFA session." + echo +fi From 3272f38d164c9749ea53efa8ff9a1638b0a23d74 Mon Sep 17 00:00:00 2001 From: Ville Walveranta Date: Tue, 13 Mar 2018 02:52:18 -0500 Subject: [PATCH 18/71] Added the ability to set profile-specific session length. Plus other fixes. --- awscli-mfa/awscli-mfa.sh | 202 ++++++++++++++++------ awscli-mfa/remaining.sh | 149 ++++++++++++---- awscli-mfa/source-to-clear-AWS-envvars.sh | 1 + 3 files changed, 260 insertions(+), 92 deletions(-) diff --git a/awscli-mfa/awscli-mfa.sh b/awscli-mfa/awscli-mfa.sh index a27866d..2c2f748 100755 --- a/awscli-mfa/awscli-mfa.sh +++ b/awscli-mfa/awscli-mfa.sh @@ -1,30 +1,29 @@ #!/usr/bin/env bash -# todo: support different session lengths (different -# AWS accounts may have different maximum -# allowed session lengths) - DEBUG="false" # uncomment below to enable the debug output #DEBUG="true" -# Set the 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. +# 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. +# 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 -# defined the standard location of the AWS credentials file +# define the standard location of the AWS credentials and config files +CONFFILE=~/.aws/config CREDFILE=~/.aws/credentials ## FUNCTIONS @@ -62,6 +61,10 @@ checkEnvSession() { [[ "$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]}" @@ -87,7 +90,7 @@ checkEnvSession() { if [[ "$PRECHECK_AWS_SESSION_TOKEN" != "" ]] && [[ "$PRECHECK_AWS_SESSION_INIT_TIME" != "" ]]; then - getRemaining _ret $PRECHECK_AWS_SESSION_INIT_TIME + getRemaining _ret $PRECHECK_AWS_SESSION_INIT_TIME $PRECHECK_AWS_SESSION_DURATION [[ "${_ret}" -eq 0 ]] && continue_maybe elif [[ "$PRECHECK_AWS_PROFILE" =~ -mfasession$ ]]; then @@ -95,8 +98,10 @@ checkEnvSession() { idxLookup idx profiles_ident[@] $PRECHECK_AWS_PROFILE profile_time=${profiles_session_init_time[$idx]} + getDuration parent_duration $PRECHECK_AWS_PROFILE + if [[ "$profile_time" != "" ]]; then - getRemaining _ret $profile_time + getRemaining _ret $profile_time $parent_duration [[ "${_ret}" -eq 0 ]] && continue_maybe fi fi @@ -108,6 +113,7 @@ checkEnvSession() { [[ "${AWS_SECRET_ACCESS_KEY}" != "" ]] || [[ "${AWS_SESSION_TOKEN}" != "" ]] || [[ "${AWS_SESSION_INIT_TIME}" != "" ]] || + [[ "${AWS_SESSION_DURATION}" != "" ]] || [[ "${AWS_DEFAULT_REGION}" != "" ]] || [[ "${AWS_DEFAULT_OUTPUT}" != "" ]] || [[ "${AWS_CA_BUNDLE}" != "" ]] || @@ -121,6 +127,7 @@ checkEnvSession() { [[ "$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" @@ -141,6 +148,7 @@ idxLookup() { declare -a arr=("${!2}") local key=$3 local result="" + local maxIndex maxIndex=${#arr[@]} ((maxIndex--)) @@ -156,20 +164,6 @@ idxLookup() { eval "$1=$result" } -# return the MFA session init time for the given profile -getInitTime() { - # $1 is _ret - # $2 is the profile ident - - this_ident=$2 - - # 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}" -} - # save the MFA session initialization timestamp # in the session profile in ~/.aws/credentials addInitTime() { @@ -199,19 +193,61 @@ addInitTime() { profiles_session_init_time[$idx]=$this_time } -# return remaining seconds for the given timestamp; -# uses the MFA_SESSION_LENGTH_IN_SECONDS global var; +# 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=$(date +%s) local remaining=0 + [[ "${duration}" == "" ]] && + duration=$MFA_SESSION_LENGTH_IN_SECONDS + if [ ! -z "${timestamp##*[!0-9]*}" ]; then - let session_end=${timestamp}+${MFA_SESSION_LENGTH_IN_SECONDS} + let session_end=${timestamp}+${duration} if [[ $session_end -gt $this_time ]]; then let remaining=${session_end}-${this_time} else @@ -224,7 +260,7 @@ getRemaining() { } # return printable output for given 'remaining' timestamp -# (must be pre-incremented with MFA_SESSION_LENGTH_IN_SECONDS, +# (must be pre-incremented with profile duration, # such as getRemaining() output) getPrintableTimeRemaining() { # $1 is _ret @@ -247,7 +283,7 @@ getPrintableTimeRemaining() { } continue_maybe() { - echo -e "THE MFA SESSION SELECTED IN THE ENVIRONMENT (${PRECHECK_AWS_PROFILE}) HAS EXPIRED.\n" + echo -e "\nTHE MFA SESSION SELECTED IN THE ENVIRONMENT (${PRECHECK_AWS_PROFILE}) HAS EXPIRED.\n" read -s -p "Do you want to continue with the default profile? - [Y]n " -n 1 -r if [[ $REPLY =~ ^[Yy]$ ]] || [[ $REPLY == "" ]]; then @@ -257,6 +293,7 @@ continue_maybe() { 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 @@ -363,6 +400,7 @@ else fi ## FUNCTIONAL PREREQS PASSED; PROCEED WITH EXPIRED SESSION CHECK + ## AMD CUSTOM CONFIGURATION/PROPERTY READ-IN # define profiles arrays, variables declare -a profiles_ident @@ -417,6 +455,33 @@ else done < $CREDFILE + + # init arrays to hold ident<->mfasec detail + declare -a confs_ident + declare -a confs_mfasec + confs_init=0 + confs_iterator=0 + + # read the config file for the optional MFA length param (MAXSEC) + 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 + # make sure environment doesn't have a stale session before we start checkEnvSession "init" @@ -444,7 +509,7 @@ else fi echo - # declare the arrays + # declare the arrays for credentials loop declare -a cred_profiles declare -a cred_profile_status declare -a cred_profile_user @@ -454,14 +519,22 @@ else declare -a mfa_profiles declare -a mfa_arns declare -a mfa_profile_status + declare -a mfa_mfasec cred_profilecounter=0 echo -n "Please wait" # read the credentials file while IFS='' read -r line || [[ -n "$line" ]]; do - [[ "$line" =~ ^\[(.*)\].* ]] && - profile_ident=${BASH_REMATCH[1]} + + [[ "$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 @@ -533,7 +606,8 @@ else if [ "$mfa_profile_ident" != "" ]; then getInitTime _ret_timestamp "$mfa_profile_ident" - getRemaining _ret_remaining ${_ret_timestamp} + getDuration _ret_duration "$mfa_profile_ident" + getRemaining _ret_remaining ${_ret_timestamp} ${_ret_duration} if [[ ${_ret_remaining} -eq 0 ]]; then # session has expired @@ -561,10 +635,12 @@ else ## 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 "MFA ARN: ${mfa_arns[$cred_profilecounter]}" + echo "MFA MAXSEC: ${mfa_mfasec[$cred_profilecounter]}" if [ "${mfa_profiles[$cred_profilecounter]}" == "" ]; then echo "MFA PROFILE IDENT:" else @@ -585,7 +661,7 @@ else mfa_profile_ident="" mfa_profile_check="" - cred_profilecounter=$(($cred_profilecounter+1)) + ((cred_profilecounter++)) fi done < $CREDFILE @@ -739,11 +815,13 @@ else AWS_2AUTH_PROFILE=${AWS_USER_PROFILE}-mfasession ARN_OF_MFA=${mfa_arns[$actual_selprofile]} + getDuration AWS_SESSION_DURATION $AWS_USER_PROFILE + echo "NOW GETTING THE 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 $MFA_SESSION_LENGTH_IN_SECONDS \ + --duration $AWS_SESSION_DURATION \ --serial-number $ARN_OF_MFA \ --token-code $mfacode \ --output text | awk '{ print $2, $4, $5 }') @@ -761,7 +839,7 @@ else # aws_access_key_id, aws_secret_access_key, and aws_session_token # for the MFA profile echo - getPrintableTimeRemaining _ret ${MFA_SESSION_LENGTH_IN_SECONDS} + getPrintableTimeRemaining _ret $AWS_SESSION_DURATION validity_period=${_ret} echo -e "Make this MFA session persistent (saved in ~/.aws/credentials)\nso that you can return to it during its validity period (${validity_period})?" read -s -p "If you answer 'No', only the envvars will be used? [Y]n " -n 1 -r @@ -774,10 +852,9 @@ else `aws --profile $AWS_2AUTH_PROFILE configure set aws_session_token "$AWS_SESSION_TOKEN"` # set init time in the static MFA profile (a custom key in ~/.aws/credentials) addInitTime "${AWS_2AUTH_PROFILE}" - else - # for non-persistent (envvars only), set the init time - AWS_SESSION_INIT_TIME=$(date +%s) fi + # init time for envvar exports (if selected) + AWS_SESSION_INIT_TIME=$(date +%s) # 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) @@ -785,10 +862,12 @@ else ## 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 @@ -853,8 +932,10 @@ else if [[ "$mfaprofile" == "true" ]]; then # this is a persistent MFA profile (a subset of [[ "$mfacode" == "" ]]) AWS_SESSION_TOKEN=$(aws --profile $final_selection configure get aws_session_token) - getInitTime _ret "$final_selection" + getInitTime _ret "${final_selection}" AWS_SESSION_INIT_TIME=${_ret} + getDuration _ret "${final_selection}" + AWS_SESSION_DURATION=${_ret} fi fi @@ -910,8 +991,9 @@ else 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" - echo -n "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_DEFAULT_REGION; unset AWS_DEFAULT_OUTPUT" | pbcopy + echo -n "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" | pbcopy else echo "export AWS_ACCESS_KEY_ID=${AWS_ACCESS_KEY_ID}" echo "export AWS_SECRET_ACCESS_KEY=${AWS_SECRET_ACCESS_KEY}" @@ -919,12 +1001,14 @@ else 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}" - echo -n "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_TOKEN=${AWS_SESSION_TOKEN}; export AWS_SESSION_INIT_TIME=${AWS_SESSION_INIT_TIME}" | pbcopy + echo -n "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}" | pbcopy else echo "unset AWS_SESSION_INIT_TIME" + echo "unset AWS_SESSION_DURATION" echo "unset AWS_SESSION_TOKEN" - echo -n "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_TOKEN; unset AWS_SESSION_INIT_TIME" | pbcopy + echo -n "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" | pbcopy echo fi fi @@ -946,16 +1030,18 @@ else 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}" if exists xclip ; then - echo -n "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_TOKEN=${AWS_SESSION_TOKEN}; export AWS_SESSION_INIT_TIME=${AWS_SESSION_INIT_TIME}" | xclip -i + echo -n "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}" | xclip -i echo "(xclip found; the activation command is now on your X PRIMARY clipboard -- just paste on the command line, and press [ENTER])" fi else echo "unset AWS_SESSION_INIT_TIME" + echo "unset AWS_SESSION_DURATION" echo "unset AWS_SESSION_TOKEN" if exists xclip ; then - echo -n "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_TOKEN; unset AWS_SESSION_INIT_TIME" | xclip -i + echo -n "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" | xclip -i echo "(xclip found; the activation command is now on your X PRIMARY clipboard -- just paste on the command line, and press [ENTER])" fi fi @@ -966,7 +1052,7 @@ else echo echo ".. or execute the following to use named profile only, clearning any previoiusly set configuration variables:" echo - echo "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_DEFAULT_REGION; unset AWS_DEFAULT_OUTPUT" + echo "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" echo echo -e "To conveniently remove any AWS profile/secrets information from the environment, simply source the attached script, like so:\nsource ./source-to-clear-AWS-envvars.sh" echo @@ -983,15 +1069,17 @@ else 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 echo echo "..or execute the following to use named profile only, clearing any previously set configuration variables:" echo - echo "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_DEFAULT_REGION; unset AWS_DEFAULT_OUTPUT" + echo "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" echo echo -e "To conveniently remove any AWS profile/secrets information from the environment, simply source the attached script, like so:\nsource ./source-to-clear-AWS-envvars.sh" echo diff --git a/awscli-mfa/remaining.sh b/awscli-mfa/remaining.sh index e4a8068..e82da40 100755 --- a/awscli-mfa/remaining.sh +++ b/awscli-mfa/remaining.sh @@ -1,25 +1,28 @@ #!/bin/bash -# Set the 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. +# 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. +# 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 -# defined the standard location of the AWS credentials file +# define the standard location of the AWS credentials and config files +CONFFILE=~/.aws/config CREDFILE=~/.aws/credentials @@ -35,6 +38,8 @@ idxLookup() { declare -a arr=("${!2}") local key=$3 local result="" + local i + local maxIndex maxIndex=${#arr[@]} ((maxIndex--)) @@ -50,19 +55,46 @@ idxLookup() { eval "$1=$result" } -# return remaining seconds for the given timestamp; -# uses the MFA_SESSION_LENGTH_IN_SECONDS global var; +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=$(date +%s) local remaining=0 + [[ "${duration}" == "" ]] && + duration=$MFA_SESSION_LENGTH_IN_SECONDS + if [ ! -z "${timestamp##*[!0-9]*}" ]; then - let session_end=${timestamp}+${MFA_SESSION_LENGTH_IN_SECONDS} + let session_end=${timestamp}+${duration} if [[ $session_end -gt $this_time ]]; then let remaining=${session_end}-${this_time} else @@ -75,7 +107,7 @@ getRemaining() { } # return printable output for given 'remaining' timestamp -# (must be pre-incremented with MFA_SESSION_LENGTH_IN_SECONDS, +# (must be pre-incremented with duration, # such as getRemaining() output) getPrintableTimeRemaining() { # $1 is _ret @@ -119,7 +151,11 @@ sessionData() { if [[ "$AWS_SESSION_INIT_TIME" != "" ]]; then - getRemaining _ret_remaining $AWS_SESSION_INIT_TIME + # 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 " MFA SESSION EXPIRED; YOU SHOULD PURGE THE ENV BY EXECUTING 'source ./source-to-clear-AWS-envvars.sh'" @@ -149,14 +185,51 @@ 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 - let IN_ENV_SESSION_TIME=${AWS_SESSION_INIT_TIME}+${MFA_SESSION_LENGTH_IN_SECONDS} + + [[ "${AWS_SESSION_DURATION}" == "" ]] && + AWS_SESSION_DURATION=$MFA_SESSION_LENGTH_IN_SECONDS + + let IN_ENV_SESSION_TIME=${AWS_SESSION_INIT_TIME}+${AWS_SESSION_DURATION} ENV_TIME="true" else ENV_TIME="false" fi +# COLLECT AWS CONFIG DATA FROM ~/.aws/config + +# init arrays to hold ident<->mfasec detail +declare -a confs_ident +declare -a confs_mfasec +confs_init=0 +confs_iterator=0 + +# read the config file for the optional MFA length param (MAXSEC) +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 ~/.aws/credentials # define profiles arrays @@ -165,10 +238,12 @@ 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]} @@ -182,6 +257,12 @@ while IFS='' read -r line || [[ -n "$line" ]]; do 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 @@ -189,7 +270,6 @@ while IFS='' read -r line || [[ -n "$line" ]]; do else profiles_type[$profiles_iterator]='session' fi - fi [[ "$line" =~ ^aws_access_key_id[[:space:]]*=[[:space:]]*(.*)$ ]] && @@ -206,15 +286,7 @@ echo done < $CREDFILE - -# lookup AWS_PROFILE, AWS_ACCESS_KEY_ID in ~/.aws/credentials -# -> profile is not found or if AWS_ACCESS_KEY_ID is not in credentials, -# this is an env-only profile (differentiate with TOKEN). For MFA -# sessions calculate remaining time (suggest/provide purge commands -# if expired) - -# calculate the remaining session times for the MFA sessions also in -# the ~/.aws/credentials file +## PRESENTATION echo echo "ENVIRONMENT" @@ -246,12 +318,16 @@ maxIndex=${#profiles_ident[@]} live_session_counter=0 -for (( i=0; i<=${maxIndex}; i++ )) +for (( z=0; z<=${maxIndex}; z++ )) do - if [[ "${profiles_type[$i]}" == "session" ]]; then - echo "MFA SESSION IDENT: ${profiles_ident[$i]}" - if [[ "${profiles_session_init_time[$i]}" != "" ]]; then - getRemaining _ret_remaining ${profiles_session_init_time[$i]} + + if [[ "${profiles_type[$z]}" == "session" ]]; then + + echo "MFA SESSION IDENT: ${profiles_ident[$z]}" + 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 " MFA SESSION EXPIRED" @@ -264,6 +340,9 @@ do fi echo fi + _ret="" + _ret_duration="" + _ret_remaining="" done echo diff --git a/awscli-mfa/source-to-clear-AWS-envvars.sh b/awscli-mfa/source-to-clear-AWS-envvars.sh index fb3ccf8..9d9a636 100644 --- a/awscli-mfa/source-to-clear-AWS-envvars.sh +++ b/awscli-mfa/source-to-clear-AWS-envvars.sh @@ -12,6 +12,7 @@ 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 From a410b4621b62a47bb261687c6f3d2d3d5928a5b1 Mon Sep 17 00:00:00 2001 From: Ville Walveranta Date: Wed, 14 Mar 2018 02:05:46 -0500 Subject: [PATCH 19/71] awscli-mfa.sh bugfix --- awscli-mfa/awscli-mfa.sh | 22 ++++++++++++++++++---- 1 file changed, 18 insertions(+), 4 deletions(-) diff --git a/awscli-mfa/awscli-mfa.sh b/awscli-mfa/awscli-mfa.sh index 2c2f748..c0b4c9c 100755 --- a/awscli-mfa/awscli-mfa.sh +++ b/awscli-mfa/awscli-mfa.sh @@ -300,7 +300,7 @@ continue_maybe() { unset AWS_SHARED_CREDENTIALS_FILE unset AWS_CONFIG_FILE -# use_profile='--profile default' + use_profile='--profile default' else echo -e "\n\nExecute \"source ./source-to-clear-AWS-envvars.sh\", and try again to proceed.\n" exit 1 @@ -489,6 +489,7 @@ else current_aws_access_key_id="$(aws configure get aws_access_key_id)" idxLookup idx profiles_key_id[@] $current_aws_access_key_id + if [[ $idx != "" ]]; then currently_selected_profile_ident="${profiles_ident[$idx]}" else @@ -496,18 +497,31 @@ else fi process_user_arn="$(aws $use_profile sts get-caller-identity --output text --query 'Arn' 2>&1)" + [[ "$process_user_arn" =~ ([^/]+)$ ]] && process_username="${BASH_REMATCH[1]}" + if [[ "$process_username" =~ ExpiredToken ]]; then + continue_maybe + + currently_selected_profile_ident="default" + process_user_arn="$(aws $use_profile sts get-caller-identity --output text --query 'Arn' 2>&1)" + + [[ "$process_user_arn" =~ ([^/]+)$ ]] && + process_username="${BASH_REMATCH[1]}" + fi + if [[ "$process_username" =~ error ]] || [[ "$currently_selected_profile_ident" == "unknown" ]]; then - echo "Default/selected profile is not functional; the script may not work as expected." - echo "Check the Default profile in your '~/.aws/credentials' file, as well as any 'AWS_' environment variables!" + echo -e "The selected profile is not functional; please check the \"default\" profile\nin your '~/.aws/credentials' file, as well as any 'AWS_' environment variables!" + exit 1 else + echo echo echo "Executing this script as the AWS/IAM user \"$process_username\" (profile \"$currently_selected_profile_ident\")." fi - echo + + echo # declare the arrays for credentials loop declare -a cred_profiles From 574cb0ec46556632da16c6ac09b407a340cecb7c Mon Sep 17 00:00:00 2001 From: Ville Walveranta Date: Wed, 14 Mar 2018 02:28:09 -0500 Subject: [PATCH 20/71] Changes to the awscli-mfa example policy file, example-MFA-enforcement-policy.txt --- awscli-mfa/example-MFA-enforcement-policy.txt | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/awscli-mfa/example-MFA-enforcement-policy.txt b/awscli-mfa/example-MFA-enforcement-policy.txt index 6662d57..9e4614b 100644 --- a/awscli-mfa/example-MFA-enforcement-policy.txt +++ b/awscli-mfa/example-MFA-enforcement-policy.txt @@ -5,11 +5,13 @@ "Sid": "AllowAllUsersToListAccounts", "Effect": "Allow", "Action": [ - "iam:GetAccountPasswordPolicy", + "iam:GetAccountPasswordPolicy", + "iam:ListVirtualMFADevices", "iam:ListAccountAliases", "iam:ListUsers" ], "Resource": [ + "arn:aws:iam::ACCOUNT-ID-WITHOUT-HYPHENS:mfa/*", "arn:aws:iam::ACCOUNT-ID-WITHOUT-HYPHENS:user/*" ] }, @@ -36,7 +38,7 @@ "iam:ListMFADevices" ], "Resource": [ - "arn:aws:iam::ACCOUNT-ID-WITHOUT-HYPHENS:mfa/${aws:username}", + "arn:aws:iam::ACCOUNT-ID-WITHOUT-HYPHENS:mfa/${aws:username}@@*", "arn:aws:iam::ACCOUNT-ID-WITHOUT-HYPHENS:user/${aws:username}" ] }, @@ -50,7 +52,7 @@ "iam:ResyncMFADevice" ], "Resource": [ - "arn:aws:iam::ACCOUNT-ID-WITHOUT-HYPHENS:mfa/${aws:username}", + "arn:aws:iam::ACCOUNT-ID-WITHOUT-HYPHENS:mfa/${aws:username}@@*", "arn:aws:iam::ACCOUNT-ID-WITHOUT-HYPHENS:user/${aws:username}" ] }, @@ -61,7 +63,7 @@ "iam:DeactivateMFADevice" ], "Resource": [ - "arn:aws:iam::ACCOUNT-ID-WITHOUT-HYPHENS:mfa/${aws:username}", + "arn:aws:iam::ACCOUNT-ID-WITHOUT-HYPHENS:mfa/${aws:username}@@*", "arn:aws:iam::ACCOUNT-ID-WITHOUT-HYPHENS:user/${aws:username}" ], "Condition": { @@ -75,13 +77,13 @@ "Effect": "Deny", "NotAction": [ "iam:ListMFADevices", - "iam:ListUsers", - "iam:ListAccountAliases", + "iam:ListVirtualMFADevices", "iam:CreateVirtualMFADevice", - "iam:DeactivateMFADevice", "iam:DeleteVirtualMFADevice", "iam:EnableMFADevice", "iam:ResyncMFADevice", + "iam:ListUsers", + "iam:ListAccountAliases", "iam:ChangePassword", "iam:CreateLoginProfile", "iam:DeleteLoginProfile", @@ -103,7 +105,6 @@ "Sid": "DenyIamAccessToOtherAccountsUnlessMFAd", "Effect": "Deny", "Action": [ - "iam:ListVirtualMFADevices", "iam:CreateVirtualMFADevice", "iam:DeactivateMFADevice", "iam:DeleteVirtualMFADevice", @@ -119,7 +120,7 @@ "iam:ListAccessKeys" ], "NotResource": [ - "arn:aws:iam::ACCOUNT-ID-WITHOUT-HYPHENS:mfa/${aws:username}", + "arn:aws:iam::ACCOUNT-ID-WITHOUT-HYPHENS:mfa/${aws:username}@@*", "arn:aws:iam::ACCOUNT-ID-WITHOUT-HYPHENS:user/${aws:username}" ], "Condition": { From 69de32abdbb87a7e3035673637dedecc70b4f966 Mon Sep 17 00:00:00 2001 From: Ville Walveranta Date: Sun, 18 Mar 2018 16:40:53 -0500 Subject: [PATCH 21/71] Added color codes, improved UX. --- awscli-mfa/awscli-mfa.sh | 123 ++++++++++++++++++++++++++++++++++----- 1 file changed, 107 insertions(+), 16 deletions(-) diff --git a/awscli-mfa/awscli-mfa.sh b/awscli-mfa/awscli-mfa.sh index c0b4c9c..e965b2b 100755 --- a/awscli-mfa/awscli-mfa.sh +++ b/awscli-mfa/awscli-mfa.sh @@ -1,5 +1,17 @@ #!/usr/bin/env bash +# todo: AWS_SHARED_CREDENTIALS_FILE and AWS_CONFIG_FILE values should either +# be utilized, or their presence should not be allowed +# +# todo: support profile names with spaces (AWS allows them) +# +# todo: quote all +# +# todo: when there is only one single profile, bypass the profile +# selection menu +# +# todo: better documentation! + DEBUG="false" # uncomment below to enable the debug output #DEBUG="true" @@ -26,7 +38,83 @@ MFA_SESSION_LENGTH_IN_SECONDS=32400 CONFFILE=~/.aws/config CREDFILE=~/.aws/credentials -## FUNCTIONS +# 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 ================================================================== # `exists` for commands exists() { @@ -346,9 +434,9 @@ fi ONEPROFILE="false" while IFS='' read -r line || [[ -n "$line" ]]; do [[ "$line" =~ ^\[(.*)\].* ]] && - profile_ident=${BASH_REMATCH[1]} + profile_ident="${BASH_REMATCH[1]}" - if [ $profile_ident != "" ]; then + if [ "$profile_ident" != "" ]; then ONEPROFILE="true" fi done < $CREDFILE @@ -516,7 +604,6 @@ else echo -e "The selected profile is not functional; please check the \"default\" profile\nin your '~/.aws/credentials' file, as well as any 'AWS_' environment variables!" exit 1 else - echo echo echo "Executing this script as the AWS/IAM user \"$process_username\" (profile \"$currently_selected_profile_ident\")." fi @@ -536,7 +623,7 @@ else declare -a mfa_mfasec cred_profilecounter=0 - echo -n "Please wait" + echo -ne "${BIWhite}Please wait" # read the credentials file while IFS='' read -r line || [[ -n "$line" ]]; do @@ -679,27 +766,27 @@ else fi done < $CREDFILE + echo -e "${Color_Off}" # create the profile selections echo - echo - echo "AVAILABLE AWS PROFILES:" + echo -e "${Black}${On_White} AVAILABLE AWS PROFILES: ${Color_Off}" echo SELECTR=0 ITER=1 for i in "${cred_profiles[@]}" do if [ "${mfa_arns[$SELECTR]}" != "" ]; then - mfa_notify=", MFA configured" + mfa_notify="; ${Green}vMFAd enabled${Color_Off}" else - mfa_notify="" + mfa_notify="; vMFAd not configured" fi - echo "${ITER}: $i (${cred_profile_user[$SELECTR]}${mfa_notify})" + echo -en "${BIWhite}${ITER}: $i${Color_Off} (IAM: ${cred_profile_user[$SELECTR]}${mfa_notify})\n" if [[ "${mfa_profile_status[$SELECTR]}" != "EXPIRED" && "${mfa_profile_status[$SELECTR]}" != "" ]]; then - echo "${ITER}m: $i MFA profile (${mfa_profile_status[$SELECTR]})" + echo -e "${BIWhite}${ITER}m: $i MFA profile${Color_Off} (${mfa_profile_status[$SELECTR]})" fi echo @@ -714,8 +801,10 @@ else mfaprofile="false" # prompt for profile selection - printf "SELECT A PROFILE BY THE ID: " + printf "You can switch to a base profile to use it as-is, start an MFA session\nfor a profile if it it 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}SELECT A PROFILE BY THE ID: " read -r selprofile + echo -en "\n${Color_Off}" # process the selection if [[ "$selprofile" != "" ]]; then @@ -798,21 +887,23 @@ else # prompt for the MFA code echo - echo -e "Enter the current MFA one time pass code for profile '${cred_profiles[$actual_selprofile]}' to start/renew the MFA session," + echo -e "Enter the current MFA one time pass code for profile '${cred_profiles[$actual_selprofile]}' to start/renew an MFA session," echo "or leave empty (just press [ENTER]) to use the selected profile without the MFA." while : do + echo -en "${BIWhite}" read mfacode + echo -en "${Color_Off}" if ! [[ "$mfacode" =~ ^$ || "$mfacode" =~ [0-9]{6} ]]; then - echo "The MFA code must be exactly six digits, or blank to bypass." + echo "The MFA pass code must be exactly six digits, or blank to bypass (to use the profile without an MFA session)." continue else break fi done - elif [[ "$active_mfa" == "false" ]]; then # no MFA configured (no MFA ARN); print a notice + 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" @@ -820,7 +911,7 @@ else # reset entered MFA code (just to be safe) mfacode="" echo - echo -e "MFA has not been set up for this profile." + echo -e "vMFAd has not been set up for this profile (run 'register-virtual-mfa-device.sh' script to configure the vMFAd)." fi if [[ "$mfacode" != "" ]]; then From e2a26d73517bb19e98594f3768e60510a02d40ba Mon Sep 17 00:00:00 2001 From: Ville Walveranta Date: Mon, 19 Mar 2018 02:26:42 -0500 Subject: [PATCH 22/71] Improved UX. AWS profile names with spaces supported. Many other fixes, corrections. --- awscli-mfa/awscli-mfa.sh | 294 +++++++++++++++++++++------------------ 1 file changed, 162 insertions(+), 132 deletions(-) diff --git a/awscli-mfa/awscli-mfa.sh b/awscli-mfa/awscli-mfa.sh index e965b2b..389073d 100755 --- a/awscli-mfa/awscli-mfa.sh +++ b/awscli-mfa/awscli-mfa.sh @@ -3,14 +3,10 @@ # todo: AWS_SHARED_CREDENTIALS_FILE and AWS_CONFIG_FILE values should either # be utilized, or their presence should not be allowed # -# todo: support profile names with spaces (AWS allows them) -# -# todo: quote all -# # todo: when there is only one single profile, bypass the profile # selection menu -# -# todo: better documentation! +# +# todo: test new functionality on Linux DEBUG="false" # uncomment below to enable the debug output @@ -124,8 +120,7 @@ exists() { # precheck envvars for existing/stale session definitions checkEnvSession() { # $1 is the check type - - local check_type=$1 + local this_time=$(date +%s) # COLLECT AWS_SESSION DATA FROM THE ENVIRONMENT @@ -177,20 +172,27 @@ checkEnvSession() { # in the environment or in ~/.aws/credentials) if [[ "$PRECHECK_AWS_SESSION_TOKEN" != "" ]] && [[ "$PRECHECK_AWS_SESSION_INIT_TIME" != "" ]]; then - + getRemaining _ret $PRECHECK_AWS_SESSION_INIT_TIME $PRECHECK_AWS_SESSION_DURATION [[ "${_ret}" -eq 0 ]] && continue_maybe - elif [[ "$PRECHECK_AWS_PROFILE" =~ -mfasession$ ]]; then - # find the profile's init time entry if one exists - idxLookup idx profiles_ident[@] $PRECHECK_AWS_PROFILE - profile_time=${profiles_session_init_time[$idx]} + elif [[ "$PRECHECK_AWS_PROFILE" != "" ]]; then + idxLookup idx profiles_ident[@] "$PRECHECK_AWS_PROFILE" - getDuration parent_duration $PRECHECK_AWS_PROFILE + if [[ "$PRECHECK_AWS_PROFILE" =~ -mfasession$ ]] && + [[ "$idx" != "" ]]; then - if [[ "$profile_time" != "" ]]; then - getRemaining _ret $profile_time $parent_duration - [[ "${_ret}" -eq 0 ]] && continue_maybe + # find the profile's init time entry if one exists + profile_time=${profiles_session_init_time[$idx]} + getDuration parent_duration "$PRECHECK_AWS_PROFILE" + + if [[ "$profile_time" != "" ]]; then + getRemaining _ret $profile_time $parent_duration + [[ "${_ret}" -eq 0 ]] && continue_maybe + fi + elif [[ "$idx" == "" ]]; then + echo "Incomplete AWS environment parameters set." + continue_maybe fi fi @@ -210,17 +212,23 @@ checkEnvSession() { echo echo "** NOTE: SOME AWS_* ENVIRONMENT VARIABLES ARE CURRENTLY IN EFFECT:" - [[ "$PRECHECK_AWS_PROFILE" != "" ]] && echo "AWS_PROFILE: $PRECHECK_AWS_PROFILE" - [[ "$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 + 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 @@ -241,7 +249,7 @@ idxLookup() { maxIndex=${#arr[@]} ((maxIndex--)) - for (( i=0; i<=${maxIndex}; i++ )) + for (( i=0; i<=maxIndex; i++ )) do if [[ "${arr[$i]}" == "$key" ]]; then result=$i @@ -290,7 +298,7 @@ getInitTime() { local profile_time # find the profile's init time entry if one exists - idxLookup idx profiles_ident[@] $this_ident + idxLookup idx profiles_ident[@] "$this_ident" profile_time=${profiles_session_init_time[$idx]} eval "$1=${profile_time}" @@ -300,7 +308,7 @@ getDuration() { # $1 is _ret # $2 is the profile ident - local this_profile_ident=$2 + local this_profile_ident="$2" local this_duration # use parent profile ident if this is an MFA session @@ -308,7 +316,7 @@ getDuration() { this_profile_ident="${BASH_REMATCH[1]}" # look up possible custom duration for the parent profile - idxLookup idx confs_ident[@] $this_profile_ident + idxLookup idx confs_ident[@] "$this_profile_ident" [[ $idx != "" && "${confs_mfasec[$idx]}" != "" ]] && this_duration=${confs_mfasec[$idx]} || @@ -371,8 +379,20 @@ getPrintableTimeRemaining() { } continue_maybe() { - echo -e "\nTHE MFA SESSION SELECTED IN THE ENVIRONMENT (${PRECHECK_AWS_PROFILE}) HAS EXPIRED.\n" - read -s -p "Do you want to continue with the default profile? - [Y]n " -n 1 -r + if [[ "${PRECHECK_AWS_PROFILE}" != "" ]] && + [[ "${PRECHECK_AWS_SESSION_TOKEN}" != "" ]]; then + echo -e "\n${BIWhite}THE MFA SESSION SET IN THE ENVIRONMENT (${PRECHECK_AWS_PROFILE}) HAS EXPIRED.${Color_Off}\n" + elif [[ "${PRECHECK_AWS_PROFILE}" == "" ]] && + [[ "${PRECHECK_AWS_SESSION_TOKEN}" != "" ]]; then + echo -e "\n${BIWhite}THE MFA SESSION SET IN THE ENVIRONMENT HAS EXPIRED.${Color_Off}\n" + elif [[ "${PRECHECK_AWS_PROFILE}" != "" ]] && + [[ "${PRECHECK_AWS_SESSION_TOKEN}" == "" ]]; then + echo -e "\n${BIWhite}THE AWS PROFILE SELECTED IN THE ENVIRONMENT (${PRECHECK_AWS_PROFILE}) IS UNKNOWN.${Color_Off}\n" + else + return + fi + + read -s -p "$(echo -e "${BIWhite}Do you want to continue with the default profile?${Color_Off} - ${BIWhite}[Y]${Color_Off}n ")" -n 1 -r if [[ $REPLY =~ ^[Yy]$ ]] || [[ $REPLY == "" ]]; then @@ -388,7 +408,9 @@ continue_maybe() { unset AWS_SHARED_CREDENTIALS_FILE unset AWS_CONFIG_FILE - use_profile='--profile default' + # override envvar for all the subshell commands + export AWS_PROFILE=default + echo else echo -e "\n\nExecute \"source ./source-to-clear-AWS-envvars.sh\", and try again to proceed.\n" exit 1 @@ -448,24 +470,8 @@ if [[ "$ONEPROFILE" == "false" ]]; then else - # 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) - default_output=$(aws --profile default configure get output) - - if [[ "$default_region" == "" ]]; then - echo - echo -e "DEFAULT REGION HAS NOT BEEN CONFIGURED.\nPlease set the default region in '~/.aws/config', 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 - # Check OS for some supported platforms - OS="`uname`" + OS="$(uname)" case $OS in 'Linux') OS='Linux' @@ -500,7 +506,6 @@ else persistent_MFA="false" profiles_iterator=0 profiles_init=0 - use_profile="" # ugly hack to relate different values because # macOS *still* does not provide bash 4.x by default, @@ -508,7 +513,7 @@ else # 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]} + _ret="${BASH_REMATCH[1]}" if [[ $profiles_init -eq 0 ]]; then profiles_ident[$profiles_iterator]=$_ret @@ -518,9 +523,9 @@ else if [[ "$_ret" != "" ]] && ! [[ "$_ret" =~ -mfasession$ ]]; then - profiles_type[$profiles_iterator]='profile' + profiles_type[$profiles_iterator]="profile" else - profiles_type[$profiles_iterator]='session' + profiles_type[$profiles_iterator]="session" fi if [[ "${profiles_ident[$profiles_iterator]}" != "$_ret" ]]; then @@ -539,7 +544,7 @@ else profiles_session_token[$profiles_iterator]="${BASH_REMATCH[1]}" [[ "$line" =~ ^aws_session_init_time[[:space:]]*=[[:space:]]*(.*)$ ]] && - profiles_session_init_time[$profiles_iterator]="${BASH_REMATCH[1]}" + profiles_session_init_time[$profiles_iterator]=${BASH_REMATCH[1]} done < $CREDFILE @@ -547,14 +552,13 @@ else # init arrays to hold ident<->mfasec detail declare -a confs_ident declare -a confs_mfasec - confs_init=0 confs_iterator=0 - # read the config file for the optional MFA length param (MAXSEC) + # 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]} + this_conf_ident="${BASH_REMATCH[1]}" [[ "$line" =~ ^[[:space:]]*mfasec[[:space:]]*=[[:space:]]*(.*)$ ]] && this_conf_mfasec=${BASH_REMATCH[1]} @@ -571,7 +575,23 @@ else done < $CONFFILE # make sure environment doesn't have a stale session before we start - checkEnvSession "init" + 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) + default_output=$(aws configure get output) + + if [[ "$default_region" == "" ]]; then + echo + echo -e "DEFAULT REGION HAS NOT BEEN CONFIGURED.\nPlease set the default region in '~/.aws/config', 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 current_aws_access_key_id="$(aws configure get aws_access_key_id)" @@ -584,7 +604,7 @@ else currently_selected_profile_ident="unknown" fi - process_user_arn="$(aws $use_profile sts get-caller-identity --output text --query 'Arn' 2>&1)" + process_user_arn="$(aws sts get-caller-identity --output text --query 'Arn' 2>&1)" [[ "$process_user_arn" =~ ([^/]+)$ ]] && process_username="${BASH_REMATCH[1]}" @@ -593,7 +613,7 @@ else continue_maybe currently_selected_profile_ident="default" - process_user_arn="$(aws $use_profile sts get-caller-identity --output text --query 'Arn' 2>&1)" + process_user_arn="$(aws sts get-caller-identity --output text --query 'Arn' 2>&1)" [[ "$process_user_arn" =~ ([^/]+)$ ]] && process_username="${BASH_REMATCH[1]}" @@ -604,7 +624,6 @@ else echo -e "The selected profile is not functional; please check the \"default\" profile\nin your '~/.aws/credentials' file, as well as any 'AWS_' environment variables!" exit 1 else - echo echo "Executing this script as the AWS/IAM user \"$process_username\" (profile \"$currently_selected_profile_ident\")." fi @@ -629,10 +648,10 @@ else while IFS='' read -r line || [[ -n "$line" ]]; do [[ "$line" =~ ^\[(.*)\].* ]] && - profile_ident=${BASH_REMATCH[1]} + profile_ident="${BASH_REMATCH[1]}" # transfer possible MFA mfasec from config array - idxLookup idx confs_ident[@] $profile_ident + idxLookup idx confs_ident[@] "$profile_ident" if [[ $idx != "" ]]; then mfa_mfasec[$cred_profilecounter]=${confs_mfasec[$idx]} fi @@ -644,15 +663,15 @@ else ! [[ "$profile_ident" =~ -mfasession$ ]]; then # store this profile ident - cred_profiles[$cred_profilecounter]=$profile_ident + cred_profiles[$cred_profilecounter]="$profile_ident" # store this profile region and output format - profile_region[$cred_profilecounter]=$(aws --profile $profile_ident configure get region) - profile_output[$cred_profilecounter]=$(aws --profile $profile_ident configure get output) + profile_region[$cred_profilecounter]=$(aws --profile "$profile_ident" configure get region) + profile_output[$cred_profilecounter]=$(aws --profile "$profile_ident" configure get output) # get the user ARN; this should be always # available for valid profiles - user_arn="$(aws sts get-caller-identity --profile $profile_ident --output text --query 'Arn' 2>&1)" + 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 @@ -682,7 +701,7 @@ else # (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 --output text --query "User.Arn" --profile $profile_ident 2>&1)" + 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 @@ -692,7 +711,7 @@ else # 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)" + mfa_arn="$(aws iam list-mfa-devices --profile "$profile_ident" --user-name ${cred_profile_user[$cred_profilecounter]} --output text --query "MFADevices[].SerialNumber" 2>&1)" if [[ "$mfa_arn" =~ ^arn:aws ]]; then mfa_arns[$cred_profilecounter]="$mfa_arn" else @@ -722,7 +741,7 @@ else elif [[ ${_ret_remaining} -eq -1 ]]; then # no timestamp; legacy or initialized outside of this utility - mfa_profile_check="$(aws iam get-user --output text --query "User.Arn" --profile $mfa_profile_ident 2>&1)" + 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 @@ -770,7 +789,7 @@ else # create the profile selections echo - echo -e "${Black}${On_White} AVAILABLE AWS PROFILES: ${Color_Off}" + echo -e "${BBlack}${On_White} AVAILABLE AWS PROFILES: ${Color_Off}" echo SELECTR=0 ITER=1 @@ -790,8 +809,8 @@ else fi echo - let ITER=${ITER}+1 - let SELECTR=${SELECTR}+1 + ((ITER++)) + ((SELECTR++)) done # this is used to determine whether to trigger a MFA request for a MFA profile @@ -815,7 +834,7 @@ else # if the numeric selection was found, # translate it to the array index and validate - let actual_selprofile=${selprofile_check}-1 + ((actual_selprofile=${selprofile_check}-1)) profilecount=${#cred_profiles[@]} if [[ $actual_selprofile -ge $profilecount || @@ -840,7 +859,7 @@ else mfa_parent_profile_ident="${cred_profiles[$actual_selprofile]}" final_selection="${mfa_profiles[$actual_selprofile]}" - echo "SELECTED MFA PROFILE: ${final_selection} (for base profile '${mfa_parent_profile_ident}')" + echo "SELECTED MFA PROFILE: ${final_selection} (for base profile \"${mfa_parent_profile_ident}\")" # this is used to determine whether to print MFA questions/details mfaprofile="true" @@ -851,7 +870,7 @@ else 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 -e "${BIRed}There is no profile '${selprofile}'.${Color_Off}" echo exit 1 @@ -862,7 +881,7 @@ else final_selection="${cred_profiles[$actual_selprofile]}" else # non-acceptable characters were present in the selection - echo "There is no profile '${selprofile}'." + echo -e "${BIRed}There is no profile '${selprofile}'.${Color_Off}" echo exit 1 fi @@ -870,13 +889,13 @@ else else # no numeric part in selection - echo "There is no profile '${selprofile}'." + echo -e "${BIRed}There is no profile '${selprofile}'.${Color_Off}" echo exit 1 fi else # empty selection - echo "There is no profile '${selprofile}'." + echo -e "${BIRed}There is no profile '${selprofile}'.${Color_Off}" echo exit 1 fi @@ -887,7 +906,7 @@ else # prompt for the MFA code echo - echo -e "Enter the current MFA one time pass code for profile '${cred_profiles[$actual_selprofile]}' to start/renew an MFA session," + echo -e "${BIWhite}Enter the current MFA one time pass code for profile '${cred_profiles[$actual_selprofile]}'${Color_Off} to start/renew an MFA session," echo "or leave empty (just press [ENTER]) to use the selected profile without the MFA." while : @@ -896,7 +915,7 @@ else read mfacode echo -en "${Color_Off}" if ! [[ "$mfacode" =~ ^$ || "$mfacode" =~ [0-9]{6} ]]; then - echo "The MFA pass code must be exactly six digits, or blank to bypass (to use the profile without an MFA session)." + echo -e "${BIRed}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 @@ -916,16 +935,17 @@ else if [[ "$mfacode" != "" ]]; then # init an MFA session (request an MFA session token) - AWS_USER_PROFILE=${cred_profiles[$actual_selprofile]} - AWS_2AUTH_PROFILE=${AWS_USER_PROFILE}-mfasession + AWS_USER_PROFILE="${cred_profiles[$actual_selprofile]}" + AWS_2AUTH_PROFILE="${AWS_USER_PROFILE}-mfasession" ARN_OF_MFA=${mfa_arns[$actual_selprofile]} - getDuration AWS_SESSION_DURATION $AWS_USER_PROFILE + getDuration AWS_SESSION_DURATION "$AWS_USER_PROFILE" - echo "NOW GETTING THE MFA SESSION TOKEN FOR THE PROFILE: $AWS_USER_PROFILE" + echo + echo -e "NOW GETTING THE MFA SESSION TOKEN FOR THE PROFILE: ${White}${AWS_USER_PROFILE}${Color_Off}" read AWS_ACCESS_KEY_ID AWS_SECRET_ACCESS_KEY AWS_SESSION_TOKEN <<< \ - $( aws --profile $AWS_USER_PROFILE sts get-session-token \ + $( aws --profile "$AWS_USER_PROFILE" sts get-session-token \ --duration $AWS_SESSION_DURATION \ --serial-number $ARN_OF_MFA \ --token-code $mfacode \ @@ -933,38 +953,42 @@ else if [ -z "$AWS_ACCESS_KEY_ID" ]; then echo - echo "Could not initialize the requested MFA session." + echo -e "${BIRed}Could not initialize the requested MFA session.${Color_Off}" echo exit 1 else # this is used to determine whether to print MFA questions/details mfaprofile="true" + echo -e "${Green}MFA session token acquired.${Color_Off}" + echo + + # export the selection to the remaining subshell commands in this script + 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" - # optioanlly set the persistent (~/.aws/credentials entries): + # optionally set the persistent (~/.aws/credentials entries): # aws_access_key_id, aws_secret_access_key, and aws_session_token # for the MFA profile - echo getPrintableTimeRemaining _ret $AWS_SESSION_DURATION validity_period=${_ret} - echo -e "Make this MFA session persistent (saved in ~/.aws/credentials)\nso that you can return to it during its validity period (${validity_period})?" - read -s -p "If you answer 'No', only the envvars will be used? [Y]n " -n 1 -r + echo -e "${BIWhite}Make this MFA session persistent${Color_Off} (saved in ~/.aws/credentials)\nso that you can return to it during its validity period (${validity_period})?" + read -s -p "$(echo -e "If you answer 'No', only the envvars will be used? ${BIWhite}[Y]${Color_Off}n ")" -n 1 -r + echo if [[ $REPLY =~ ^[Yy]$ ]] || [[ $REPLY == "" ]]; then persistent_MFA="true" - `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"` + 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" # set init time in the static MFA 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) - # 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 - ## DEBUG if [ "$DEBUG" == "true" ]; then echo @@ -983,9 +1007,13 @@ else 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) - AWS_DEFAULT_OUTPUT=$(aws --profile $final_selection configure get output) + AWS_DEFAULT_REGION=$(aws configure get region) + AWS_DEFAULT_OUTPUT=$(aws configure get output) # 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 @@ -1006,7 +1034,7 @@ else if [[ "$mfacode" == "" ]] || ( [[ "$mfacode" != "" ]] && [[ "$persistent_MFA" == "true" ]] ); then - `aws --profile $final_selection configure set region "${set_new_region}"` + aws configure set region "${set_new_region}" fi fi @@ -1026,17 +1054,17 @@ else if [[ "$mfacode" == "" ]] || ( [[ "$mfacode" != "" ]] && [[ "$persistent_MFA" == "true" ]] ); then - `aws --profile $final_selection configure set output "${set_new_output}"` + aws configure 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 --profile $final_selection configure get aws_access_key_id) - AWS_SECRET_ACCESS_KEY=$(aws --profile $final_selection configure get aws_secret_access_key) + AWS_ACCESS_KEY_ID=$(aws configure get aws_access_key_id) + AWS_SECRET_ACCESS_KEY=$(aws configure get aws_secret_access_key) if [[ "$mfaprofile" == "true" ]]; then # this is a persistent MFA profile (a subset of [[ "$mfacode" == "" ]]) - AWS_SESSION_TOKEN=$(aws --profile $final_selection configure get aws_session_token) + AWS_SESSION_TOKEN=$(aws configure get aws_session_token) getInitTime _ret "${final_selection}" AWS_SESSION_INIT_TIME=${_ret} getDuration _ret "${final_selection}" @@ -1045,24 +1073,25 @@ else fi echo - echo "========================================================================" + echo + echo -e "${BIWhite}${On_Green} * * * PROFILE DETAILS * * * ${Color_Off}" echo if [[ "$mfaprofile" == "true" ]]; then - echo "MFA profile name: '${final_selection}'" + echo -e "${BIWhite}MFA profile name: '${final_selection}'${Color_Off}" echo else - echo "Profile name '${final_selection}'" - echo "** NOTE: This is not an MFA session!" + echo -e "${BIWhite}Profile name '${final_selection}'${Color_Off}" + echo "NOTE: This is not an MFA session!" echo fi - echo "Region is set to: $AWS_DEFAULT_REGION" - echo "Output format is set to: $AWS_DEFAULT_OUTPUT" + echo -e "Region is set to: ${BIWhite}${AWS_DEFAULT_REGION}${Color_Off}" + echo -e "Output format is set to: ${BIWhite}${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 "Do you want to export the selected profile's secrets to the environment (for s3cmd, etc)? - y[N] " -n 1 -r + read -s -p "$(echo -e "${BIWhite}Do you want to export the selected profile's secrets to the environment${Color_Off} (for s3cmd, etc)? - y${BIWhite}[N]${Color_Off} ")" -n 1 -r if [[ $REPLY =~ ^[Nn]$ ]] || [[ $REPLY == "" ]]; then @@ -1079,14 +1108,13 @@ else fi if [[ "$mfacode" != "" ]] && [[ "$persistent_MFA" == "false" ]]; then - echo "*** THIS IS A NON-PERSISTENT MFA SESSION; YOU *MUST* EXPORT THE BELOW ENVVARS TO ACTIVATE ***" + echo -e "${BIWhite}*** THIS IS A NON-PERSISTENT MFA SESSION${Color_Off}! THE MFA SESSION ACCESS KEY ID,\nSECRET ACCESS KEY, AND THE SESSION TOKEN ARE *ONLY* SHOWN BELOW!" echo fi if [[ "$OS" == "macOS" ]]; then - echo "Execute the following in Terminal to activate the selected profile" - echo "(it's already on your clipboard; just paste it and press [ENTER]):" + echo -e "${BIGreen}*** 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 echo "export AWS_PROFILE=${final_selection}" @@ -1098,7 +1126,7 @@ else echo "unset AWS_SESSION_INIT_TIME" echo "unset AWS_SESSION_DURATION" echo "unset AWS_SESSION_TOKEN" - echo -n "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" | pbcopy + echo -n "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" | pbcopy else echo "export AWS_ACCESS_KEY_ID=${AWS_ACCESS_KEY_ID}" echo "export AWS_SECRET_ACCESS_KEY=${AWS_SECRET_ACCESS_KEY}" @@ -1108,19 +1136,21 @@ else 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}" - echo -n "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}" | pbcopy + echo -n "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}" | pbcopy else echo "unset AWS_SESSION_INIT_TIME" echo "unset AWS_SESSION_DURATION" echo "unset AWS_SESSION_TOKEN" - echo -n "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" | pbcopy + echo -n "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" | pbcopy echo fi fi echo - echo "NOTE: Make sure to set/unset all the new values as instructed above to make sure no conflicting profile/secrets remain in the envrionment!" + echo -e "${Green}*** Make sure to export/unset all the new values as instructed above to\n make sure no conflicting profile/secrets remain in the envrionment!" echo - echo -e "To conveniently remove any AWS profile/secrets information from the environment, simply source the attached script, like so:\nsource ./source-to-clear-AWS-envvars.sh" + echo -e "*** 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}aws sts get-caller-identity --profile default${Color_Off}" + echo + echo -e "${Green}*** 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}source ./source-to-clear-AWS-envvars.sh" echo elif [ "$OS" == "Linux" ]; then @@ -1138,7 +1168,7 @@ else echo "export AWS_SESSION_DURATION=${AWS_SESSION_DURATION}" echo "export AWS_SESSION_TOKEN=${AWS_SESSION_TOKEN}" if exists xclip ; then - echo -n "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}" | xclip -i + echo -n "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}" | xclip -i echo "(xclip found; the activation command is now on your X PRIMARY clipboard -- just paste on the command line, and press [ENTER])" fi else @@ -1146,7 +1176,7 @@ else echo "unset AWS_SESSION_DURATION" echo "unset AWS_SESSION_TOKEN" if exists xclip ; then - echo -n "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" | xclip -i + echo -n "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" | xclip -i echo "(xclip found; the activation command is now on your X PRIMARY clipboard -- just paste on the command line, and press [ENTER])" fi fi @@ -1155,9 +1185,9 @@ else echo "If you're using an X GUI on Linux, install 'xclip' to have the activation command copied to the clipboard automatically." fi echo - echo ".. or execute the following to use named profile only, clearning any previoiusly set configuration variables:" + echo ".. or execute the following to use a named profile only, clearing any previously set environment variables:" echo - echo "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" + echo "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" echo echo -e "To conveniently remove any AWS profile/secrets information from the environment, simply source the attached script, like so:\nsource ./source-to-clear-AWS-envvars.sh" echo @@ -1173,18 +1203,18 @@ else 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_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_INIT_TIME \\" + echo "unset AWS_SESSION_DURATION \\" echo "unset AWS_SESSION_TOKEN" fi echo echo "..or execute the following to use named profile only, clearing any previously set configuration variables:" echo - echo "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" + echo "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" echo echo -e "To conveniently remove any AWS profile/secrets information from the environment, simply source the attached script, like so:\nsource ./source-to-clear-AWS-envvars.sh" echo From 20996ff9010efd45883d558c9a9fbf03974fb1e3 Mon Sep 17 00:00:00 2001 From: Ville Walveranta Date: Tue, 20 Mar 2018 03:58:00 -0500 Subject: [PATCH 23/71] Various bug fixes; clarifications enhancements. --- awscli-mfa/awscli-mfa.sh | 200 ++++++++++++++++++++++++--------------- 1 file changed, 124 insertions(+), 76 deletions(-) diff --git a/awscli-mfa/awscli-mfa.sh b/awscli-mfa/awscli-mfa.sh index 389073d..93242b1 100755 --- a/awscli-mfa/awscli-mfa.sh +++ b/awscli-mfa/awscli-mfa.sh @@ -168,33 +168,54 @@ checkEnvSession() { [[ "$PRECHECK_AWS_CONFIG_FILE" =~ ^AWS_CONFIG_FILE[[:space:]]*=[[:space:]]*(.*)$ ]] && PRECHECK_AWS_CONFIG_FILE="${BASH_REMATCH[1]}" - # makes sure that the MFA session has not expired (whether it's defined - # in the environment or in ~/.aws/credentials) + # 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" != "" ]]; then + [[ "$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 + [[ "${_ret}" -eq 0 ]] && continue_maybe "expired" - elif [[ "$PRECHECK_AWS_PROFILE" != "" ]]; then - idxLookup idx profiles_ident[@] "$PRECHECK_AWS_PROFILE" - - if [[ "$PRECHECK_AWS_PROFILE" =~ -mfasession$ ]] && - [[ "$idx" != "" ]]; then - - # find the profile's init time entry if one exists - profile_time=${profiles_session_init_time[$idx]} + 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]} + + # 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" - - if [[ "$profile_time" != "" ]]; then - getRemaining _ret $profile_time $parent_duration - [[ "${_ret}" -eq 0 ]] && continue_maybe - fi - elif [[ "$idx" == "" ]]; then - echo "Incomplete AWS environment parameters set." - continue_maybe + 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 @@ -211,7 +232,7 @@ checkEnvSession() { [[ "${AWS_CONFIG_FILE}" != "" ]]; then echo - echo "** NOTE: SOME AWS_* ENVIRONMENT VARIABLES ARE CURRENTLY IN EFFECT:" + echo "** NOTE: THE FOLLOWING AWS_* ENVIRONMENT VARIABLES ARE CURRENTLY IN EFFECT:" echo if [[ "$PRECHECK_AWS_PROFILE" != "$AWS_PROFILE" ]]; then env_notice=" (overridden to 'default')" @@ -286,6 +307,7 @@ addInitTime() { # update the selected profile's existing # init time entry in this script + idxLookup idx profiles_ident[@] "$this_ident" profiles_session_init_time[$idx]=$this_time } @@ -343,9 +365,9 @@ getRemaining() { duration=$MFA_SESSION_LENGTH_IN_SECONDS if [ ! -z "${timestamp##*[!0-9]*}" ]; then - let session_end=${timestamp}+${duration} + ((session_end=${timestamp}+${duration})) if [[ $session_end -gt $this_time ]]; then - let remaining=${session_end}-${this_time} + ((remaining=${session_end}-${this_time})) else remaining=0 fi @@ -372,24 +394,21 @@ getPrintableTimeRemaining() { response="EXPIRED" ;; *) - response=$(printf '%02dh:%02dm:%02ds' $(($timestamp/3600)) $(($timestamp%3600/60)) $(($timestamp%60))) + response=$(printf '%02dh:%02dm:%02ds' $((timestamp/3600)) $((timestamp%3600/60)) $((timestamp%60))) ;; esac eval "$1=${response}" } continue_maybe() { - if [[ "${PRECHECK_AWS_PROFILE}" != "" ]] && - [[ "${PRECHECK_AWS_SESSION_TOKEN}" != "" ]]; then - echo -e "\n${BIWhite}THE MFA SESSION SET IN THE ENVIRONMENT (${PRECHECK_AWS_PROFILE}) HAS EXPIRED.${Color_Off}\n" - elif [[ "${PRECHECK_AWS_PROFILE}" == "" ]] && - [[ "${PRECHECK_AWS_SESSION_TOKEN}" != "" ]]; then - echo -e "\n${BIWhite}THE MFA SESSION SET IN THE ENVIRONMENT HAS EXPIRED.${Color_Off}\n" - elif [[ "${PRECHECK_AWS_PROFILE}" != "" ]] && - [[ "${PRECHECK_AWS_SESSION_TOKEN}" == "" ]]; then - echo -e "\n${BIWhite}THE AWS PROFILE SELECTED IN THE ENVIRONMENT (${PRECHECK_AWS_PROFILE}) IS UNKNOWN.${Color_Off}\n" + # $1 is "invalid" or "expired" + + local failtype=$1 + + if [[ "${failtype}" == "expired" ]]; then + echo -e "\n${BIRed}THE MFA SESSION SELECTED/CONFIGURED IN THE ENVIRONMENT HAS EXPIRED.${Color_Off}\n" else - return + echo -e "\n${BIRed}THE AWS PROFILE SELECTED/CONFIGURED IN THE ENVIRONMENT IS INVALID.${Color_Off}\n" fi read -s -p "$(echo -e "${BIWhite}Do you want to continue with the default profile?${Color_Off} - ${BIWhite}[Y]${Color_Off}n ")" -n 1 -r @@ -493,6 +512,12 @@ else 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 @@ -551,40 +576,50 @@ else # 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 the config file for the optional MFA length param (mfasec) + # read in the config file params while IFS='' read -r line || [[ -n "$line" ]]; do - [[ "$line" =~ ^\[[[:space:]]*profile[[:space:]]*(.*)[[:space:]]*\].* ]] && - this_conf_ident="${BASH_REMATCH[1]}" + if [[ "$line" =~ ^\[[[:space:]]*profile[[:space:]]*(.*)[[:space:]]*\].* ]]; then + _ret="${BASH_REMATCH[1]}" - [[ "$line" =~ ^[[:space:]]*mfasec[[:space:]]*=[[:space:]]*(.*)$ ]] && - this_conf_mfasec=${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 - if [[ "$this_conf_mfasec" != "" ]]; then - confs_ident[$confs_iterator]=$this_conf_ident - confs_mfasec[$confs_iterator]=$this_conf_mfasec + [[ "$line" =~ ^[[:space:]]*region[[:space:]]*=[[:space:]]*(.*)$ ]] && + confs_region[$confs_iterator]=${BASH_REMATCH[1]} - ((confs_iterator++)) - fi + [[ "$line" =~ ^[[:space:]]*output[[:space:]]*=[[:space:]]*(.*)$ ]] && + confs_output[$confs_iterator]=${BASH_REMATCH[1]} - this_conf_mfasec="" + [[ "$line" =~ ^[[:space:]]*mfasec[[:space:]]*=[[:space:]]*(.*)$ ]] && + confs_mfasec[$confs_iterator]=${BASH_REMATCH[1]} done < $CONFFILE - # make sure environment doesn't have a stale session before we start + # 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) - default_output=$(aws configure get output) + default_region=$(aws configure get region --profile default) + default_output=$(aws configure get output --profile default) if [[ "$default_region" == "" ]]; then echo - echo -e "DEFAULT REGION HAS NOT BEEN CONFIGURED.\nPlease set the default region in '~/.aws/config', for example, like so:\naws configure set region \"us-east-1\"" + echo -e "DEFAULT REGION HAS NOT BEEN CONFIGURED.\nPlease set the default region in '~/.aws/config', for example like so:\naws configure set region \"us-east-1\"" echo exit 1 fi @@ -594,14 +629,21 @@ else fi echo - current_aws_access_key_id="$(aws configure get aws_access_key_id)" - idxLookup idx profiles_key_id[@] $current_aws_access_key_id + [[ "$AWS_ACCESS_KEY_ID" != "" ]] && + current_aws_access_key_id="${AWS_ACCESS_KEY_ID}" || + current_aws_access_key_id="$(aws configure get aws_access_key_id)" + + idxLookup idx profiles_key_id[@] "$current_aws_access_key_id" if [[ $idx != "" ]]; then - currently_selected_profile_ident="${profiles_ident[$idx]}" + currently_selected_profile_ident="\"${profiles_ident[$idx]}\"" else - currently_selected_profile_ident="unknown" + if [[ "${PRECHECK_AWS_PROFILE}" != "" ]]; then + currently_selected_profile_ident="\"${PRECHECK_AWS_PROFILE}\" [transient]" + else + currently_selected_profile_ident="unknown/transient" + fi fi process_user_arn="$(aws sts get-caller-identity --output text --query 'Arn' 2>&1)" @@ -610,21 +652,20 @@ else process_username="${BASH_REMATCH[1]}" if [[ "$process_username" =~ ExpiredToken ]]; then - continue_maybe + continue_maybe "invalid" - currently_selected_profile_ident="default" + currently_selected_profile_ident="\"default\"" process_user_arn="$(aws sts get-caller-identity --output text --query 'Arn' 2>&1)" [[ "$process_user_arn" =~ ([^/]+)$ ]] && process_username="${BASH_REMATCH[1]}" fi - if [[ "$process_username" =~ error ]] || - [[ "$currently_selected_profile_ident" == "unknown" ]]; then - echo -e "The selected profile is not functional; please check the \"default\" profile\nin your '~/.aws/credentials' file, as well as any 'AWS_' environment variables!" + if [[ "$process_username" =~ error ]]; then + echo -e "The selected profile is not functional; please check the \"default\" profile\nin your '~/.aws/credentials' file, and purge any 'AWS_' environment variables!" exit 1 else - echo "Executing this script as the AWS/IAM user \"$process_username\" (profile \"$currently_selected_profile_ident\")." + echo "Executing this script as the AWS/IAM user \"$process_username\" (profile $currently_selected_profile_ident)." fi echo @@ -711,7 +752,7 @@ else # 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)" + mfa_arn="$(aws iam list-mfa-devices --profile "$profile_ident" --user-name "${cred_profile_user[$cred_profilecounter]}" --output text --query "MFADevices[].SerialNumber" 2>&1)" if [[ "$mfa_arn" =~ ^arn:aws ]]; then mfa_arns[$cred_profilecounter]="$mfa_arn" else @@ -834,7 +875,7 @@ else # if the numeric selection was found, # translate it to the array index and validate - ((actual_selprofile=${selprofile_check}-1)) + ((actual_selprofile=selprofile_check-1)) profilecount=${#cred_profiles[@]} if [[ $actual_selprofile -ge $profilecount || @@ -939,10 +980,17 @@ else AWS_2AUTH_PROFILE="${AWS_USER_PROFILE}-mfasession" ARN_OF_MFA=${mfa_arns[$actual_selprofile]} - getDuration AWS_SESSION_DURATION "$AWS_USER_PROFILE" + # 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 >> $CONFFILE + echo "[profile ${AWS_2AUTH_PROFILE}]" >> $CONFFILE + fi echo - echo -e "NOW GETTING THE MFA SESSION TOKEN FOR THE PROFILE: ${White}${AWS_USER_PROFILE}${Color_Off}" + echo -e "Acquiring MFA session token for the profile: ${BIWhite}${AWS_USER_PROFILE}${Color_Off}..." + + getDuration AWS_SESSION_DURATION "$AWS_USER_PROFILE" read AWS_ACCESS_KEY_ID AWS_SECRET_ACCESS_KEY AWS_SESSION_TOKEN <<< \ $( aws --profile "$AWS_USER_PROFILE" sts get-session-token \ @@ -973,8 +1021,8 @@ else # for the MFA profile getPrintableTimeRemaining _ret $AWS_SESSION_DURATION validity_period=${_ret} - echo -e "${BIWhite}Make this MFA session persistent${Color_Off} (saved in ~/.aws/credentials)\nso that you can return to it during its validity period (${validity_period})?" - read -s -p "$(echo -e "If you answer 'No', only the envvars will be used? ${BIWhite}[Y]${Color_Off}n ")" -n 1 -r + echo -e "${BIWhite}Make this MFA session persistent?${Color_Off} (Saves the session in ~/.aws/credentials\nso that you can return to it during its validity period, ${validity_period}.)" + read -s -p "$(echo -e "Yes (default) - make peristent; No - only the envvars will be used ${BIWhite}[Y]${Color_Off}n ")" -n 1 -r echo if [[ $REPLY =~ ^[Yy]$ ]] || [[ $REPLY == "" ]]; then @@ -1012,10 +1060,10 @@ else export AWS_PROFILE=$final_selection # get region and output format for the selected profile - AWS_DEFAULT_REGION=$(aws configure get region) - AWS_DEFAULT_OUTPUT=$(aws configure get output) + AWS_DEFAULT_REGION=$(aws configure get region --profile "${final_selection}") + AWS_DEFAULT_OUTPUT=$(aws configure get output --profile "${final_selection}") - # If the region and output format have not been set for this profile, set them + # 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 @@ -1023,18 +1071,18 @@ else if [[ "${profile_region[$actual_selprofile]}" != "" && "${mfaprofile}" == "true" ]]; then set_new_region=${profile_region[$actual_selprofile]} - echo "Region had not been configured for the selected MFA profile; it has been set to same as the parent profile ('$set_new_region')." + echo -e "Region had not been configured for the selected MFA profile;\nit has been set to same as the parent profile ('$set_new_region')." fi if [[ "${set_new_region}" == "" ]]; then set_new_region=${default_region} - echo "Region had not been configured for the selected profile; it has been set to the default region ('${default_region}')." + echo -e "Region had not been configured for the selected profile;\nit has been set to the default region ('${default_region}')." fi AWS_DEFAULT_REGION="${set_new_region}" if [[ "$mfacode" == "" ]] || ( [[ "$mfacode" != "" ]] && [[ "$persistent_MFA" == "true" ]] ); then - aws configure set region "${set_new_region}" + aws configure --profile "${final_selection}" set region "${set_new_region}" fi fi @@ -1054,17 +1102,17 @@ else if [[ "$mfacode" == "" ]] || ( [[ "$mfacode" != "" ]] && [[ "$persistent_MFA" == "true" ]] ); then - aws configure set output "${set_new_output}" + 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 get aws_access_key_id) - AWS_SECRET_ACCESS_KEY=$(aws configure get aws_secret_access_key) + AWS_ACCESS_KEY_ID=$(aws configure --profile "${final_selection}" get aws_access_key_id) + AWS_SECRET_ACCESS_KEY=$(aws configure --profile "${final_selection}" get aws_secret_access_key) if [[ "$mfaprofile" == "true" ]]; then # this is a persistent MFA profile (a subset of [[ "$mfacode" == "" ]]) - AWS_SESSION_TOKEN=$(aws configure get aws_session_token) + AWS_SESSION_TOKEN=$(aws configure --profile "${final_selection}" get aws_session_token) getInitTime _ret "${final_selection}" AWS_SESSION_INIT_TIME=${_ret} getDuration _ret "${final_selection}" @@ -1108,7 +1156,7 @@ else fi if [[ "$mfacode" != "" ]] && [[ "$persistent_MFA" == "false" ]]; then - echo -e "${BIWhite}*** THIS IS A NON-PERSISTENT MFA SESSION${Color_Off}! THE MFA SESSION ACCESS KEY ID,\nSECRET ACCESS KEY, AND THE SESSION TOKEN ARE *ONLY* SHOWN BELOW!" + echo -e "${BIWhite}*** 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 From fbaa85794592767c3735e657aa35fd10cae2fbf8 Mon Sep 17 00:00:00 2001 From: Ville Walveranta Date: Wed, 21 Mar 2018 00:30:50 -0500 Subject: [PATCH 24/71] Custom profile file selection by envvar now supported; renamed remaining.sh to status.sh --- awscli-mfa/awscli-mfa.sh | 65 +++++++++++++--- awscli-mfa/{remaining.sh => status.sh} | 103 +++++++++++++++++++++---- 2 files changed, 140 insertions(+), 28 deletions(-) rename awscli-mfa/{remaining.sh => status.sh} (75%) diff --git a/awscli-mfa/awscli-mfa.sh b/awscli-mfa/awscli-mfa.sh index 93242b1..df9e372 100755 --- a/awscli-mfa/awscli-mfa.sh +++ b/awscli-mfa/awscli-mfa.sh @@ -446,31 +446,72 @@ This script requires the AWS CLI. See the details here: http://docs.aws.amazon.c exit 1 fi +filexit="false" # check for ~/.aws directory, and ~/.aws/{config|credentials} files -if [ ! -d ~/.aws ]; then +if [[ "$AWS_CONFIG_FILE" == "" ]] && + [[ "$AWS_SHARED_CREDENTIALS_FILE" == "" ]] && + [ ! -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 + filexit="true" fi -if [[ ! -f ~/.aws/config && ! -f ~/.aws/credentials ]]; then +# 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 "'~/.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 -e "** NOTE: A custom configuration file defined with AWS_CONFIG_FILE envvar in effect: '$AWS_CONFIG_FILE'" + +elif [[ "$AWS_CONFIG_FILE" != "" ]] && + [ ! -f "$AWS_CONFIG_FILE" ]; then + echo - exit 1 -elif [ ! -f ~/.aws/config ]; then + echo -e "The custom config file defined with AWS_CONFIG_FILE envvar, '$AWS_CONFIG_FILE', is not present.\nMake sure it is present or purge the envvar. See 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 "'$CONFFILE' file not present.\nMake sure it and '$CREDFILE' files exist. See 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 "'~/.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 -e "** NOTE: A custom credentials file defined with AWS_SHARED_CREDENTIALS_FILE envvar in effect: '$AWS_SHARED_CREDENTIALS_FILE'" + +elif [[ "$AWS_SHARED_CREDENTIALS_FILE" != "" ]] && + [ ! -f "$AWS_SHARED_CREDENTIALS_FILE" ]; then + echo - exit 1 -elif [ ! -f ~/.aws/credentials ]; then + echo -e "The custom credentials file defined with AWS_SHARED_CREDENTIALS_FILE envvar, '$AWS_SHARED_CREDENTIALS_FILE', is not present.\nMake sure it is present or purge the envvar. See 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 "'~/.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 -e "'$CREDFILE' file not present.\nMake sure it and '$CONFFILE' files exist. See 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 @@ -484,7 +525,7 @@ 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 -e "NO CONFIGURED AWS PROFILES FOUND.\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 diff --git a/awscli-mfa/remaining.sh b/awscli-mfa/status.sh similarity index 75% rename from awscli-mfa/remaining.sh rename to awscli-mfa/status.sh index e82da40..069a294 100755 --- a/awscli-mfa/remaining.sh +++ b/awscli-mfa/status.sh @@ -25,8 +25,83 @@ MFA_SESSION_LENGTH_IN_SECONDS=32400 CONFFILE=~/.aws/config CREDFILE=~/.aws/credentials - -# FUNCTIONS +# 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 @@ -44,7 +119,7 @@ idxLookup() { maxIndex=${#arr[@]} ((maxIndex--)) - for (( i=0; i<=${maxIndex}; i++ )) + for (( i=0; i<=maxIndex; i++ )) do if [[ "${arr[$i]}" == "$key" ]]; then result=$i @@ -67,7 +142,7 @@ getDuration() { this_profile_ident="${BASH_REMATCH[1]}" # look up possible custom duration for the parent profile - idxLookup idx confs_ident[@] $this_profile_ident + idxLookup idx confs_ident[@] "$this_profile_ident" [[ $idx != "" && "${confs_mfasec[$idx]}" != "" ]] && this_duration=${confs_mfasec[$idx]} || @@ -94,9 +169,9 @@ getRemaining() { duration=$MFA_SESSION_LENGTH_IN_SECONDS if [ ! -z "${timestamp##*[!0-9]*}" ]; then - let session_end=${timestamp}+${duration} + ((session_end=timestamp+duration)) if [[ $session_end -gt $this_time ]]; then - let remaining=${session_end}-${this_time} + ((remaining=session_end-this_time)) else remaining=0 fi @@ -123,14 +198,14 @@ getPrintableTimeRemaining() { response="EXPIRED" ;; *) - response=$(printf '%02dh:%02dm:%02ds' $(($timestamp/3600)) $(($timestamp%3600/60)) $(($timestamp%60))) + 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 + idxLookup idx profiles_key_id[@] "$AWS_ACCESS_KEY_ID" if [ "$idx" = "" ]; then if [[ ${AWS_PROFILE} != "" ]]; then matched="(not the persistent session)" @@ -195,10 +270,7 @@ if [[ "$AWS_SESSION_INIT_TIME" != "" ]]; then [[ "${AWS_SESSION_DURATION}" == "" ]] && AWS_SESSION_DURATION=$MFA_SESSION_LENGTH_IN_SECONDS - let IN_ENV_SESSION_TIME=${AWS_SESSION_INIT_TIME}+${AWS_SESSION_DURATION} - ENV_TIME="true" -else - ENV_TIME="false" + ((IN_ENV_SESSION_TIME=AWS_SESSION_INIT_TIME+AWS_SESSION_DURATION)) fi # COLLECT AWS CONFIG DATA FROM ~/.aws/config @@ -206,10 +278,9 @@ fi # init arrays to hold ident<->mfasec detail declare -a confs_ident declare -a confs_mfasec -confs_init=0 confs_iterator=0 -# read the config file for the optional MFA length param (MAXSEC) +# read the config file for the optional MFA length param (mfasec) while IFS='' read -r line || [[ -n "$line" ]]; do [[ "$line" =~ ^\[[[:space:]]*profile[[:space:]]*(.*)[[:space:]]*\].* ]] && @@ -318,7 +389,7 @@ maxIndex=${#profiles_ident[@]} live_session_counter=0 -for (( z=0; z<=${maxIndex}; z++ )) +for (( z=0; z<=maxIndex; z++ )) do if [[ "${profiles_type[$z]}" == "session" ]]; then @@ -326,7 +397,7 @@ do echo "MFA SESSION IDENT: ${profiles_ident[$z]}" if [[ "${profiles_session_init_time[$z]}" != "" ]]; then - getDuration _ret_duration ${profiles_ident[$z]} + getDuration _ret_duration "${profiles_ident[$z]}" getRemaining _ret_remaining ${profiles_session_init_time[$z]} ${_ret_duration} getPrintableTimeRemaining _ret ${_ret_remaining} if [ "${_ret}" = "EXPIRED" ]; then From 1158dbaa57f4691e9ee28d5b5494a279d8ec002e Mon Sep 17 00:00:00 2001 From: Ville Walveranta Date: Sun, 25 Mar 2018 00:47:27 -0500 Subject: [PATCH 25/71] Various fixes and enhancements. Checkpoint before macOS+Linux outputs get merged. --- awscli-mfa/awscli-mfa.sh | 71 ++++++++++++++++++++++++++-------------- 1 file changed, 47 insertions(+), 24 deletions(-) diff --git a/awscli-mfa/awscli-mfa.sh b/awscli-mfa/awscli-mfa.sh index df9e372..41abfd2 100755 --- a/awscli-mfa/awscli-mfa.sh +++ b/awscli-mfa/awscli-mfa.sh @@ -1,13 +1,16 @@ #!/usr/bin/env bash -# todo: AWS_SHARED_CREDENTIALS_FILE and AWS_CONFIG_FILE values should either -# be utilized, or their presence should not be allowed -# # todo: when there is only one single profile, bypass the profile # selection menu # +# todo: if custom config/credentials files are selected, shouldn't they be kept past the selection? +# This script should not modify them, right? +# +# todo: secrets_out has not been configured for Linux, OTHER!!!! :-O +# # todo: test new functionality on Linux + DEBUG="false" # uncomment below to enable the debug output #DEBUG="true" @@ -30,7 +33,12 @@ DEBUG="false" # 32400 seconds, or 9 hours. MFA_SESSION_LENGTH_IN_SECONDS=32400 -# define the standard location of the AWS credentials and config files +# 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 @@ -400,6 +408,7 @@ getPrintableTimeRemaining() { eval "$1=${response}" } +# here are my args, so.. continue_maybe() { # $1 is "invalid" or "expired" @@ -415,6 +424,17 @@ continue_maybe() { if [[ $REPLY =~ ^[Yy]$ ]] || [[ $REPLY == "" ]]; then + # 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 @@ -424,8 +444,6 @@ continue_maybe() { unset AWS_DEFAULT_REGION unset AWS_DEFAULT_OUTPUT unset AWS_CA_BUNDLE - unset AWS_SHARED_CREDENTIALS_FILE - unset AWS_CONFIG_FILE # override envvar for all the subshell commands export AWS_PROFILE=default @@ -453,7 +471,7 @@ if [[ "$AWS_CONFIG_FILE" == "" ]] && [ ! -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 -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 @@ -463,20 +481,20 @@ if [[ "$AWS_CONFIG_FILE" != "" ]] && active_config_file=$AWS_CONFIG_FILE echo - echo -e "** NOTE: A custom configuration file defined with AWS_CONFIG_FILE envvar in effect: '$AWS_CONFIG_FILE'" + echo -e "${BIWhite}** 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 "The custom config file defined with AWS_CONFIG_FILE envvar, '$AWS_CONFIG_FILE', is not present.\nMake sure it is present or purge the envvar. See http://docs.aws.amazon.com/cli/latest/userguide/cli-config-files.html for details on how to set them up." + echo -e "${BIRed}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 "'$CONFFILE' file not present.\nMake sure it and '$CREDFILE' files exist. See http://docs.aws.amazon.com/cli/latest/userguide/cli-config-files.html for details on how to set them up." + echo -e "${BIRed}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 @@ -486,20 +504,20 @@ if [[ "$AWS_SHARED_CREDENTIALS_FILE" != "" ]] && active_credentials_file=$AWS_SHARED_CREDENTIALS_FILE echo - echo -e "** NOTE: A custom credentials file defined with AWS_SHARED_CREDENTIALS_FILE envvar in effect: '$AWS_SHARED_CREDENTIALS_FILE'" + echo -e "${BIWhite}** 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 "The custom credentials file defined with AWS_SHARED_CREDENTIALS_FILE envvar, '$AWS_SHARED_CREDENTIALS_FILE', is not present.\nMake sure it is present or purge the envvar. See http://docs.aws.amazon.com/cli/latest/userguide/cli-config-files.html for details on how to set them up." + echo -e "${BIRed}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 "'$CREDFILE' file not present.\nMake sure it and '$CONFFILE' files exist. See http://docs.aws.amazon.com/cli/latest/userguide/cli-config-files.html for details on how to set them up." + echo -e "${BIRed}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 @@ -525,7 +543,7 @@ done < $CREDFILE if [[ "$ONEPROFILE" == "false" ]]; then echo - echo -e "NO CONFIGURED AWS PROFILES FOUND.\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 -e "${BIRed}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 @@ -660,7 +678,7 @@ else if [[ "$default_region" == "" ]]; then echo - echo -e "DEFAULT REGION HAS NOT BEEN CONFIGURED.\nPlease set the default region in '~/.aws/config', for example like so:\naws configure set region \"us-east-1\"" + echo -e "${BIWhite}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 @@ -703,7 +721,7 @@ else fi if [[ "$process_username" =~ error ]]; then - echo -e "The selected profile is not functional; please check the \"default\" profile\nin your '~/.aws/credentials' file, and purge any 'AWS_' environment variables!" + echo -e "${BIRed}The selected profile is not functional${Color_Off}; please check the \"default\" profile\nin your '$CREDFILE' file, and purge any 'AWS_' environment variables!" exit 1 else echo "Executing this script as the AWS/IAM user \"$process_username\" (profile $currently_selected_profile_ident)." @@ -1063,7 +1081,7 @@ else getPrintableTimeRemaining _ret $AWS_SESSION_DURATION validity_period=${_ret} echo -e "${BIWhite}Make this MFA session persistent?${Color_Off} (Saves the session in ~/.aws/credentials\nso that you can return to it during its validity period, ${validity_period}.)" - read -s -p "$(echo -e "Yes (default) - make peristent; No - only the envvars will be used ${BIWhite}[Y]${Color_Off}n ")" -n 1 -r + read -s -p "$(echo -e "${BIWhite}Yes (default) - make peristent${Color_Off}; No - only the envvars will be used ${BIWhite}[Y]${Color_Off}n ")" -n 1 -r echo if [[ $REPLY =~ ^[Yy]$ ]] || [[ $REPLY == "" ]]; then @@ -1112,11 +1130,11 @@ else if [[ "${profile_region[$actual_selprofile]}" != "" && "${mfaprofile}" == "true" ]]; then set_new_region=${profile_region[$actual_selprofile]} - echo -e "Region had not been configured for the selected MFA profile;\nit has been set to same as the parent profile ('$set_new_region')." + echo -e "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 set_new_region=${default_region} - echo -e "Region had not been configured for the selected profile;\nit has been set to the default region ('${default_region}')." + echo -e "NOTE: Region had not been configured for the selected profile;\n it has been set to the default region ('${default_region}')." fi AWS_DEFAULT_REGION="${set_new_region}" @@ -1132,11 +1150,11 @@ else if [[ "${profile_output[$actual_selprofile]}" != "" && "${mfaprofile}" == "true" ]]; then set_new_output=${profile_output[$actual_selprofile]} - echo "Output format had not been configured for the selected MFA profile; it has been set to same as the parent profile ('$set_new_output')." + echo -e "NOTE: 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 "Output format had not been configured for the selected profile; it has been set to the default output format ('${default_output}')." + echo -e "Output format had not been configured for the selected profile;\n it has been set to the default output format ('${default_output}')." fi AWS_DEFAULT_OUTPUT="${set_new_output}" @@ -1205,8 +1223,14 @@ else echo -e "${BIGreen}*** 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 - echo "export AWS_PROFILE=${final_selection}" - + if [ "$final_selection" == "default" ]; then + # default profile doesn't need to be selected with an envvar + echo -n "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" | pbcopy + echo "unset AWS_PROFILE" + else + echo -n "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" | pbcopy + echo "export AWS_PROFILE=${final_selection}" + fi if [[ "$secrets_out" == "false" ]]; then echo "unset AWS_ACCESS_KEY_ID" echo "unset AWS_SECRET_ACCESS_KEY" @@ -1215,7 +1239,6 @@ else echo "unset AWS_SESSION_INIT_TIME" echo "unset AWS_SESSION_DURATION" echo "unset AWS_SESSION_TOKEN" - echo -n "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" | pbcopy else echo "export AWS_ACCESS_KEY_ID=${AWS_ACCESS_KEY_ID}" echo "export AWS_SECRET_ACCESS_KEY=${AWS_SECRET_ACCESS_KEY}" From bac61ae69b68d93b3f83dc801131353783f0297b Mon Sep 17 00:00:00 2001 From: Ville Walveranta Date: Sun, 25 Mar 2018 03:57:58 -0500 Subject: [PATCH 26/71] Combined macOS and Linux outputs; completed Linux and 'other' system outputs. Other fixes. --- awscli-mfa/awscli-mfa.sh | 213 ++++++++++++++++++++++++--------------- 1 file changed, 133 insertions(+), 80 deletions(-) diff --git a/awscli-mfa/awscli-mfa.sh b/awscli-mfa/awscli-mfa.sh index 41abfd2..00b6c0e 100755 --- a/awscli-mfa/awscli-mfa.sh +++ b/awscli-mfa/awscli-mfa.sh @@ -3,11 +3,6 @@ # todo: when there is only one single profile, bypass the profile # selection menu # -# todo: if custom config/credentials files are selected, shouldn't they be kept past the selection? -# This script should not modify them, right? -# -# todo: secrets_out has not been configured for Linux, OTHER!!!! :-O -# # todo: test new functionality on Linux @@ -433,6 +428,8 @@ continue_maybe() { unset AWS_SHARED_CREDENTIALS_FILE unset AWS_CONFIG_FILE + + custom_configfiles_reset="true" fi unset AWS_PROFILE @@ -528,7 +525,7 @@ fi CONFFILE="$active_config_file" CREDFILE="$active_credentials_file" - +custom_configfiles_reset="false" # read the credentials file and make sure that at least one profile is configured ONEPROFILE="false" @@ -536,7 +533,7 @@ while IFS='' read -r line || [[ -n "$line" ]]; do [[ "$line" =~ ^\[(.*)\].* ]] && profile_ident="${BASH_REMATCH[1]}" - if [ "$profile_ident" != "" ]; then + if [[ "$profile_ident" != "" ]]; then ONEPROFILE="true" fi done < $CREDFILE @@ -567,13 +564,13 @@ else # make sure ~/.aws/credentials has a linefeed in the end c=$(tail -c 1 "$CREDFILE") - if [ "$c" != "" ]; then + if [[ "$c" != "" ]]; then echo "" >> "$CREDFILE" fi # make sure ~/.aws/config has a linefeed in the end c=$(tail -c 1 "$CONFFILE") - if [ "$c" != "" ]; then + if [[ "$c" != "" ]]; then echo "" >> "$CONFFILE" fi @@ -823,7 +820,7 @@ else # 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 + if [[ "$mfa_profile_ident" != "" ]]; then getInitTime _ret_timestamp "$mfa_profile_ident" getDuration _ret_duration "$mfa_profile_ident" @@ -853,7 +850,7 @@ else fi ## DEBUG (enable with DEBUG="true" on top of the file) - if [ "$DEBUG" == "true" ]; then + if [[ "$DEBUG" == "true" ]]; then echo echo "PROFILE IDENT: $profile_ident (${cred_profile_status[$cred_profilecounter]})" @@ -861,7 +858,7 @@ else echo "USER NAME: ${cred_profile_user[$cred_profilecounter]}" echo "MFA ARN: ${mfa_arns[$cred_profilecounter]}" echo "MFA MAXSEC: ${mfa_mfasec[$cred_profilecounter]}" - if [ "${mfa_profiles[$cred_profilecounter]}" == "" ]; then + if [[ "${mfa_profiles[$cred_profilecounter]}" == "" ]]; then echo "MFA PROFILE IDENT:" else echo "MFA PROFILE IDENT: ${mfa_profiles[$cred_profilecounter]} (${mfa_profile_status[$cred_profilecounter]})" @@ -895,7 +892,7 @@ else ITER=1 for i in "${cred_profiles[@]}" do - if [ "${mfa_arns[$SELECTR]}" != "" ]; then + if [[ "${mfa_arns[$SELECTR]}" != "" ]]; then mfa_notify="; ${Green}vMFAd enabled${Color_Off}" else mfa_notify="; vMFAd not configured" @@ -1075,12 +1072,12 @@ else # (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 entries): + # 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}Make this MFA session persistent?${Color_Off} (Saves the session in ~/.aws/credentials\nso that you can return to it during its validity period, ${validity_period}.)" + echo -e "${BIWhite}Make this MFA session persistent?${Color_Off} (Saves the session in $CREDFILE\nso that you can return to it during its validity period, ${validity_period}.)" read -s -p "$(echo -e "${BIWhite}Yes (default) - make peristent${Color_Off}; No - only the envvars will be used ${BIWhite}[Y]${Color_Off}n ")" -n 1 -r echo if [[ $REPLY =~ ^[Yy]$ ]] || @@ -1092,12 +1089,12 @@ else aws configure set aws_session_token "$AWS_SESSION_TOKEN" # set init time in the static MFA profile (a custom key in ~/.aws/credentials) addInitTime "${AWS_2AUTH_PROFILE}" - fi + fi # init time for envvar exports (if selected) AWS_SESSION_INIT_TIME=$(date +%s) ## DEBUG - if [ "$DEBUG" == "true" ]; then + if [[ "$DEBUG" == "true" ]]; then echo echo "AWS_ACCESS_KEY_ID: $AWS_ACCESS_KEY_ID" echo "AWS_SECRET_ACCESS_KEY: $AWS_SECRET_ACCESS_KEY" @@ -1130,11 +1127,11 @@ else if [[ "${profile_region[$actual_selprofile]}" != "" && "${mfaprofile}" == "true" ]]; then set_new_region=${profile_region[$actual_selprofile]} - echo -e "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')." + echo -e "\nNOTE: 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 set_new_region=${default_region} - echo -e "NOTE: Region had not been configured for the selected profile;\n it has been set to the default region ('${default_region}')." + echo -e "\nNOTE: Region had not been configured for the selected profile;\n it has been set to the default region ('${default_region}')." fi AWS_DEFAULT_REGION="${set_new_region}" @@ -1145,7 +1142,7 @@ else fi fi - if [ "${AWS_DEFAULT_OUTPUT}" == "" ]; then + if [[ "${AWS_DEFAULT_OUTPUT}" == "" ]]; then # retrieve parent profile output format if an MFA profile if [[ "${profile_output[$actual_selprofile]}" != "" && "${mfaprofile}" == "true" ]]; then @@ -1219,18 +1216,51 @@ else echo fi - if [[ "$OS" == "macOS" ]]; then + if [[ "$OS" == "macOS" ]] || + [[ "$OS" == "Linux" ]] ; then echo -e "${BIGreen}*** 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 - if [ "$final_selection" == "default" ]; then + + # 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 - echo -n "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" | pbcopy + 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 + echo + fi echo "unset AWS_PROFILE" else - echo -n "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" | pbcopy - echo "export AWS_PROFILE=${final_selection}" + 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 + 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" @@ -1240,21 +1270,47 @@ else 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_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}" - echo -n "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}" | pbcopy + 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 + fi else echo "unset AWS_SESSION_INIT_TIME" echo "unset AWS_SESSION_DURATION" echo "unset AWS_SESSION_TOKEN" - echo -n "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" | pbcopy + + 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 + fi + fi + fi + echo + if [[ "$OS" == "Linux" ]]; then + if exists xclip; then + echo "${BIGreen}*** NOTE: xclip found; the envvar configuration command is now on your X PRIMARY clipboard -- just paste on the command line, and press [ENTER])${Color_Off}" + else echo + echo "*** NOTE: If you're using an X GUI on Linux, install 'xclip' to have the activation command copied to the clipboard automatically!" fi fi echo @@ -1265,70 +1321,67 @@ else echo -e "${Green}*** 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}source ./source-to-clear-AWS-envvars.sh" echo - elif [ "$OS" == "Linux" ]; then - echo "Execute the following on the command line to activate this profile for the 'aws', 's3cmd', etc. commands." + else # not macOS, not Linux, so some other weird OS like Windows.. + + echo "It is imperative that the following environment variables are exported/unset to activate the selected profile!" + echo + echo "Execute the following on the command line to activate this profile for the 'aws', 's3cmd', etc. commands." + echo echo "NOTE: Even if you only use a named profile ('AWS_PROFILE'), it's important to execute all of the export/unset" echo " commands to make sure previously set environment variables won't override the selected configuration." echo - 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}" - if exists xclip ; then - echo -n "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}" | xclip -i - echo "(xclip found; the activation command is now on your X PRIMARY clipboard -- just paste on the command line, and press [ENTER])" - fi + + if [[ "$final_selection" == "default" ]]; then + # default profile doesn't need to be selected with an envvar + echo "unset AWS_PROFILE \\" else - echo "unset AWS_SESSION_INIT_TIME" - echo "unset AWS_SESSION_DURATION" - echo "unset AWS_SESSION_TOKEN" - if exists xclip ; then - echo -n "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" | xclip -i - echo "(xclip found; the activation command is now on your X PRIMARY clipboard -- just paste on the command line, and press [ENTER])" - fi + echo "export AWS_PROFILE=\"${final_selection}\" \\" fi - if ! exists xclip ; then - echo - echo "If you're using an X GUI on Linux, install 'xclip' to have the activation command copied to the clipboard automatically." + + # 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 - echo - echo ".. or execute the following to use a named profile only, clearing any previously set environment variables:" - echo - echo "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" - echo - echo -e "To conveniently remove any AWS profile/secrets information from the environment, simply source the attached script, like so:\nsource ./source-to-clear-AWS-envvars.sh" - echo - else # not macOS, not Linux, so some other weird OS like Windows.. - echo "Execute the following on the command line to activate this profile for the 'aws', 's3cmd', etc. commands." - echo "NOTE: Even if you only use a named profile ('AWS_PROFILE'), it's important to execute all of the export/unset" - echo " commands to make sure previously set environment variables won't override the selected configuration." - echo - 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 + 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 - echo "..or execute the following to use named profile only, clearing any previously set configuration variables:" + echo "*** Make sure to export/unset all the new values as instructed above to" + echo " make sure no conflicting profile/secrets remain in the envrionment!" echo - echo "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" + echo "*** You can temporarily override the profile set/selected in the environment" + echo " using the \"--profile AWS_PROFILE_NAME\" switch with awscli. For example:" + echo " aws sts get-caller-identity --profile default" echo - echo -e "To conveniently remove any AWS profile/secrets information from the environment, simply source the attached script, like so:\nsource ./source-to-clear-AWS-envvars.sh" + echo "*** To easily remove any all AWS profile settings and secrets information" + echo " from the environment, simply source the included script, like so:" + echo " source ./source-to-clear-AWS-envvars.sh" echo fi From c1ac438f174749f60dd0807d27f15021d9f35956 Mon Sep 17 00:00:00 2001 From: Ville Walveranta Date: Sun, 25 Mar 2018 19:44:41 -0500 Subject: [PATCH 27/71] Single profile (+ MFA session) - a common use-case - now handled more simply. --- awscli-mfa/awscli-mfa.sh | 210 ++++++++++++++++++++++++++------------- 1 file changed, 139 insertions(+), 71 deletions(-) diff --git a/awscli-mfa/awscli-mfa.sh b/awscli-mfa/awscli-mfa.sh index 00b6c0e..b6a4edd 100755 --- a/awscli-mfa/awscli-mfa.sh +++ b/awscli-mfa/awscli-mfa.sh @@ -1,11 +1,5 @@ #!/usr/bin/env bash -# todo: when there is only one single profile, bypass the profile -# selection menu -# -# todo: test new functionality on Linux - - DEBUG="false" # uncomment below to enable the debug output #DEBUG="true" @@ -404,50 +398,56 @@ getPrintableTimeRemaining() { } # here are my args, so.. +already_failed="false" continue_maybe() { # $1 is "invalid" or "expired" local failtype=$1 - if [[ "${failtype}" == "expired" ]]; then - echo -e "\n${BIRed}THE MFA SESSION SELECTED/CONFIGURED IN THE ENVIRONMENT HAS EXPIRED.${Color_Off}\n" - else - echo -e "\n${BIRed}THE AWS PROFILE SELECTED/CONFIGURED IN THE ENVIRONMENT IS INVALID.${Color_Off}\n" - fi + if [[ "$already_failed" == "false" ]]; then - read -s -p "$(echo -e "${BIWhite}Do you want to continue with the default profile?${Color_Off} - ${BIWhite}[Y]${Color_Off}n ")" -n 1 -r - if [[ $REPLY =~ ^[Yy]$ ]] || - [[ $REPLY == "" ]]; then + if [[ "${failtype}" == "expired" ]]; then + echo -e "\n${BIRed}THE MFA SESSION SELECTED/CONFIGURED IN THE ENVIRONMENT HAS EXPIRED.${Color_Off}\n" + else + echo -e "\n${BIRed}THE AWS PROFILE SELECTED/CONFIGURED IN THE ENVIRONMENT IS INVALID.${Color_Off}\n" + fi - # 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 + read -s -p "$(echo -e "${BIWhite}Do you want to continue with the default profile?${Color_Off} - ${BIWhite}[Y]${Color_Off}n ")" -n 1 -r + if [[ $REPLY =~ ^[Yy]$ ]] || + [[ $REPLY == "" ]]; then - custom_configfiles_reset="true" - fi + already_failed="true" - 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-to-clear-AWS-envvars.sh\", and try again to proceed.\n" - exit 1 + # 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 + + 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-to-clear-AWS-envvars.sh\", and try again to proceed.\n" + exit 1 + fi fi } @@ -884,43 +884,108 @@ else done < $CREDFILE echo -e "${Color_Off}" - # create the profile selections - echo - echo -e "${BBlack}${On_White} AVAILABLE AWS PROFILES: ${Color_Off}" - echo - SELECTR=0 - ITER=1 - for i in "${cred_profiles[@]}" - do - if [[ "${mfa_arns[$SELECTR]}" != "" ]]; then - mfa_notify="; ${Green}vMFAd enabled${Color_Off}" + # select the profile (first, single profile + a possible persistent MFA session) + mfa_req="false" + if [[ ${#cred_profiles[@]} == 1 ]]; then + echo + echo -e "${Green}You have one configured profile: ${BIGreen}${cred_profiles[0]} ${Green}(IAM: ${cred_profile_user[0]})${Color_Off}" + + mfa_session_status="false" + if [[ "${mfa_arns[0]}" != "" ]]; then + echo ".. its vMFAd is enabled" + + if [[ "${mfa_profile_status[0]}" != "EXPIRED" && + "${mfa_profile_status[0]}" != "" ]]; then + + echo -e ".. and it ${BIWhite}has an active MFA session with ${mfa_profile_status[0]}${Color_Off}" + + mfa_session_status="true" + else + echo -e ".. but no active persistent MFA sessions exist" + fi else - mfa_notify="; vMFAd not configured" + echo -e "${BIRed}.. but it doesn't have a virtual MFA device attached/enabled;\n cannot continue${Color_Off} (use register-virtual-mfa-device.sh script first to enable a vMFAd)!" + echo + exit 1 fi - echo -en "${BIWhite}${ITER}: $i${Color_Off} (IAM: ${cred_profile_user[$SELECTR]}${mfa_notify})\n" + echo + echo "Do you want to:" + echo -e "${BIWhite}1${Color_Off}: Start/renew an MFA session for the profile mentioned above?" + echo -e "${BIWhite}2${Color_Off}: Use the above profile as-is (without MFA)?" + [[ "${mfa_session_status}" == "true" ]] && echo -e "${BIWhite}3${Color_Off}: Resume the existing active MFA session (${mfa_profile_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 - if [[ "${mfa_profile_status[$SELECTR]}" != "EXPIRED" && - "${mfa_profile_status[$SELECTR]}" != "" ]]; then - echo -e "${BIWhite}${ITER}m: $i MFA profile${Color_Off} (${mfa_profile_status[$SELECTR]})" - fi + else # more than 1 profile + # create the profile selections echo - ((ITER++)) - ((SELECTR++)) - done + echo -e "${BBlack}${On_White} AVAILABLE AWS PROFILES: ${Color_Off}" + echo + SELECTR=0 + ITER=1 + for i in "${cred_profiles[@]}" + do + if [[ "${mfa_arns[$SELECTR]}" != "" ]]; then + mfa_notify="; ${Green}vMFAd enabled${Color_Off}" + else + mfa_notify="; vMFAd not configured" + fi - # this is used to determine whether to trigger a MFA request for a MFA profile - active_mfa="false" + echo -en "${BIWhite}${ITER}: $i${Color_Off} (IAM: ${cred_profile_user[$SELECTR]}${mfa_notify})\n" - # this is used to determine whether to print MFA questions/details - mfaprofile="false" + if [[ "${mfa_profile_status[$SELECTR]}" != "EXPIRED" && + "${mfa_profile_status[$SELECTR]}" != "" ]]; then + echo -e "${BIWhite}${ITER}m: $i MFA profile${Color_Off} (${mfa_profile_status[$SELECTR]})" + fi + + echo + ((ITER++)) + ((SELECTR++)) + done - # 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 it 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}SELECT A PROFILE BY THE ID: " - read -r selprofile - echo -en "\n${Color_Off}" + # 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 it 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}SELECT A PROFILE BY THE ID: " + read -r selprofile + echo -en "\n${Color_Off}" + + fi # end profile selection # process the selection if [[ "$selprofile" != "" ]]; then @@ -998,8 +1063,9 @@ else fi # this is an MFA request (an MFA ARN exists but the MFA is not active) - if [[ "${mfa_arns[$actual_selprofile]}" != "" && - "$active_mfa" == "false" ]]; then + if ( [[ "${mfa_arns[$actual_selprofile]}" != "" && + "$active_mfa" == "false" ]] ) || + [[ "$mfa_req" == "true" ]]; then # mfa_req is a single profile MFA request # prompt for the MFA code echo @@ -1320,6 +1386,8 @@ else echo echo -e "${Green}*** 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}source ./source-to-clear-AWS-envvars.sh" echo + echo -e "${BIWhite}PASTE THE PROFILE ACTIVATION COMMAND FROM THE CLIPBOARD\nON THE COMMAND LINE NOW, AND PRESS ENTER! THEN YOU'RE DONE!${Color_Off}" + echo else # not macOS, not Linux, so some other weird OS like Windows.. From 3aa66d31dfe602b99eeb19a5c92df86f6f6db704 Mon Sep 17 00:00:00 2001 From: Ville Walveranta Date: Mon, 26 Mar 2018 00:38:38 -0500 Subject: [PATCH 28/71] Quoted more vars to allow spaces and to avoid errors from word splitting. --- awscli-mfa/awscli-mfa.sh | 23 ++++++++++++----------- 1 file changed, 12 insertions(+), 11 deletions(-) diff --git a/awscli-mfa/awscli-mfa.sh b/awscli-mfa/awscli-mfa.sh index b6a4edd..c22c43e 100755 --- a/awscli-mfa/awscli-mfa.sh +++ b/awscli-mfa/awscli-mfa.sh @@ -294,12 +294,12 @@ addInitTime() { # update/add session init time if [[ $profile_time != "" ]]; then # time entry exists for the profile, update - sed -i '' -e "s/${profile_time}/${this_time}/g" $CREDFILE + sed -i '' -e "s/${profile_time}/${this_time}/g" "$CREDFILE" 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 + echo "$(awk -v var="${DATA//$'\n'/\\n}" '{sub(/'$replace_me'/,var)}1' "$CREDFILE")" > "$CREDFILE" fi # update the selected profile's existing @@ -397,8 +397,8 @@ getPrintableTimeRemaining() { eval "$1=${response}" } -# here are my args, so.. already_failed="false" +# here are my args, so.. continue_maybe() { # $1 is "invalid" or "expired" @@ -463,6 +463,7 @@ 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 @@ -536,7 +537,7 @@ while IFS='' read -r line || [[ -n "$line" ]]; do if [[ "$profile_ident" != "" ]]; then ONEPROFILE="true" fi -done < $CREDFILE +done < "$CREDFILE" if [[ "$ONEPROFILE" == "false" ]]; then echo @@ -627,7 +628,7 @@ else [[ "$line" =~ ^aws_session_init_time[[:space:]]*=[[:space:]]*(.*)$ ]] && profiles_session_init_time[$profiles_iterator]=${BASH_REMATCH[1]} - done < $CREDFILE + done < "$CREDFILE" # init arrays to hold ident<->mfasec detail @@ -662,7 +663,7 @@ else [[ "$line" =~ ^[[:space:]]*mfasec[[:space:]]*=[[:space:]]*(.*)$ ]] && confs_mfasec[$confs_iterator]=${BASH_REMATCH[1]} - done < $CONFFILE + done < "$CONFFILE" # make sure environment has either no config or a functional config # before we proceed @@ -791,7 +792,7 @@ else while IFS='' read -r line || [[ -n "$line" ]]; do [[ "$line" =~ \[(${profile_ident}-mfasession)\]$ ]] && mfa_profile_ident="${BASH_REMATCH[1]}" - done < $CREDFILE + done < "$CREDFILE" mfa_profiles[$cred_profilecounter]="$mfa_profile_ident" # check to see if this profile has access currently @@ -881,7 +882,7 @@ else ((cred_profilecounter++)) fi - done < $CREDFILE + done < "$CREDFILE" echo -e "${Color_Off}" # select the profile (first, single profile + a possible persistent MFA session) @@ -1103,10 +1104,10 @@ else ARN_OF_MFA=${mfa_arns[$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:]]*$')" + profile_lookup="$(grep "$CONFFILE" -e '^[[:space:]]*\[[[:space:]]*profile '${AWS_2AUTH_PROFILE}'[[:space:]]*\][[:space:]]*$')" if [[ "$profile_lookup" == "" ]]; then - echo >> $CONFFILE - echo "[profile ${AWS_2AUTH_PROFILE}]" >> $CONFFILE + echo >> "$CONFFILE" + echo "[profile ${AWS_2AUTH_PROFILE}]" >> "$CONFFILE" fi echo From b3eeaf94b371a767e988c5402f4046ee309fb01f Mon Sep 17 00:00:00 2001 From: Ville Walveranta Date: Mon, 26 Mar 2018 00:39:36 -0500 Subject: [PATCH 29/71] Updated status.sh; LOTS of updates and bug fixes. Now also supports spaces in profile names. --- awscli-mfa/awscli-mfa.sh | 4 +- awscli-mfa/status.sh | 182 +++++++++++++++++++++++++++++++-------- 2 files changed, 149 insertions(+), 37 deletions(-) diff --git a/awscli-mfa/awscli-mfa.sh b/awscli-mfa/awscli-mfa.sh index c22c43e..85c2e0f 100755 --- a/awscli-mfa/awscli-mfa.sh +++ b/awscli-mfa/awscli-mfa.sh @@ -1,5 +1,7 @@ #!/usr/bin/env bash +# todo: handle roles with MFA + DEBUG="false" # uncomment below to enable the debug output #DEBUG="true" @@ -981,7 +983,7 @@ else 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 it 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" + 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}SELECT A PROFILE BY THE ID: " read -r selprofile echo -en "\n${Color_Off}" diff --git a/awscli-mfa/status.sh b/awscli-mfa/status.sh index 069a294..ed08640 100755 --- a/awscli-mfa/status.sh +++ b/awscli-mfa/status.sh @@ -21,7 +21,12 @@ # awscli-mfa.sh SCRIPT! MFA_SESSION_LENGTH_IN_SECONDS=32400 -# define the standard location of the AWS credentials and config files +# 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 @@ -206,23 +211,29 @@ getPrintableTimeRemaining() { sessionData() { idxLookup idx profiles_key_id[@] "$AWS_ACCESS_KEY_ID" - if [ "$idx" = "" ]; then - if [[ ${AWS_PROFILE} != "" ]]; then - matched="(not the persistent session)" + 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)" + fi else - matched="(not a persistent session)" + matched="(an in-env session only)" fi else - if [[ ${AWS_PROFILE} != "" ]]; then - matched="(same as the persistent session)" + 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 - [[ ${AWS_PROFILE} == "" ]] && AWS_PROFILE="[unnamed]" + [[ "${AWS_PROFILE}" == "" ]] && AWS_PROFILE="[unnamed]" - echo "AWS_PROFILE IN THE ENVIRONMENT: ${AWS_PROFILE} ${matched}" + echo -e "${Green}AWS PROFILE IN THE ENVIRONMENT: ${BIGreen}"${AWS_PROFILE}" ${Green}\n ${matched}${Color_Off}" if [[ "$AWS_SESSION_INIT_TIME" != "" ]]; then @@ -233,15 +244,98 @@ sessionData() { getRemaining _ret_remaining $AWS_SESSION_INIT_TIME $AWS_SESSION_DURATION getPrintableTimeRemaining _ret ${_ret_remaining} if [ "${_ret}" = "EXPIRED" ]; then - echo " MFA SESSION EXPIRED; YOU SHOULD PURGE THE ENV BY EXECUTING 'source ./source-to-clear-AWS-envvars.sh'" + echo -e " ${Red}THE MFA SESSION EXPIRED; ${BIRed}YOU SHOULD PURGE THE ENV BY EXECUTING 'source ./source-to-clear-AWS-envvars.sh'${Color_Off}" else - echo " MFA SESSION REMAINING: ${_ret}" + 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 string_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))"}"); +} + # -- 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}** 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}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}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}** 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}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}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) @@ -273,7 +367,7 @@ if [[ "$AWS_SESSION_INIT_TIME" != "" ]]; then ((IN_ENV_SESSION_TIME=AWS_SESSION_INIT_TIME+AWS_SESSION_DURATION)) fi -# COLLECT AWS CONFIG DATA FROM ~/.aws/config +# COLLECT AWS CONFIG DATA FROM $CONFFILE # init arrays to hold ident<->mfasec detail declare -a confs_ident @@ -298,10 +392,10 @@ while IFS='' read -r line || [[ -n "$line" ]]; do this_conf_mfasec="" -done < $CONFFILE +done < "$CONFFILE" -# COLLECT AWS_SESSION DATA FROM ~/.aws/credentials +# COLLECT AWS_SESSION DATA FROM $CREDFILE # define profiles arrays declare -a profiles_ident @@ -325,11 +419,11 @@ while IFS='' read -r line || [[ -n "$line" ]]; do if [[ "${profiles_ident[$profiles_iterator]}" != "$_ret" ]]; then ((profiles_iterator++)) - profiles_ident[$profiles_iterator]=$_ret + profiles_ident[$profiles_iterator]="$_ret" fi # transfer possible MFA mfasec from config array - idxLookup idx confs_ident[@] ${_ret} + idxLookup idx confs_ident[@] "${_ret}" if [[ $idx != "" ]]; then profiles_mfa_mfasec[$profiles_iterator]=${confs_mfasec[$idx]} fi @@ -337,9 +431,9 @@ while IFS='' read -r line || [[ -n "$line" ]]; do if [[ "$_ret" != "" ]] && ! [[ "$_ret" =~ -mfasession$ ]]; then - profiles_type[$profiles_iterator]='profile' + profiles_type[$profiles_iterator]="profile" else - profiles_type[$profiles_iterator]='session' + profiles_type[$profiles_iterator]="session" fi fi @@ -352,36 +446,48 @@ while IFS='' read -r line || [[ -n "$line" ]]; do [[ "$line" =~ ^aws_session_init_time[[:space:]]*=[[:space:]]*(.*)$ ]] && profiles_session_init_time[$profiles_iterator]="${BASH_REMATCH[1]}" -echo -echo - -done < $CREDFILE +done < "$CREDFILE" ## PRESENTATION echo -echo "ENVIRONMENT" -echo "-----------" +echo -e "${BIWhite}ENVIRONMENT" +echo -e "===========${Color_Off}" echo if [[ "$AWS_PROFILE" != "" ]]; then if [[ "$AWS_ACCESS_KEY_ID" != "" ]]; then sessionData else - echo "AWS_PROFILE SELECTING A PERSISTENT PROFILE: ${AWS_PROFILE}" + 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}ENVVAR 'AWS_PROFILE' SELECTING A BASE PROFILE (not an MFA session): ${BIGreen}${AWS_PROFILE}${Color_Off}" + elif [[ "${profile_type}" == "session" ]]; then + echo -e "${Green}ENVVAR 'AWS_PROFILE' SELECTING A PERSISTENT MFA SESSION (as below): ${BIGreen}${AWS_PROFILE}${Color_Off}" + else + echo -e "${BIRed}INVALID ENVIRONMENT CONFIGURATION!\nExecute \"source ./source-to-clear-AWS-envvars.sh\" to clear the environment.\n${Color_Off}" + fi fi else if [[ "$AWS_ACCESS_KEY_ID" != "" ]]; then sessionData + else - echo "NO AWS PROFILE PRESENT IN THE ENVIRONMENT" + echo -e "No AWS profile variables present in the environment;\nusing the default base profile." fi fi echo echo -echo "PERSISTENT MFA SESSIONS (in ~/.aws/credentials)" -echo "-----------------------------------------------" +echo -e "${BIWhite}PERSISTENT MFA SESSIONS (in $CREDFILE)" +repeatr "=" 29 ${#CREDFILE} +echo -e "${Color_Off}" echo maxIndex=${#profiles_ident[@]} @@ -394,20 +500,20 @@ do if [[ "${profiles_type[$z]}" == "session" ]]; then - echo "MFA SESSION IDENT: ${profiles_ident[$z]}" + echo -e "${Green}MFA SESSION IDENT: ${BIGreen}${profiles_ident[$z]}${Color_Off}" 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 " MFA SESSION EXPIRED" + echo -e " ${Red}**MFA SESSION EXPIRED**${Color_Off}" else ((live_session_counter++)) - echo " MFA SESSION REMAINING: ${_ret}" + echo -e " ${Green}MFA SESSION REMAINING TO EXPIRATION: ${BIGreen}${_ret}${Color_Off}" fi else - echo " no recorded init time (legacy or external init?)" + echo -e " ${Yellow}No recorded init time (legacy or external init?)${Color_Off}" fi echo fi @@ -416,9 +522,13 @@ do _ret_remaining="" done -echo - -if [[ "$live_session_counter" -gt 0 ]]; then - echo "** Execute awscli-mfa.sh to select an active MFA session." +if [[ $live_session_counter -eq 0 ]]; then + echo "No current active persistent MFA sessions." + echo echo fi + +echo -e "NOTE: Execute 'awscli-mfa.sh' to renew/start a new MFA session,\n or to select (switch to) an existing active MFA session." + +echo +echo From 1232e6b1fbc05b9b794d23338d7dc7a27f33c440 Mon Sep 17 00:00:00 2001 From: Ville Walveranta Date: Wed, 28 Mar 2018 10:46:30 -0500 Subject: [PATCH 30/71] Renamed status.sh to mfastatus.sh --- awscli-mfa/{status.sh => mfastatus.sh} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename awscli-mfa/{status.sh => mfastatus.sh} (100%) diff --git a/awscli-mfa/status.sh b/awscli-mfa/mfastatus.sh similarity index 100% rename from awscli-mfa/status.sh rename to awscli-mfa/mfastatus.sh From ad6c9ea36ffc67e70de75268dad1edc966ad8413 Mon Sep 17 00:00:00 2001 From: Ville Walveranta Date: Mon, 2 Apr 2018 03:21:52 -0500 Subject: [PATCH 31/71] vMFAd register/deregister script 'register-virtual-mfa-device.sh' nearly complete. --- awscli-mfa/register-virtual-mfa-device.sh | 1309 +++++++++++++++++++++ 1 file changed, 1309 insertions(+) create mode 100755 awscli-mfa/register-virtual-mfa-device.sh diff --git a/awscli-mfa/register-virtual-mfa-device.sh b/awscli-mfa/register-virtual-mfa-device.sh new file mode 100755 index 0000000..f312c9a --- /dev/null +++ b/awscli-mfa/register-virtual-mfa-device.sh @@ -0,0 +1,1309 @@ +#!/usr/bin/env bash + +# todo: handle roles with MFA + +DEBUG="false" +# uncomment below to enable the debug output +#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_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 ================================================================== + +# `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=$(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]} + + # 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" +} + +# 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=$(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}THE MFA SESSION SELECTED/CONFIGURED IN THE ENVIRONMENT HAS EXPIRED.${Color_Off}\\n" + else + echo -e "\\n${BIRed}THE AWS PROFILE SELECTED/CONFIGURED IN THE ENVIRONMENT IS INVALID.${Color_Off}\\n" + fi + + read -s -p "$(echo -e "${BIWhite}Do you want to continue with the default profile?${Color_Off} - ${BIWhite}[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 + + 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-to-clear-AWS-envvars.sh\", and try again to proceed.\\n" + exit 1 + fi + fi +} + +print_mfa_notice() { + echo -e "To disable/detach a vMFAd from the profile, you must have\\nan active MFA session established with it. Use the 'awscli-mfa.sh'\\nscript to establish an MFA session for the profile first, then\\nrun this script again.\\n" + echo -e "If you do not have possession of the vMFAd for this profile\\n(in GA/Authy app), please request ops to disable the vMFAd\\nfor your profile, or if you have admin credentials for AWS,\\nuse them outside this script to disable the vMFAd for this\\nprofile." +} + +## 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}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}** 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}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}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}** 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}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}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" +custom_configfiles_reset="false" + +# 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}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 + persistent_MFA="false" + 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) + default_output=$(aws configure get output --profile default) + + if [[ "$default_region" == "" ]]; then + echo + echo -e "${BIWhite}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 + + [[ "$AWS_ACCESS_KEY_ID" != "" ]] && + current_aws_access_key_id="${AWS_ACCESS_KEY_ID}" || + current_aws_access_key_id="$(aws configure get aws_access_key_id)" + + idxLookup idx profiles_key_id[@] "$current_aws_access_key_id" + + if [[ $idx != "" ]]; then + currently_selected_profile_ident="\"${profiles_ident[$idx]}\"" + else + if [[ "${PRECHECK_AWS_PROFILE}" != "" ]]; then + currently_selected_profile_ident="\"${PRECHECK_AWS_PROFILE}\" [transient]" + else + currently_selected_profile_ident="unknown/transient" + fi + fi + + process_user_arn="$(aws sts get-caller-identity --output text --query 'Arn' 2>&1)" + + [[ "$process_user_arn" =~ ([^/]+)$ ]] && + process_username="${BASH_REMATCH[1]}" + + if [[ "$process_username" =~ ExpiredToken ]]; then + continue_maybe "invalid" + + currently_selected_profile_ident="\"default\"" + process_user_arn="$(aws sts get-caller-identity --output text --query 'Arn' 2>&1)" + + [[ "$process_user_arn" =~ ([^/]+)$ ]] && + process_username="${BASH_REMATCH[1]}" + fi + + if [[ "$process_username" =~ error ]]; then + echo -e "${BIRed}The selected profile is not functional${Color_Off}; please check the \"default\" profile\\nin your '$CREDFILE' file, and purge any 'AWS_' environment variables!" + exit 1 + else + echo "Executing this script as the AWS/IAM user \"$process_username\" (profile $currently_selected_profile_ident)." + fi + + 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 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}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 --profile "$profile_ident" configure get region) + profile_output[$cred_profilecounter]=$(aws --profile "$profile_ident" configure get output) + + # get the user ARN; this should be always + # available for valid profiles + 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 + # 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 ]]; then + cred_profile_user[$cred_profilecounter]="" + else + cred_profile_user[$cred_profilecounter]="$profile_username" + fi + + # 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 --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 + # (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)" + + 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 --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 + 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 "MFA ARN: ${mfa_arns[$cred_profilecounter]}" + echo "MFA MAXSEC: ${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="" + 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) + mfa_req="false" + disable_vmfad="false" + enable_vmfad="false" + if [[ ${#cred_profiles[@]} == 1 ]]; then + echo + echo -e "${Green}You have one configured profile: ${BIGreen}${cred_profiles[0]} ${Green}(IAM: ${cred_profile_user[0]})${Color_Off}" + + if [[ "${mfa_arns[0]}" != "" ]]; then + echo -en ".. and its virtual MFA device is already enabled.\\n\\n${BIWhite}Do you want to disable its vMFAd? [Y]es/[N]o${Color_Off} " + + while : + do + read -s -n 1 -r + if [[ $REPLY =~ ^[Yy]$ ]]; then + disable_vmfad="true" + selprofile="-1" + break; + elif [[ $REPLY =~ ^[Nn]$ ]]; then + echo -e "\\n\\nNo action taken. 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}Do you want to attach/enable a vMFAd? [Y]es/[N]o${Color_Off} " + echo + + while : + do + read -s -n 1 -r + if [[ $REPLY =~ ^[Yy]$ ]]; then + enable_vmfad="true" + selprofile="-1" + break; + elif [[ $REPLY =~ ^[Nn]$ ]]; then + echo -e "\\n\\nNo action taken. 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 "${BBlack}${On_White} AWS PROFILES WITH NO ATTACHED/ENABLED VIRTUAL MFA DEVICE (vMFAd): ${Color_Off}" + echo -e " Select a profile to which you want to attach (enable) a vMFAd.\\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 + echo -en "${BIWhite}${ITER}: $i${Color_Off} (IAM: ${cred_profile_user[$SELECTR]})\\n\\n" + + # add to the translation table for the selection + iter_to_profile[$ITER]=$SELECTR + ((ITER++)) + fi + ((SELECTR++)) + done + + echo + echo -e "${BBlack}${On_White} AWS PROFILES WITH ACTIVE (ENABLED) VIRTUAL MFA DEVICE (vMFAd): ${Color_Off}" + echo -e " Select a profile whose vMFAd you want to detach (disable).\\n Once detached, you'll have the option to delete the vMFAd." + echo + SELECTR=0 + for i in "${cred_profiles[@]}" + do + if [[ "${mfa_arns[$SELECTR]}" != "" ]]; then + # vMFAd configured + echo -en "${BIWhite}${ITER}: $i${Color_Off} (IAM: ${cred_profile_user[$SELECTR]})\\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}SELECT A PROFILE BY THE ID: " + read -r selprofile + echo -en "\\n${Color_Off}" + + fi # end profile selection + + # process the selection + if [[ "$selprofile" == "-1" ]]; then + selprofile="1" + else + translated_selprofile=iter_to_profile[$selprofile] + ((selprofile=translated_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 + ((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 + + # a base profile was selected + if [[ $selprofile =~ ^[[:digit:]]+$ ]]; then + echo + final_selection="${cred_profiles[$actual_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 "Could not acquire AWS account ID or current IAM user name. A bad profile? Cannot continue." + echo + exit 1 + fi + + available_user_vmfad=$(aws --profile "${final_selection}" \ + iam list-virtual-mfa-devices \ + --assignment-status Unassigned \ + --output text \ + --query 'VirtualMFADevices[?SerialNumber==`arn:aws:iam::'${aws_account_id}':mfa/'${aws_iam_user}'`].SerialNumber' 2>&1) + + existing_mfa_deleted="false" + if [[ "$available_user_vmfad" =~ error ]]; then + echo "Could not execute list-virtual-mfa-devices. Cannot continue." + echo + exit 1 + elif [[ "$available_user_vmfad" != "" ]]; then + unassigned_vmfad_preexisted="true" + + echo -e "Unassigned vMFAd found for the profile:\\n${BIWhite}$available_user_vmfad${Color_Off}" + echo + echo -en "${BIWhite}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),\\nso make your choice: ${BIWhite}[Y]es/[N]o${Color_Off}" + + while : + do + read -s -n 1 -r + if [[ $REPLY =~ ^[Yy]$ ]]; then + break; + elif [[ $REPLY =~ ^[Nn]$ ]]; then + mfa_deletion_result=$(aws --profile "${final_selection}" \ + iam delete-virtual-mfa-device \ + --serial-number ${available_user_vmfad} 2>&1) + + if [[ "$mfa_deletion_result" =~ error ]]; then + echo + echo "Could not delete inaccessible vMFAd. Cannot continue." + echo + exit 1 + fi + + 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 --profile "${final_selection}" \ + iam create-virtual-mfa-device \ + --virtual-mfa-device-name "${aws_iam_user}" \ + --outfile "${qr_with_path}" \ + --bootstrap-method QRCodePNG 2>&1) + + if [[ "$vmfad_creation_status" =~ error ]]; then + echo -e "Could not execute create-virtual-mfa-device.\\nNo virtual MFA device to enable. Cannot continue." + echo + exit 1 + fi + + echo -e "A new vMFAd has been created. ${BIWhite}Please scan\\nthe QRCode with Authy to add the vMFAd on\\nyour portable device.${Color_Off}" + echo -e "\\nNOTE: The QRCode file, \"${qr_file_name}\",\\nis $qr_file_target!" + echo + echo "Press 'x' once you have scanned the QRCode to proceed." + while : + do + read -s -n 1 -r + if [[ $REPLY =~ ^[Xx]$ ]]; then + break; + fi + done + + echo + echo -en "NOTE: Anyone who gains possession of the\\n QRCode file can initialize the vMFDd like you\\n just did, so optimally it should not be kept around.\\n\\nDo you want to delete the QRCode securely? [Y]es/[N]o " + + while : + do + read -s -n 1 -r + if [[ $REPLY =~ ^[Yy]$ ]]; then + rm -fP "${qr_with_path}" + echo + echo "QRCode file deleted securely." + break; + elif [[ $REPLY =~ ^[Nn]$ ]]; then + echo + echo -e "You chose not to delete the vMFAd initializer QRCode;\\nplease store it securely as if it were a password!" + break; + fi + done + echo + + available_user_vmfad=$(aws --profile "${final_selection}" \ + iam list-virtual-mfa-devices \ + --assignment-status Unassigned \ + --output text \ + --query 'VirtualMFADevices[?SerialNumber==`arn:aws:iam::'${aws_account_id}':mfa/'${aws_iam_user}'`].SerialNumber' 2>&1) + + if [[ "$available_user_vmfad" =~ error ]]; then + echo "Could not execute list-virtual-mfa-devices. Cannot continue." + exit 1 + fi + + fi + + if [[ "$available_user_vmfad" == "" ]]; then + # no vMFAd existed, none could be created + echo "No virtual MFA device to enable. Cannot continue." + exit 1 + else + [[ "$unassigned_vmfad_preexisted" == "true" ]] && vmfad_source="existing" || vmfad_source="newly created" + echo "Enabling the $vmfad_source virtual MFA device: $available_user_vmfad" + fi + + echo + echo -e "${BIWhite}Please enter two consequtively 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 "Bad authcodes. Please enter two consequtively generated six-digit numbers separated by a space." + fi + done + + echo + + vmfad_enablement_status=$(aws --profile "${final_selection}" \ + iam enable-mfa-device \ + --user-name "${aws_iam_user}" \ + --serial-number ${available_user_vmfad} \ + --authentication-code-1 ${authcode1} \ + --authentication-code-2 ${authcode2} 2>&1) + + if [[ "$vmfad_enablement_status" =~ error ]]; then + echo "Could not enable vMFAd. Cannot continue." + exit 1 + else + echo "vMFAd enabled for the profile \"${final_selection}\" (IAM user name \"$aws_iam_user\")." + fi + + # find available vMFAds for the profile (IAM user); there should be 0 or 1 + # + # none found: create -> scan QRC -> Q: confirm del QRC -> enable + # + # one found: Q: has access? + # + # no: confirm delete/recreate -> delete vMFAd -> create new -> enable + # yes: enable + # + # enable vMFAd: QR is created -> scan -> Q: delete securely? (yes->delete) -> enter two consequtive codes (check for 2 x six digits, separated by one or more spaces before executing the aws command) + + else + echo -e "disable the vMFAd for the profile...\\n" + + transient_mfa_profile_check="$(aws sts get-caller-identity --output text --query 'Arn' 2>&1)" + + 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 + echo -e "Could not acquire AWS account ID or current IAM user name. A bad profile? Cannot continue.\\n" + echo + exit 1 + fi + + # First checking the envvars + if [[ "$PRECHECK_AWS_PROFILE" =~ ^${final_selection}-mfasession$ ]] && + [[ "$PRECHECK_AWS_SESSION_TOKEN" != "" ]] && + [[ "$PRECHECK_AWS_SESSION_INIT_TIME" != "" ]] && + [[ "$PRECHECK_AWS_SESSION_DURATION" != "" ]]; then + # this is a MFA profile in the environment + + getRemaining _ret $PRECHECK_AWS_SESSION_INIT_TIME $PRECHECK_AWS_SESSION_DURATION + + elif [[ "$PRECHECK_AWS_PROFILE" =~ ^${final_selection}-mfasession$ ]] && + [[ "$profiles_idx" != "" ]]; then + # this is a 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 $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 a transient profile, check for which base profile + + idxLookup persistent_equivalent_idx cred_profile_user[@] "$aws_iam_user" + if [[ "${persistent_equivalent_idx}" != "" ]]; then + # IAM user of the transient in-env MFA session matches + # the IAM user of the selected persistent base profile + + getRemaining _ret $PRECHECK_AWS_SESSION_INIT_TIME $PRECHECK_AWS_SESSION_DURATION + + else + echo -e "This is an unknown in-env MFA session. Cannot continue.\\n" + print_mfa_notice + exit 1 + fi + + else # no valid MFA profile (in-env or functional reference) found + echo -e "No active MFA session found for the profile \"${final_selection}\".\\n" + print_mfa_notice + echo + exit 1 + fi + + if [[ ${_ret} -gt 120 ]]; then # at least 120 seconds of the session remain + + # below profile is not defined because an active MFA must be used + + 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) + + if [[ "$deactivation_result" =~ error ]]; then + echo "Could not deactivate vMFAd for profile ${final_selection}. Cannot continue." + exit 1 + else + echo + echo "vMFAd disable for the profile ${final_selection}." + echo + fi + + else + echo -e "The MFA session for the profile \"${final_selection}\" has expired.\\n" + print_mfa_notice + echo + exit 1 + fi + + exit 1 + + # check for the active MFA session with this vMFAd/profile + # + # no: Q: possession of the vMFAd? + # + # no: instructions ("must have active session to disable, contact ops if not able") & exit + # + # aws iam deactivate-mfa-device --user-name mfa-test-user --serial-number arn:aws:iam::248783370565:mfa/mfa-test-user + # + # yes: init an MFA session (use the vars locally) -> after that like 'yes' below + # + # yes: Q: confirm disable -> disable -> Q: delete vMFAd? + # + # no: exit & print notice of autodelete in X days + # yes: delete vMFAd and exit with info msg + + fi + + else + # non-acceptable characters were present in the selection + echo -e "${BIRed}There is no profile '${selprofile}'.${Color_Off}" + echo + exit 1 + fi + + else + # no numeric part in selection + echo -e "${BIRed}There is no profile '${selprofile}'.${Color_Off}" + echo + exit 1 + fi + else + # empty selection + echo -e "${BIRed}There is no profile '${selprofile}'.${Color_Off}" + echo + exit 1 + fi + +fi From 953ba991fb3412ffcb8fb5be930f6679a4fc0aa1 Mon Sep 17 00:00:00 2001 From: Ville Walveranta Date: Mon, 2 Apr 2018 14:01:03 -0500 Subject: [PATCH 32/71] Added vMFAd delete-after-disable functionality. --- awscli-mfa/awscli-mfa.sh | 8 ++-- ...evice.sh => enable-disable-vmfa-device.sh} | 37 ++++++++++++++++--- 2 files changed, 36 insertions(+), 9 deletions(-) rename awscli-mfa/{register-virtual-mfa-device.sh => enable-disable-vmfa-device.sh} (97%) diff --git a/awscli-mfa/awscli-mfa.sh b/awscli-mfa/awscli-mfa.sh index 85c2e0f..fb84456 100755 --- a/awscli-mfa/awscli-mfa.sh +++ b/awscli-mfa/awscli-mfa.sh @@ -364,9 +364,9 @@ getRemaining() { duration=$MFA_SESSION_LENGTH_IN_SECONDS if [ ! -z "${timestamp##*[!0-9]*}" ]; then - ((session_end=${timestamp}+${duration})) + ((session_end=timestamp+duration)) if [[ $session_end -gt $this_time ]]; then - ((remaining=${session_end}-${this_time})) + ((remaining=session_end-this_time)) else remaining=0 fi @@ -907,7 +907,7 @@ else echo -e ".. but no active persistent MFA sessions exist" fi else - echo -e "${BIRed}.. but it doesn't have a virtual MFA device attached/enabled;\n cannot continue${Color_Off} (use register-virtual-mfa-device.sh script first to enable a vMFAd)!" + echo -e "${BIRed}.. 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 @@ -1096,7 +1096,7 @@ else # reset entered MFA code (just to be safe) mfacode="" echo - echo -e "vMFAd has not been set up for this profile (run 'register-virtual-mfa-device.sh' script to configure the vMFAd)." + echo -e "vMFAd has not been set up for this profile (run 'enable-disable-vmfa-device.sh' script to configure the vMFAd)." fi if [[ "$mfacode" != "" ]]; then diff --git a/awscli-mfa/register-virtual-mfa-device.sh b/awscli-mfa/enable-disable-vmfa-device.sh similarity index 97% rename from awscli-mfa/register-virtual-mfa-device.sh rename to awscli-mfa/enable-disable-vmfa-device.sh index f312c9a..7a6ab16 100755 --- a/awscli-mfa/register-virtual-mfa-device.sh +++ b/awscli-mfa/enable-disable-vmfa-device.sh @@ -1247,19 +1247,46 @@ else # below profile is not defined because an active MFA must be used - deactivation_result=$(aws iam deactivate-mfa-device \ + 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) - if [[ "$deactivation_result" =~ error ]]; then - echo "Could not deactivate vMFAd for profile ${final_selection}. Cannot continue." + if [[ "$vmfad_deactivation_result" =~ error ]]; then + echo "Could not disable/detach vMFAd for profile ${final_selection}. Cannot continue." exit 1 else echo - echo "vMFAd disable for the profile ${final_selection}." + echo "vMFAd disabled/detached for the profile ${final_selection}." echo + + echo -en "${BIWhite}Do you want to DELETE the disabled/detached vMFAd? [Y]es/[N]o${Color_Off} " + echo + while : + do + read -s -n 1 -r + if [[ $REPLY =~ ^[Yy]$ ]]; then + vmfad_delete_result=$(aws --profile ${final_selection} \ + iam delete-virtual-mfa-device \ + --serial-number arn:aws:iam::${aws_account_id}:mfa/${aws_iam_user}) + + if [[ "$vmfad_deactivation_result" =~ error ]]; then + echo "Could not deleted vMFAd for profile ${final_selection}. Cannot continue." + exit 1 + else + echo "vMFAd deleted for the profile ${final_selection}." + echo + echo "To set up a new vMFAd, run this script again." + echo + fi + + break; + elif [[ $REPLY =~ ^[Nn]$ ]]; then + echo -e "\\n\\nNo action taken. Exiting.\\n\\nNOTE: Detached vMFAd's may be automatically deleted after some time.\\n" + exit 1 + break; + fi + done fi - else echo -e "The MFA session for the profile \"${final_selection}\" has expired.\\n" print_mfa_notice From 86e37bc6222d13ffe9ca3912407ec482cfcf0dc3 Mon Sep 17 00:00:00 2001 From: Ville Walveranta Date: Mon, 2 Apr 2018 23:12:10 -0500 Subject: [PATCH 33/71] Debugging, code cleanup. --- awscli-mfa/awscli-mfa.sh | 83 +++++------ awscli-mfa/enable-disable-vmfa-device.sh | 168 +++++++++-------------- awscli-mfa/mfastatus.sh | 18 +-- 3 files changed, 119 insertions(+), 150 deletions(-) diff --git a/awscli-mfa/awscli-mfa.sh b/awscli-mfa/awscli-mfa.sh index fb84456..c619dc0 100755 --- a/awscli-mfa/awscli-mfa.sh +++ b/awscli-mfa/awscli-mfa.sh @@ -409,12 +409,12 @@ continue_maybe() { if [[ "$already_failed" == "false" ]]; then if [[ "${failtype}" == "expired" ]]; then - echo -e "\n${BIRed}THE MFA SESSION SELECTED/CONFIGURED IN THE ENVIRONMENT HAS EXPIRED.${Color_Off}\n" + echo -e "\\n${BIRed}THE MFA SESSION SELECTED/CONFIGURED IN THE ENVIRONMENT HAS EXPIRED.${Color_Off}\\n" else - echo -e "\n${BIRed}THE AWS PROFILE SELECTED/CONFIGURED IN THE ENVIRONMENT IS INVALID.${Color_Off}\n" + echo -e "\\n${BIRed}THE AWS PROFILE SELECTED/CONFIGURED IN THE ENVIRONMENT IS INVALID.${Color_Off}\\n" fi - read -s -p "$(echo -e "${BIWhite}Do you want to continue with the default profile?${Color_Off} - ${BIWhite}[Y]${Color_Off}n ")" -n 1 -r + read -s -p "$(echo -e "${BIWhite}Do you want to continue with the default profile?${Color_Off} - ${BIWhite}[Y]${Color_Off}/N ")" -n 1 -r if [[ $REPLY =~ ^[Yy]$ ]] || [[ $REPLY == "" ]]; then @@ -447,7 +447,7 @@ continue_maybe() { export AWS_PROFILE=default echo else - echo -e "\n\nExecute \"source ./source-to-clear-AWS-envvars.sh\", and try again to proceed.\n" + echo -e "\\n\\nExecute \"source ./source-to-clear-AWS-envvars.sh\", and try again to proceed.\\n" exit 1 fi fi @@ -457,9 +457,9 @@ continue_maybe() { # 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" + 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 @@ -471,7 +471,7 @@ if [[ "$AWS_CONFIG_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." + 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 @@ -487,14 +487,14 @@ elif [[ "$AWS_CONFIG_FILE" != "" ]] && [ ! -f "$AWS_CONFIG_FILE" ]; then echo - echo -e "${BIRed}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." + echo -e "${BIRed}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}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." + echo -e "${BIRed}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 @@ -510,14 +510,14 @@ elif [[ "$AWS_SHARED_CREDENTIALS_FILE" != "" ]] && [ ! -f "$AWS_SHARED_CREDENTIALS_FILE" ]; then echo - echo -e "${BIRed}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." + echo -e "${BIRed}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}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." + echo -e "${BIRed}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 @@ -543,7 +543,7 @@ done < "$CREDFILE" if [[ "$ONEPROFILE" == "false" ]]; then echo - echo -e "${BIRed}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 -e "${BIRed}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 @@ -678,7 +678,7 @@ else if [[ "$default_region" == "" ]]; then echo - echo -e "${BIWhite}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 -e "${BIWhite}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 @@ -696,10 +696,10 @@ else idxLookup idx profiles_key_id[@] "$current_aws_access_key_id" if [[ $idx != "" ]]; then - currently_selected_profile_ident="\"${profiles_ident[$idx]}\"" + currently_selected_profile_ident="'${profiles_ident[$idx]}'" else if [[ "${PRECHECK_AWS_PROFILE}" != "" ]]; then - currently_selected_profile_ident="\"${PRECHECK_AWS_PROFILE}\" [transient]" + currently_selected_profile_ident="'${PRECHECK_AWS_PROFILE}' [transient]" else currently_selected_profile_ident="unknown/transient" fi @@ -713,7 +713,7 @@ else if [[ "$process_username" =~ ExpiredToken ]]; then continue_maybe "invalid" - currently_selected_profile_ident="\"default\"" + currently_selected_profile_ident="'default'" process_user_arn="$(aws sts get-caller-identity --output text --query 'Arn' 2>&1)" [[ "$process_user_arn" =~ ([^/]+)$ ]] && @@ -721,10 +721,10 @@ else fi if [[ "$process_username" =~ error ]]; then - echo -e "${BIRed}The selected profile is not functional${Color_Off}; please check the \"default\" profile\nin your '$CREDFILE' file, and purge any 'AWS_' environment variables!" + echo -e "${BIRed}The selected profile is not functional${Color_Off}; please check the 'default' profile\\nin your '${CREDFILE}' file, and purge any 'AWS_' environment variables by executing\\n${Green}source ./source-to-clear-AWS-envvars.sh${Color_Off}" exit 1 else - echo "Executing this script as the AWS/IAM user \"$process_username\" (profile $currently_selected_profile_ident)." + echo "Executing this script as the AWS/IAM user '$process_username' (profile $currently_selected_profile_ident)." fi echo @@ -891,7 +891,8 @@ else mfa_req="false" if [[ ${#cred_profiles[@]} == 1 ]]; then echo - echo -e "${Green}You have one configured profile: ${BIGreen}${cred_profiles[0]} ${Green}(IAM: ${cred_profile_user[0]})${Color_Off}" + [[ "${cred_profile_user[0]}" != "" ]] && prcpu="${cred_profile_user[0]}" || prcpu="unknown -- a bad profile?" + echo -e "${Green}You have one configured profile: ${BIGreen}${cred_profiles[0]} ${Green}(IAM: ${prcpu})${Color_Off}" mfa_session_status="false" if [[ "${mfa_arns[0]}" != "" ]]; then @@ -904,10 +905,10 @@ else mfa_session_status="true" else - echo -e ".. but no active persistent MFA sessions exist" + echo -e ".. but no active persistent MFA sessions exist" fi else - echo -e "${BIRed}.. 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 -e "${BIRed}.. 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 @@ -963,8 +964,8 @@ else else mfa_notify="; vMFAd not configured" fi - - echo -en "${BIWhite}${ITER}: $i${Color_Off} (IAM: ${cred_profile_user[$SELECTR]}${mfa_notify})\n" + [[ "${cred_profile_user[$SELECTR]}" != "" ]] && prcpu="${cred_profile_user[$SELECTR]}" || prcpu="unknown -- a bad profile?" + echo -en "${BIWhite}${ITER}: $i${Color_Off} (IAM: ${prcpu}${mfa_notify})\\n" if [[ "${mfa_profile_status[$SELECTR]}" != "EXPIRED" && "${mfa_profile_status[$SELECTR]}" != "" ]]; then @@ -983,10 +984,10 @@ else 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}SELECT A PROFILE BY THE ID: " + 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}SELECT A PROFILE BY THE ID: " read -r selprofile - echo -en "\n${Color_Off}" + echo -en "\\n${Color_Off}" fi # end profile selection @@ -1096,7 +1097,7 @@ else # reset entered MFA code (just to be safe) mfacode="" echo - echo -e "vMFAd has not been set up for this profile (run 'enable-disable-vmfa-device.sh' script to configure the vMFAd)." + echo -e "A vMFAd has not been set up for this profile (run 'enable-disable-vmfa-device.sh' script to configure the vMFAd)." fi if [[ "$mfacode" != "" ]]; then @@ -1146,8 +1147,8 @@ else # for the MFA profile getPrintableTimeRemaining _ret $AWS_SESSION_DURATION validity_period=${_ret} - echo -e "${BIWhite}Make this MFA session persistent?${Color_Off} (Saves the session in $CREDFILE\nso that you can return to it during its validity period, ${validity_period}.)" - read -s -p "$(echo -e "${BIWhite}Yes (default) - make peristent${Color_Off}; No - only the envvars will be used ${BIWhite}[Y]${Color_Off}n ")" -n 1 -r + echo -e "${BIWhite}Make this MFA session persistent?${Color_Off} (Saves the session in $CREDFILE\\nso that you can return to it during its validity period, ${validity_period}.)" + read -s -p "$(echo -e "${BIWhite}Yes (default) - make peristent${Color_Off}; No - only the envvars will be used ${BIWhite}[Y]${Color_Off}/N ")" -n 1 -r echo if [[ $REPLY =~ ^[Yy]$ ]] || [[ $REPLY == "" ]]; then @@ -1196,11 +1197,11 @@ else if [[ "${profile_region[$actual_selprofile]}" != "" && "${mfaprofile}" == "true" ]]; then set_new_region=${profile_region[$actual_selprofile]} - echo -e "\nNOTE: Region had not been configured for the selected MFA profile;\n it has been set to same as the parent profile ('$set_new_region')." + echo -e "\\nNOTE: 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 set_new_region=${default_region} - echo -e "\nNOTE: Region had not been configured for the selected profile;\n it has been set to the default region ('${default_region}')." + echo -e "\\nNOTE: Region had not been configured for the selected profile;\\n it has been set to the default region ('${default_region}')." fi AWS_DEFAULT_REGION="${set_new_region}" @@ -1216,11 +1217,11 @@ else if [[ "${profile_output[$actual_selprofile]}" != "" && "${mfaprofile}" == "true" ]]; then set_new_output=${profile_output[$actual_selprofile]} - echo -e "NOTE: 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')." + echo -e "NOTE: 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 "Output format had not been configured for the selected profile;\n it has been set to the default output format ('${default_output}')." + echo -e "Output format had not been configured for the selected profile;\\n it has been set to the default output format ('${default_output}')." fi AWS_DEFAULT_OUTPUT="${set_new_output}" @@ -1264,7 +1265,7 @@ else 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}Do you want to export the selected profile's secrets to the environment${Color_Off} (for s3cmd, etc)? - y${BIWhite}[N]${Color_Off} ")" -n 1 -r + read -s -p "$(echo -e "${BIWhite}Do you want to export the selected profile's secrets to the environment${Color_Off} (for s3cmd, etc)? - Y/${BIWhite}[N]${Color_Off} ")" -n 1 -r if [[ $REPLY =~ ^[Nn]$ ]] || [[ $REPLY == "" ]]; then @@ -1281,14 +1282,14 @@ else fi if [[ "$mfacode" != "" ]] && [[ "$persistent_MFA" == "false" ]]; then - echo -e "${BIWhite}*** 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 -e "${BIWhite}*** 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}*** 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 -e "${BIGreen}*** 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, @@ -1383,13 +1384,13 @@ else fi fi echo - echo -e "${Green}*** Make sure to export/unset all the new values as instructed above to\n make sure no conflicting profile/secrets remain in the envrionment!" + echo -e "${Green}*** Make sure to export/unset all the new values as instructed above to\\n make sure no conflicting profile/secrets remain in the envrionment!" echo - echo -e "*** 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}aws sts get-caller-identity --profile default${Color_Off}" + echo -e "*** 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}aws sts get-caller-identity --profile default${Color_Off}" echo - echo -e "${Green}*** 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}source ./source-to-clear-AWS-envvars.sh" + echo -e "${Green}*** 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}source ./source-to-clear-AWS-envvars.sh" echo - echo -e "${BIWhite}PASTE THE PROFILE ACTIVATION COMMAND FROM THE CLIPBOARD\nON THE COMMAND LINE NOW, AND PRESS ENTER! THEN YOU'RE DONE!${Color_Off}" + echo -e "${BIWhite}PASTE THE PROFILE ACTIVATION COMMAND FROM THE CLIPBOARD\\nON THE COMMAND LINE NOW, AND PRESS ENTER! THEN YOU'RE DONE!${Color_Off}" echo else # not macOS, not Linux, so some other weird OS like Windows.. diff --git a/awscli-mfa/enable-disable-vmfa-device.sh b/awscli-mfa/enable-disable-vmfa-device.sh index 7a6ab16..1cc8710 100755 --- a/awscli-mfa/enable-disable-vmfa-device.sh +++ b/awscli-mfa/enable-disable-vmfa-device.sh @@ -120,7 +120,8 @@ exists() { checkEnvSession() { # $1 is the check type - local this_time=$(date +%s) + local this_time + this_time=$(date +%s) # COLLECT AWS_SESSION DATA FROM THE ENVIRONMENT PRECHECK_AWS_PROFILE=$(env | grep AWS_PROFILE) @@ -193,7 +194,7 @@ checkEnvSession() { # 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 + getRemaining _ret "$PRECHECK_AWS_SESSION_INIT_TIME" "$PRECHECK_AWS_SESSION_DURATION" [[ "${_ret}" -eq 0 ]] && continue_maybe "expired" elif [[ "$PRECHECK_AWS_PROFILE" =~ -mfasession$ ]] && @@ -210,7 +211,7 @@ checkEnvSession() { # the parent/base profile's duration if [[ "$profile_time" != "" ]]; then getDuration parent_duration "$PRECHECK_AWS_PROFILE" - getRemaining _ret $profile_time $parent_duration + getRemaining _ret "$profile_time" "$parent_duration" [[ "${_ret}" -eq 0 ]] && continue_maybe "expired" fi fi @@ -327,7 +328,8 @@ getRemaining() { local timestamp=$2 local duration=$3 - local this_time=$(date +%s) + local this_time + this_time=$(date +%s) local remaining=0 [[ "${duration}" == "" ]] && @@ -384,7 +386,7 @@ continue_maybe() { echo -e "\\n${BIRed}THE AWS PROFILE SELECTED/CONFIGURED IN THE ENVIRONMENT IS INVALID.${Color_Off}\\n" fi - read -s -p "$(echo -e "${BIWhite}Do you want to continue with the default profile?${Color_Off} - ${BIWhite}[Y]${Color_Off}n ")" -n 1 -r + read -s -p "$(echo -e "${BIWhite}Do you want to continue with the default profile?${Color_Off} - ${BIWhite}[Y]${Color_Off}/N ")" -n 1 -r if [[ $REPLY =~ ^[Yy]$ ]] || [[ $REPLY == "" ]]; then @@ -503,7 +505,6 @@ fi CONFFILE="$active_config_file" CREDFILE="$active_credentials_file" -custom_configfiles_reset="false" # read the credentials file and make sure that at least one profile is configured ONEPROFILE="false" @@ -562,7 +563,6 @@ else declare -a profiles_secret_key declare -a profiles_session_token declare -a profiles_session_init_time - persistent_MFA="false" profiles_iterator=0 profiles_init=0 @@ -670,10 +670,10 @@ else idxLookup idx profiles_key_id[@] "$current_aws_access_key_id" if [[ $idx != "" ]]; then - currently_selected_profile_ident="\"${profiles_ident[$idx]}\"" + currently_selected_profile_ident="'${profiles_ident[$idx]}'" else if [[ "${PRECHECK_AWS_PROFILE}" != "" ]]; then - currently_selected_profile_ident="\"${PRECHECK_AWS_PROFILE}\" [transient]" + currently_selected_profile_ident="'${PRECHECK_AWS_PROFILE}' [transient]" else currently_selected_profile_ident="unknown/transient" fi @@ -695,10 +695,10 @@ else fi if [[ "$process_username" =~ error ]]; then - echo -e "${BIRed}The selected profile is not functional${Color_Off}; please check the \"default\" profile\\nin your '$CREDFILE' file, and purge any 'AWS_' environment variables!" + echo -e "${BIRed}The selected profile is not functional${Color_Off}; please check the 'default' profile\\nin your '${CREDFILE}' file, and purge any 'AWS_' environment variables by executing\\n${Green}source ./source-to-clear-AWS-envvars.sh${Color_Off}" exit 1 else - echo "Executing this script as the AWS/IAM user \"$process_username\" (profile $currently_selected_profile_ident)." + echo "Executing this script as the AWS/IAM user '${process_username}' (profile ${currently_selected_profile_ident})." fi echo @@ -806,7 +806,7 @@ else getInitTime _ret_timestamp "$mfa_profile_ident" getDuration _ret_duration "$mfa_profile_ident" - getRemaining _ret_remaining ${_ret_timestamp} ${_ret_duration} + getRemaining _ret_remaining "${_ret_timestamp}" "${_ret_duration}" if [[ ${_ret_remaining} -eq 0 ]]; then # session has expired @@ -867,25 +867,22 @@ else echo -e "${Color_Off}" # select the profile (first, single profile + a possible persistent MFA session) - mfa_req="false" - disable_vmfad="false" - enable_vmfad="false" if [[ ${#cred_profiles[@]} == 1 ]]; then echo - echo -e "${Green}You have one configured profile: ${BIGreen}${cred_profiles[0]} ${Green}(IAM: ${cred_profile_user[0]})${Color_Off}" + [[ "${cred_profile_user[0]}" != "" ]] && prcpu="${cred_profile_user[0]}" || prcpu="unknown -- a bad profile?" + echo -e "${Green}You have one configured profile: ${BIGreen}${cred_profiles[0]} ${Green}(IAM: ${prcpu})${Color_Off}" if [[ "${mfa_arns[0]}" != "" ]]; then - echo -en ".. and its virtual MFA device is already enabled.\\n\\n${BIWhite}Do you want to disable its vMFAd? [Y]es/[N]o${Color_Off} " + echo -en ".. and its virtual MFA device is already enabled.\\n\\n${BIWhite}Do you want to disable its vMFAd? Y/N${Color_Off} " while : do read -s -n 1 -r if [[ $REPLY =~ ^[Yy]$ ]]; then - disable_vmfad="true" selprofile="-1" break; elif [[ $REPLY =~ ^[Nn]$ ]]; then - echo -e "\\n\\nNo action taken. Exiting.\\n" + echo -e "\\n\\nA vMFAd not disabled. Exiting.\\n" exit 1 break; fi @@ -893,18 +890,15 @@ else echo else - echo -en ".. but it doesn't have a virtual MFA device attached/enabled.\\n\\n${BIWhite}Do you want to attach/enable a vMFAd? [Y]es/[N]o${Color_Off} " - echo - + echo -en ".. but it doesn't have a virtual MFA device attached/enabled.\\n\\n${BIWhite}Do you want to attach/enable a vMFAd? Y/N${Color_Off} " while : do read -s -n 1 -r if [[ $REPLY =~ ^[Yy]$ ]]; then - enable_vmfad="true" selprofile="-1" break; elif [[ $REPLY =~ ^[Nn]$ ]]; then - echo -e "\\n\\nNo action taken. Exiting.\\n" + echo -e "\\n\\nvA MFAd not attached/enabled. Exiting.\\n" exit 1 break; fi @@ -920,7 +914,7 @@ else # create the profile selections for "no vMFAd configured" and "vMFAd enabled" echo echo -e "${BBlack}${On_White} AWS PROFILES WITH NO ATTACHED/ENABLED VIRTUAL MFA DEVICE (vMFAd): ${Color_Off}" - echo -e " Select a profile to which you want to attach (enable) a vMFAd.\\n A new vMFAd is created/initialized if one doesn't exist." + echo -e " ${BIWhite}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 @@ -928,7 +922,8 @@ else do if [[ "${mfa_arns[$SELECTR]}" == "" ]]; then # no vMFAd configured - echo -en "${BIWhite}${ITER}: $i${Color_Off} (IAM: ${cred_profile_user[$SELECTR]})\\n\\n" + [[ "${cred_profile_user[$SELECTR]}" != "" ]] && prcpu="${cred_profile_user[$SELECTR]}" || prcpu="unknown -- a bad profile?" + echo -en "${BIWhite}${ITER}: $i${Color_Off} (IAM: ${prcpu})\\n\\n" # add to the translation table for the selection iter_to_profile[$ITER]=$SELECTR @@ -939,15 +934,15 @@ else echo echo -e "${BBlack}${On_White} AWS PROFILES WITH ACTIVE (ENABLED) VIRTUAL MFA DEVICE (vMFAd): ${Color_Off}" - echo -e " Select a profile whose vMFAd you want to detach (disable).\\n Once detached, you'll have the option to delete the vMFAd." + echo -e " ${BIWhite}Select a profile whose vMFAd you want to detach/disable.${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 - echo -en "${BIWhite}${ITER}: $i${Color_Off} (IAM: ${cred_profile_user[$SELECTR]})\\n\\n" - + [[ "${cred_profile_user[$SELECTR]}" != "" ]] && prcpu="${cred_profile_user[$SELECTR]}" || prcpu="unknown -- a bad profile?" + echo -en "${BIWhite}${ITER}: $i${Color_Off} (IAM: ${prcpu})\\n\\n" # add to the translation table for the selection iter_to_profile[$ITER]=$SELECTR ((ITER++)) @@ -966,7 +961,7 @@ else if [[ "$selprofile" == "-1" ]]; then selprofile="1" else - translated_selprofile=iter_to_profile[$selprofile] + translated_selprofile=${iter_to_profile[$selprofile]} ((selprofile=translated_selprofile+1)) fi @@ -982,7 +977,7 @@ else 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 -e "${BIRed}There is no profile '${selprofile}'.${Color_Off}" echo exit 1 fi @@ -1004,7 +999,7 @@ else aws_account_id="${BASH_REMATCH[1]}" aws_iam_user="${BASH_REMATCH[2]}" else - echo "Could not acquire AWS account ID or current IAM user name. A bad profile? Cannot continue." + echo -e "${BIRed}Could not acquire AWS account ID or current IAM user name. A bad profile? Cannot continue.${Color_Off}" echo exit 1 fi @@ -1013,19 +1008,19 @@ else iam list-virtual-mfa-devices \ --assignment-status Unassigned \ --output text \ - --query 'VirtualMFADevices[?SerialNumber==`arn:aws:iam::'${aws_account_id}':mfa/'${aws_iam_user}'`].SerialNumber' 2>&1) + --query 'VirtualMFADevices[?SerialNumber==`arn:aws:iam::'"${aws_account_id}"':mfa/'"${aws_iam_user}"'`].SerialNumber' 2>&1) existing_mfa_deleted="false" if [[ "$available_user_vmfad" =~ error ]]; then - echo "Could not execute list-virtual-mfa-devices. Cannot continue." + echo -e "${BIRed}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 "Unassigned vMFAd found for the profile:\\n${BIWhite}$available_user_vmfad${Color_Off}" + echo -e "${Green}Unassigned vMFAd found for the profile:\\n${BIGreen}$available_user_vmfad${Color_Off}" echo - echo -en "${BIWhite}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),\\nso make your choice: ${BIWhite}[Y]es/[N]o${Color_Off}" + echo -en "${BIWhite}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}Y/N${Color_Off} " while : do @@ -1035,11 +1030,11 @@ else elif [[ $REPLY =~ ^[Nn]$ ]]; then mfa_deletion_result=$(aws --profile "${final_selection}" \ iam delete-virtual-mfa-device \ - --serial-number ${available_user_vmfad} 2>&1) + --serial-number "${available_user_vmfad}" 2>&1) if [[ "$mfa_deletion_result" =~ error ]]; then echo - echo "Could not delete inaccessible vMFAd. Cannot continue." + echo -e "${BIRed}Could not delete inaccessible vMFAd. Cannot continue.${Color_Off}" echo exit 1 fi @@ -1079,15 +1074,15 @@ else --bootstrap-method QRCodePNG 2>&1) if [[ "$vmfad_creation_status" =~ error ]]; then - echo -e "Could not execute create-virtual-mfa-device.\\nNo virtual MFA device to enable. Cannot continue." + echo -e "${BIRed}Could not execute create-virtual-mfa-device.\\nNo virtual MFA device to enable. Cannot continue.${Color_Off}" echo exit 1 fi - echo -e "A new vMFAd has been created. ${BIWhite}Please scan\\nthe QRCode with Authy to add the vMFAd on\\nyour portable device.${Color_Off}" + echo -e "${BIGreen}A new vMFAd has been created. ${BIWhite}Please scan\\nthe QRCode with Authy to add the vMFAd on\\nyour portable device.${Color_Off}" echo -e "\\nNOTE: The QRCode file, \"${qr_file_name}\",\\nis $qr_file_target!" echo - echo "Press 'x' once you have scanned the QRCode to proceed." + echo -e "${BIWhite}Press 'x' once you have scanned the QRCode to proceed.${Color_Off}" while : do read -s -n 1 -r @@ -1097,7 +1092,7 @@ else done echo - echo -en "NOTE: Anyone who gains possession of the\\n QRCode file can initialize the vMFDd like you\\n just did, so optimally it should not be kept around.\\n\\nDo you want to delete the QRCode securely? [Y]es/[N]o " + 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}Do you want to delete the QRCode securely? Y/N${Color_Off} " while : do @@ -1105,11 +1100,11 @@ else if [[ $REPLY =~ ^[Yy]$ ]]; then rm -fP "${qr_with_path}" echo - echo "QRCode file deleted securely." + echo -e "${BIWhite}QRCode file deleted securely.${Color_Off}" break; elif [[ $REPLY =~ ^[Nn]$ ]]; then echo - echo -e "You chose not to delete the vMFAd initializer QRCode;\\nplease store it securely as if it were a password!" + echo -e "${BIWhite}You chose not to delete the vMFAd initializer QRCode;\\nplease store it securely as if it were a password!${Color_Off}" break; fi done @@ -1119,10 +1114,10 @@ else iam list-virtual-mfa-devices \ --assignment-status Unassigned \ --output text \ - --query 'VirtualMFADevices[?SerialNumber==`arn:aws:iam::'${aws_account_id}':mfa/'${aws_iam_user}'`].SerialNumber' 2>&1) + --query 'VirtualMFADevices[?SerialNumber==`arn:aws:iam::'"${aws_account_id}"':mfa/'"${aws_iam_user}"'`].SerialNumber' 2>&1) if [[ "$available_user_vmfad" =~ error ]]; then - echo "Could not execute list-virtual-mfa-devices. Cannot continue." + echo -e "${BIRed}Could not execute list-virtual-mfa-devices. Cannot continue.${Color_Off}" exit 1 fi @@ -1130,11 +1125,11 @@ else if [[ "$available_user_vmfad" == "" ]]; then # no vMFAd existed, none could be created - echo "No virtual MFA device to enable. Cannot continue." + echo -e "\\n\\n${BIRed}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 "Enabling the $vmfad_source virtual MFA device: $available_user_vmfad" + echo -e "\\n\\nEnabling the $vmfad_source virtual MFA device:\\n$available_user_vmfad\\n" fi echo @@ -1148,7 +1143,7 @@ else authcode2="${BASH_REMATCH[2]}" break; else - echo "Bad authcodes. Please enter two consequtively generated six-digit numbers separated by a space." + echo -e "${BIRed}Bad authcodes.${Color_Off} Please enter two consequtively generated six-digit numbers separated by a space." fi done @@ -1157,28 +1152,18 @@ else vmfad_enablement_status=$(aws --profile "${final_selection}" \ iam enable-mfa-device \ --user-name "${aws_iam_user}" \ - --serial-number ${available_user_vmfad} \ - --authentication-code-1 ${authcode1} \ - --authentication-code-2 ${authcode2} 2>&1) + --serial-number "${available_user_vmfad}" \ + --authentication-code-1 "${authcode1}" \ + --authentication-code-2 "${authcode2}" 2>&1) if [[ "$vmfad_enablement_status" =~ error ]]; then - echo "Could not enable vMFAd. Cannot continue." + echo -e "${BIRed}Could not enable vMFAd. Cannot continue.${Color_Off}" exit 1 else - echo "vMFAd enabled for the profile \"${final_selection}\" (IAM user name \"$aws_iam_user\")." + echo -e "${BIGreen}vMFAd successfully enabled for the profile '${final_selection}' ${Green}(IAM user name '$aws_iam_user').${Color_Off}" + echo fi - # find available vMFAds for the profile (IAM user); there should be 0 or 1 - # - # none found: create -> scan QRC -> Q: confirm del QRC -> enable - # - # one found: Q: has access? - # - # no: confirm delete/recreate -> delete vMFAd -> create new -> enable - # yes: enable - # - # enable vMFAd: QR is created -> scan -> Q: delete securely? (yes->delete) -> enter two consequtive codes (check for 2 x six digits, separated by one or more spaces before executing the aws command) - else echo -e "disable the vMFAd for the profile...\\n" @@ -1188,7 +1173,7 @@ else aws_account_id="${BASH_REMATCH[1]}" # this AWS account aws_iam_user="${BASH_REMATCH[2]}" # IAM user of the (hopefully :-) active MFA session else - echo -e "Could not acquire AWS account ID or current IAM user name. A bad profile? Cannot continue.\\n" + echo -e "${BIRed}Could not acquire AWS account ID or current IAM user name. A bad profile? Cannot continue.${Color_Off}\\n" echo exit 1 fi @@ -1200,7 +1185,7 @@ else [[ "$PRECHECK_AWS_SESSION_DURATION" != "" ]]; then # this is a MFA profile in the environment - getRemaining _ret $PRECHECK_AWS_SESSION_INIT_TIME $PRECHECK_AWS_SESSION_DURATION + getRemaining _ret "$PRECHECK_AWS_SESSION_INIT_TIME" "$PRECHECK_AWS_SESSION_DURATION" elif [[ "$PRECHECK_AWS_PROFILE" =~ ^${final_selection}-mfasession$ ]] && [[ "$profiles_idx" != "" ]]; then @@ -1214,7 +1199,7 @@ else # the parent/base profile's duration if [[ "$profile_time" != "" ]]; then getDuration parent_duration "$PRECHECK_AWS_PROFILE" - getRemaining _ret $profile_time $parent_duration + getRemaining _ret "$profile_time" "$parent_duration" fi elif [[ "$PRECHECK_AWS_PROFILE" == "" ]] && @@ -1228,16 +1213,16 @@ else # IAM user of the transient in-env MFA session matches # the IAM user of the selected persistent base profile - getRemaining _ret $PRECHECK_AWS_SESSION_INIT_TIME $PRECHECK_AWS_SESSION_DURATION + getRemaining _ret "$PRECHECK_AWS_SESSION_INIT_TIME" "$PRECHECK_AWS_SESSION_DURATION" else - echo -e "This is an unknown in-env MFA session. Cannot continue.\\n" + echo -e "${BIRed}This is an unknown in-env MFA session. Cannot continue.${Color_Off}\\n" print_mfa_notice exit 1 fi else # no valid MFA profile (in-env or functional reference) found - echo -e "No active MFA session found for the profile \"${final_selection}\".\\n" + echo -e "${BIRed}No active MFA session found for the profile '${final_selection}'.${Color_Off}\\n" print_mfa_notice echo exit 1 @@ -1248,32 +1233,31 @@ else # below profile is not defined because an active MFA must be used 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) + --user-name "${aws_iam_user}" \ + --serial-number "arn:aws:iam::${aws_account_id}:mfa/${aws_iam_user}" 2>&1) if [[ "$vmfad_deactivation_result" =~ error ]]; then - echo "Could not disable/detach vMFAd for profile ${final_selection}. Cannot continue." + echo -e "${BIRed}Could not disable/detach vMFAd for the profile '${final_selection}'. Cannot continue.${Color_Off}" exit 1 else echo - echo "vMFAd disabled/detached for the profile ${final_selection}." + echo -e "${BIGreen}vMFAd disabled/detached for the profile '${final_selection}'.${Color_Off}" echo - echo -en "${BIWhite}Do you want to DELETE the disabled/detached vMFAd? [Y]es/[N]o${Color_Off} " - echo + echo -en "${BIWhite}Do you want to DELETE the disabled/detached vMFAd? Y/N${Color_Off} " while : do read -s -n 1 -r if [[ $REPLY =~ ^[Yy]$ ]]; then - vmfad_delete_result=$(aws --profile ${final_selection} \ + vmfad_delete_result=$(aws --profile "${final_selection}" \ iam delete-virtual-mfa-device \ - --serial-number arn:aws:iam::${aws_account_id}:mfa/${aws_iam_user}) + --serial-number "arn:aws:iam::${aws_account_id}:mfa/${aws_iam_user}") - if [[ "$vmfad_deactivation_result" =~ error ]]; then - echo "Could not deleted vMFAd for profile ${final_selection}. Cannot continue." + if [[ "$vmfad_delete_result" =~ error ]]; then + echo -e "\\n${BIRed}Could not delete vMFAd for the profile '${final_selection}'. Cannot continue.${Color_Off}" exit 1 else - echo "vMFAd deleted for the profile ${final_selection}." + echo -e "\\n${BIGreen}vMFAd deleted for the profile '${final_selection}'.${Color_Off}" echo echo "To set up a new vMFAd, run this script again." echo @@ -1281,36 +1265,20 @@ else break; elif [[ $REPLY =~ ^[Nn]$ ]]; then - echo -e "\\n\\nNo action taken. Exiting.\\n\\nNOTE: Detached vMFAd's may be automatically deleted after some time.\\n" + echo -e "\\n\\n${BIWhite}The following vMFAd was detached/disabled, 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 fi else - echo -e "The MFA session for the profile \"${final_selection}\" has expired.\\n" + echo -e "\\n${BIRed}The MFA session for the profile \"${final_selection}\" has expired.${Color_Off}\\n" print_mfa_notice echo exit 1 fi exit 1 - - # check for the active MFA session with this vMFAd/profile - # - # no: Q: possession of the vMFAd? - # - # no: instructions ("must have active session to disable, contact ops if not able") & exit - # - # aws iam deactivate-mfa-device --user-name mfa-test-user --serial-number arn:aws:iam::248783370565:mfa/mfa-test-user - # - # yes: init an MFA session (use the vars locally) -> after that like 'yes' below - # - # yes: Q: confirm disable -> disable -> Q: delete vMFAd? - # - # no: exit & print notice of autodelete in X days - # yes: delete vMFAd and exit with info msg - fi else diff --git a/awscli-mfa/mfastatus.sh b/awscli-mfa/mfastatus.sh index ed08640..546072a 100755 --- a/awscli-mfa/mfastatus.sh +++ b/awscli-mfa/mfastatus.sh @@ -233,7 +233,7 @@ sessionData() { [[ "${AWS_PROFILE}" == "" ]] && AWS_PROFILE="[unnamed]" - echo -e "${Green}AWS PROFILE IN THE ENVIRONMENT: ${BIGreen}"${AWS_PROFILE}" ${Green}\n ${matched}${Color_Off}" + echo -e "${Green}AWS PROFILE IN THE ENVIRONMENT: ${BIGreen}"${AWS_PROFILE}" ${Green}\\n ${matched}${Color_Off}" if [[ "$AWS_SESSION_INIT_TIME" != "" ]]; then @@ -278,7 +278,7 @@ if [[ "$AWS_CONFIG_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." + 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 @@ -294,14 +294,14 @@ elif [[ "$AWS_CONFIG_FILE" != "" ]] && [ ! -f "$AWS_CONFIG_FILE" ]; then echo - echo -e "${BIRed}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." + echo -e "${BIRed}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}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." + echo -e "${BIRed}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 @@ -317,14 +317,14 @@ elif [[ "$AWS_SHARED_CREDENTIALS_FILE" != "" ]] && [ ! -f "$AWS_SHARED_CREDENTIALS_FILE" ]; then echo - echo -e "${BIRed}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." + echo -e "${BIRed}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}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." + echo -e "${BIRed}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 @@ -471,7 +471,7 @@ if [[ "$AWS_PROFILE" != "" ]]; then elif [[ "${profile_type}" == "session" ]]; then echo -e "${Green}ENVVAR 'AWS_PROFILE' SELECTING A PERSISTENT MFA SESSION (as below): ${BIGreen}${AWS_PROFILE}${Color_Off}" else - echo -e "${BIRed}INVALID ENVIRONMENT CONFIGURATION!\nExecute \"source ./source-to-clear-AWS-envvars.sh\" to clear the environment.\n${Color_Off}" + echo -e "${BIRed}INVALID ENVIRONMENT CONFIGURATION!\\nExecute ${Red}source ./source-to-clear-AWS-envvars.sh${BIRed} to clear the environment.\\n${Color_Off}" fi fi else @@ -479,7 +479,7 @@ else sessionData else - echo -e "No AWS profile variables present in the environment;\nusing the default base profile." + echo -e "No AWS profile variables present in the environment;\\nusing the default base profile." fi fi @@ -528,7 +528,7 @@ if [[ $live_session_counter -eq 0 ]]; then echo fi -echo -e "NOTE: Execute 'awscli-mfa.sh' to renew/start a new MFA session,\n or to select (switch to) an existing active MFA session." +echo -e "NOTE: Execute 'awscli-mfa.sh' to renew/start a new MFA session,\\n or to select (switch to) an existing active MFA session." echo echo From 96fcae723030639733f76da7f0708b15a6f020c2 Mon Sep 17 00:00:00 2001 From: Ville Walveranta Date: Tue, 3 Apr 2018 01:04:46 -0500 Subject: [PATCH 34/71] Code cleanup, bug & typo fixes, small feature enhancements. --- awscli-mfa/awscli-mfa.sh | 42 +++++++------ awscli-mfa/enable-disable-vmfa-device.sh | 8 +-- awscli-mfa/mfastatus.sh | 77 +++++++++++++++++------- 3 files changed, 79 insertions(+), 48 deletions(-) diff --git a/awscli-mfa/awscli-mfa.sh b/awscli-mfa/awscli-mfa.sh index c619dc0..d1266ee 100755 --- a/awscli-mfa/awscli-mfa.sh +++ b/awscli-mfa/awscli-mfa.sh @@ -120,7 +120,8 @@ exists() { checkEnvSession() { # $1 is the check type - local this_time=$(date +%s) + local this_time + this_time=$(date +%s) # COLLECT AWS_SESSION DATA FROM THE ENVIRONMENT PRECHECK_AWS_PROFILE=$(env | grep AWS_PROFILE) @@ -193,7 +194,7 @@ checkEnvSession() { # 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 + getRemaining _ret "$PRECHECK_AWS_SESSION_INIT_TIME" "$PRECHECK_AWS_SESSION_DURATION" [[ "${_ret}" -eq 0 ]] && continue_maybe "expired" elif [[ "$PRECHECK_AWS_PROFILE" =~ -mfasession$ ]] && @@ -210,7 +211,7 @@ checkEnvSession() { # the parent/base profile's duration if [[ "$profile_time" != "" ]]; then getDuration parent_duration "$PRECHECK_AWS_PROFILE" - getRemaining _ret $profile_time $parent_duration + getRemaining _ret "$profile_time" "$parent_duration" [[ "${_ret}" -eq 0 ]] && continue_maybe "expired" fi fi @@ -299,9 +300,9 @@ addInitTime() { sed -i '' -e "s/${profile_time}/${this_time}/g" "$CREDFILE" 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" + 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 @@ -357,7 +358,8 @@ getRemaining() { local timestamp=$2 local duration=$3 - local this_time=$(date +%s) + local this_time + this_time=$(date +%s) local remaining=0 [[ "${duration}" == "" ]] && @@ -827,7 +829,7 @@ else getInitTime _ret_timestamp "$mfa_profile_ident" getDuration _ret_duration "$mfa_profile_ident" - getRemaining _ret_remaining ${_ret_timestamp} ${_ret_duration} + getRemaining _ret_remaining "${_ret_timestamp}" "${_ret_duration}" if [[ ${_ret_remaining} -eq 0 ]]; then # session has expired @@ -836,7 +838,7 @@ else elif [[ ${_ret_remaining} -gt 0 ]]; then # session time remains - getPrintableTimeRemaining _ret ${_ret_remaining} + 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 @@ -1025,7 +1027,7 @@ else mfa_parent_profile_ident="${cred_profiles[$actual_selprofile]}" final_selection="${mfa_profiles[$actual_selprofile]}" - echo "SELECTED MFA PROFILE: ${final_selection} (for base profile \"${mfa_parent_profile_ident}\")" + 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" @@ -1073,13 +1075,13 @@ else # prompt for the MFA code echo - echo -e "${BIWhite}Enter the current MFA one time pass code for profile '${cred_profiles[$actual_selprofile]}'${Color_Off} to start/renew an MFA session," + echo -e "${BIWhite}Enter the current MFA one time pass code for the profile '${cred_profiles[$actual_selprofile]}'${Color_Off} to start/renew an MFA session," echo "or leave empty (just press [ENTER]) to use the selected profile without the MFA." - + echo while : do echo -en "${BIWhite}" - read mfacode + read -p ">>> " -r mfacode echo -en "${Color_Off}" if ! [[ "$mfacode" =~ ^$ || "$mfacode" =~ [0-9]{6} ]]; then echo -e "${BIRed}The MFA pass code must be exactly six digits, or blank to bypass (to use the profile without an MFA session).${Color_Off}" @@ -1107,7 +1109,7 @@ else ARN_OF_MFA=${mfa_arns[$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:]]*$')" + profile_lookup="$(grep "$CONFFILE" -e '^[[:space:]]*\[[[:space:]]*profile '"${AWS_2AUTH_PROFILE}"'[[:space:]]*\][[:space:]]*$')" if [[ "$profile_lookup" == "" ]]; then echo >> "$CONFFILE" echo "[profile ${AWS_2AUTH_PROFILE}]" >> "$CONFFILE" @@ -1118,12 +1120,12 @@ else getDuration AWS_SESSION_DURATION "$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 $AWS_SESSION_DURATION \ - --serial-number $ARN_OF_MFA \ + read -r AWS_ACCESS_KEY_ID AWS_SECRET_ACCESS_KEY AWS_SESSION_TOKEN <<< \ + $(aws --profile "$AWS_USER_PROFILE" sts get-session-token \ + --duration "$AWS_SESSION_DURATION" \ + --serial-number "$ARN_OF_MFA" \ --token-code $mfacode \ - --output text | awk '{ print $2, $4, $5 }') + --output text | awk '{ print $2, $4, $5 }') if [ -z "$AWS_ACCESS_KEY_ID" ]; then echo @@ -1145,7 +1147,7 @@ else # 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 + getPrintableTimeRemaining _ret "$AWS_SESSION_DURATION" validity_period=${_ret} echo -e "${BIWhite}Make this MFA session persistent?${Color_Off} (Saves the session in $CREDFILE\\nso that you can return to it during its validity period, ${validity_period}.)" read -s -p "$(echo -e "${BIWhite}Yes (default) - make peristent${Color_Off}; No - only the envvars will be used ${BIWhite}[Y]${Color_Off}/N ")" -n 1 -r diff --git a/awscli-mfa/enable-disable-vmfa-device.sh b/awscli-mfa/enable-disable-vmfa-device.sh index 1cc8710..e2cd9e9 100755 --- a/awscli-mfa/enable-disable-vmfa-device.sh +++ b/awscli-mfa/enable-disable-vmfa-device.sh @@ -401,8 +401,6 @@ continue_maybe() { unset AWS_SHARED_CREDENTIALS_FILE unset AWS_CONFIG_FILE - - custom_configfiles_reset="true" fi unset AWS_PROFILE @@ -815,7 +813,7 @@ else elif [[ ${_ret_remaining} -gt 0 ]]; then # session time remains - getPrintableTimeRemaining _ret ${_ret_remaining} + 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 @@ -1133,7 +1131,7 @@ else fi echo - echo -e "${BIWhite}Please enter two consequtively 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" + echo -e "${BIWhite}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 @@ -1143,7 +1141,7 @@ else authcode2="${BASH_REMATCH[2]}" break; else - echo -e "${BIRed}Bad authcodes.${Color_Off} Please enter two consequtively generated six-digit numbers separated by a space." + echo -e "${BIRed}Bad authcodes.${Color_Off} Please enter two consecutively generated six-digit numbers separated by a space." fi done diff --git a/awscli-mfa/mfastatus.sh b/awscli-mfa/mfastatus.sh index 546072a..c71ff1e 100755 --- a/awscli-mfa/mfastatus.sh +++ b/awscli-mfa/mfastatus.sh @@ -167,7 +167,8 @@ getRemaining() { local timestamp=$2 local duration=$3 - local this_time=$(date +%s) + local this_time + this_time=$(date +%s) local remaining=0 [[ "${duration}" == "" ]] && @@ -211,29 +212,50 @@ getPrintableTimeRemaining() { 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)" + matched="not same as the similarly named persistent session" else - matched="(an in-env session only)" + matched="an in-env session [only]" + in_env_only="true" fi else - matched="(an in-env session only)" + 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)" + matched="same as the similarly named persistent session below" else - matched="(same as the persistent session \"${profiles_ident[$idx]}\")" + 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]" - echo -e "${Green}AWS PROFILE IN THE ENVIRONMENT: ${BIGreen}"${AWS_PROFILE}" ${Green}\\n ${matched}${Color_Off}" + 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 @@ -241,7 +263,7 @@ sessionData() { [[ "${AWS_SESSION_DURATION}" == "" ]] && AWS_SESSION_DURATION=$MFA_SESSION_LENGTH_IN_SECONDS - getRemaining _ret_remaining $AWS_SESSION_INIT_TIME $AWS_SESSION_DURATION + 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-to-clear-AWS-envvars.sh'${Color_Off}" @@ -256,14 +278,13 @@ repeatr() { # $2 is the base_length # $3 is the variable_repeat_length - local string_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))"}"); + printf "$repeat_char"'%.s' $(eval "echo {1.."$((repeat_length))"}"); } # -- end functions -- @@ -494,18 +515,33 @@ 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 - echo -e "${Green}MFA SESSION IDENT: ${BIGreen}${profiles_ident[$z]}${Color_Off}" - if [[ "${profiles_session_init_time[$z]}" != "" ]]; 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" + 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')${Color_Off}" + else + echo -e "${Green}MFA SESSION IDENT: ${BIGreen}${profiles_ident[$z]} ${Red}(IAM user: $for_iam)${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} + 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 @@ -523,12 +559,7 @@ do done if [[ $live_session_counter -eq 0 ]]; then - echo "No current active persistent MFA sessions." - echo - echo + echo -e "No current active persistent MFA sessions.\\n\\n" fi -echo -e "NOTE: Execute 'awscli-mfa.sh' to renew/start a new MFA session,\\n or to select (switch to) an existing active MFA session." - -echo -echo +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" From da5f84dd225fc61b0f168f38e5e4094b430716b3 Mon Sep 17 00:00:00 2001 From: Ville Walveranta Date: Tue, 3 Apr 2018 03:37:39 -0500 Subject: [PATCH 35/71] UX improvements, bug fixes to vmfad registration, mfastatus, and awscli-mfa. --- awscli-mfa/awscli-mfa.sh | 5 ++-- awscli-mfa/enable-disable-vmfa-device.sh | 31 +++++++++++------------- 2 files changed, 17 insertions(+), 19 deletions(-) diff --git a/awscli-mfa/awscli-mfa.sh b/awscli-mfa/awscli-mfa.sh index d1266ee..f5fc716 100755 --- a/awscli-mfa/awscli-mfa.sh +++ b/awscli-mfa/awscli-mfa.sh @@ -102,6 +102,7 @@ BIWhite='\033[1;97m' # White 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 @@ -955,7 +956,7 @@ else # create the profile selections echo - echo -e "${BBlack}${On_White} AVAILABLE AWS PROFILES: ${Color_Off}" + echo -e "${BIWhite}${On_DGreen} AVAILABLE AWS PROFILES: ${Color_Off}" echo SELECTR=0 ITER=1 @@ -1250,7 +1251,7 @@ else echo echo - echo -e "${BIWhite}${On_Green} * * * PROFILE DETAILS * * * ${Color_Off}" + echo -e "${BIWhite}${On_DGreen} * * * PROFILE DETAILS * * * ${Color_Off}" echo if [[ "$mfaprofile" == "true" ]]; then echo -e "${BIWhite}MFA profile name: '${final_selection}'${Color_Off}" diff --git a/awscli-mfa/enable-disable-vmfa-device.sh b/awscli-mfa/enable-disable-vmfa-device.sh index e2cd9e9..029b654 100755 --- a/awscli-mfa/enable-disable-vmfa-device.sh +++ b/awscli-mfa/enable-disable-vmfa-device.sh @@ -102,6 +102,7 @@ BIWhite='\033[1;97m' # White 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 @@ -911,7 +912,7 @@ else # create the profile selections for "no vMFAd configured" and "vMFAd enabled" echo - echo -e "${BBlack}${On_White} AWS PROFILES WITH NO ATTACHED/ENABLED VIRTUAL MFA DEVICE (vMFAd): ${Color_Off}" + echo -e "${BIWhite}${On_Red} AWS PROFILES WITH NO ATTACHED/ENABLED VIRTUAL MFA DEVICE (vMFAd): ${Color_Off}" echo -e " ${BIWhite}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 @@ -931,7 +932,7 @@ else done echo - echo -e "${BBlack}${On_White} AWS PROFILES WITH ACTIVE (ENABLED) VIRTUAL MFA DEVICE (vMFAd): ${Color_Off}" + echo -e "${BIWhite}${On_DGreen} AWS PROFILES WITH ACTIVE (ENABLED) VIRTUAL MFA DEVICE (vMFAd): ${Color_Off}" echo -e " ${BIWhite}Select a profile whose vMFAd you want to detach/disable.${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 @@ -949,7 +950,7 @@ else done # prompt for profile selection - echo -en "\\n${BIWhite}SELECT A PROFILE BY THE ID: " + echo -en "\\n${BIWhite}SELECT A PROFILE BY THE NUMBER: " read -r selprofile echo -en "\\n${Color_Off}" @@ -958,32 +959,32 @@ else # process the selection if [[ "$selprofile" == "-1" ]]; then selprofile="1" - else - translated_selprofile=${iter_to_profile[$selprofile]} - ((selprofile=translated_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 - ((actual_selprofile=selprofile_check-1)) profilecount=${#cred_profiles[@]} - if [[ $actual_selprofile -ge $profilecount || - $actual_selprofile -lt 0 ]]; then + if [[ $selprofile_check -gt $profilecount || + $selprofile_check -lt 1 ]]; then + # a selection outside of the existing range was specified - echo -e "${BIRed}There is no profile '${selprofile}'.${Color_Off}" + echo -e "${BIRed}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 + # a base profile was selected (sessions are not considered) if [[ $selprofile =~ ^[[:digit:]]+$ ]]; then echo - final_selection="${cred_profiles[$actual_selprofile]}" + final_selection="${cred_profiles[$translated_selprofile]}" echo -n "Preparing to " idxLookup idx cred_profiles[@] "$final_selection" @@ -1242,7 +1243,7 @@ else echo -e "${BIGreen}vMFAd disabled/detached for the profile '${final_selection}'.${Color_Off}" echo - echo -en "${BIWhite}Do you want to DELETE the disabled/detached vMFAd? Y/N${Color_Off} " + echo -en "${BIWhite}Do you want to ${BIRed}DELETE${BIWhite} the disabled/detached vMFAd? Y/N${Color_Off} " while : do read -s -n 1 -r @@ -1275,17 +1276,14 @@ else echo exit 1 fi - exit 1 fi - else # non-acceptable characters were present in the selection echo -e "${BIRed}There is no profile '${selprofile}'.${Color_Off}" echo exit 1 fi - else # no numeric part in selection echo -e "${BIRed}There is no profile '${selprofile}'.${Color_Off}" @@ -1298,5 +1296,4 @@ else echo exit 1 fi - fi From 4f1c9c83fedf45ce45e2d995cd4a6a59c70e5f1e Mon Sep 17 00:00:00 2001 From: Ville Walveranta Date: Wed, 4 Apr 2018 03:03:03 -0500 Subject: [PATCH 36/71] Documentation update. --- README.md | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 084385d..822551a 100644 --- a/README.md +++ b/README.md @@ -5,9 +5,15 @@ This repository contains non-proprietary (MIT license) utility scripts for use w * **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/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/). +* **awscli-mfa/awscli-mfa.sh** - Makes it easy to use MFA sessions with AWS CLI. Multiple profiles are supported, but if only a simple profile 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, 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/). -* **awscli-mfa/source-to-clear-AWS-envvars.sh** - Simple sourceable script that removes any AWS secrets/settings that may have been set in the local environment by `awscli-mfa.sh` script (above). +* **awscli-mfa/enable-disable-vmfa-device.sh** - Makes it very easy to enable/attach and disable/detach (as well as delete) a virtual MFA device (vMFAd). Assumes that each IAM user can have one vMFAd configured at a time, and that is named the same as their IAM username (i.e. `.../mfa/{IAMusername}` instead of `.../user/{IAMusername}`). Disabling a vMFAd requires an active MFA session with that profile. + +* **awscli-mfa/mfastatus.sh** - Displays the currently active MFA sessions and their remaining activity period. + +* **awscli-mfa/example-MFA-enforcement-policy.txt** - An example IAM policy to enforce MFA usage. A policy similar to this can be used in conjunction with these MFA management scripts. + +* **awscli-mfa/source-to-clear-AWS-envvars.sh** - Simple sourceable script that removes any AWS secrets/settings that may have been set in the local environment by `awscli-mfa.sh` script (above). Source this like so: `source awscli-mfa/source-to-clear-AWS-envvars.sh` * **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. From 1590f4611760a7306135cf792d788b80e9fca414 Mon Sep 17 00:00:00 2001 From: Ville Walveranta Date: Wed, 4 Apr 2018 12:52:02 -0500 Subject: [PATCH 37/71] Small UX enhancement. --- awscli-mfa/awscli-mfa.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/awscli-mfa/awscli-mfa.sh b/awscli-mfa/awscli-mfa.sh index f5fc716..5691744 100755 --- a/awscli-mfa/awscli-mfa.sh +++ b/awscli-mfa/awscli-mfa.sh @@ -1258,7 +1258,7 @@ else echo else echo -e "${BIWhite}Profile name '${final_selection}'${Color_Off}" - echo "NOTE: This is not an MFA session!" + echo -e "\\n${BIWhite}NOTE: This is not an MFA session!${Color_Off}" echo fi echo -e "Region is set to: ${BIWhite}${AWS_DEFAULT_REGION}${Color_Off}" From c6f68e7a0221fc00e4f47f0f6f4a92e29e37d822 Mon Sep 17 00:00:00 2001 From: Ville Walveranta Date: Thu, 5 Apr 2018 01:28:40 -0500 Subject: [PATCH 38/71] Updates to the example MFA enforcement policy. --- awscli-mfa/example-MFA-enforcement-policy.txt | 33 ++++++++++++------- 1 file changed, 21 insertions(+), 12 deletions(-) diff --git a/awscli-mfa/example-MFA-enforcement-policy.txt b/awscli-mfa/example-MFA-enforcement-policy.txt index 9e4614b..b4d2c75 100644 --- a/awscli-mfa/example-MFA-enforcement-policy.txt +++ b/awscli-mfa/example-MFA-enforcement-policy.txt @@ -1,18 +1,27 @@ { "Version": "2012-10-17", "Statement": [ + { + "Sid": "AllowAllUsersToListTheAvailbleMFADevices", + "Effect": "Allow", + "Action": [ + "iam:ListVirtualMFADevices" + ], + "Resource": [ + "arn:aws:iam::REPLACE-WITH-YOUR-AWS-ACCOUNT-ID:*" + ] + }, { "Sid": "AllowAllUsersToListAccounts", "Effect": "Allow", "Action": [ "iam:GetAccountPasswordPolicy", - "iam:ListVirtualMFADevices", "iam:ListAccountAliases", "iam:ListUsers" ], "Resource": [ - "arn:aws:iam::ACCOUNT-ID-WITHOUT-HYPHENS:mfa/*", - "arn:aws:iam::ACCOUNT-ID-WITHOUT-HYPHENS:user/*" + "arn:aws:iam::REPLACE-WITH-YOUR-AWS-ACCOUNT-ID:mfa/*", + "arn:aws:iam::REPLACE-WITH-YOUR-AWS-ACCOUNT-ID:user/*" ] }, { @@ -28,7 +37,7 @@ "iam:UpdateLoginProfile" ], "Resource": [ - "arn:aws:iam::ACCOUNT-ID-WITHOUT-HYPHENS:user/${aws:username}" + "arn:aws:iam::REPLACE-WITH-YOUR-AWS-ACCOUNT-ID:user/${aws:username}" ] }, { @@ -38,8 +47,8 @@ "iam:ListMFADevices" ], "Resource": [ - "arn:aws:iam::ACCOUNT-ID-WITHOUT-HYPHENS:mfa/${aws:username}@@*", - "arn:aws:iam::ACCOUNT-ID-WITHOUT-HYPHENS:user/${aws:username}" + "arn:aws:iam::REPLACE-WITH-YOUR-AWS-ACCOUNT-ID:mfa/${aws:username}", + "arn:aws:iam::REPLACE-WITH-YOUR-AWS-ACCOUNT-ID:user/${aws:username}" ] }, { @@ -52,8 +61,8 @@ "iam:ResyncMFADevice" ], "Resource": [ - "arn:aws:iam::ACCOUNT-ID-WITHOUT-HYPHENS:mfa/${aws:username}@@*", - "arn:aws:iam::ACCOUNT-ID-WITHOUT-HYPHENS:user/${aws:username}" + "arn:aws:iam::REPLACE-WITH-YOUR-AWS-ACCOUNT-ID:mfa/${aws:username}", + "arn:aws:iam::REPLACE-WITH-YOUR-AWS-ACCOUNT-ID:user/${aws:username}" ] }, { @@ -63,8 +72,8 @@ "iam:DeactivateMFADevice" ], "Resource": [ - "arn:aws:iam::ACCOUNT-ID-WITHOUT-HYPHENS:mfa/${aws:username}@@*", - "arn:aws:iam::ACCOUNT-ID-WITHOUT-HYPHENS:user/${aws:username}" + "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": { "Bool": { @@ -120,8 +129,8 @@ "iam:ListAccessKeys" ], "NotResource": [ - "arn:aws:iam::ACCOUNT-ID-WITHOUT-HYPHENS:mfa/${aws:username}@@*", - "arn:aws:iam::ACCOUNT-ID-WITHOUT-HYPHENS:user/${aws:username}" + "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": { From d554b23f61e34e26c48eb37a6fa6d47b55bb806b Mon Sep 17 00:00:00 2001 From: Ville Walveranta Date: Thu, 5 Apr 2018 01:33:03 -0500 Subject: [PATCH 39/71] UX enhancement to enable-disable-vmfa-device.sh --- awscli-mfa/enable-disable-vmfa-device.sh | 1 + 1 file changed, 1 insertion(+) diff --git a/awscli-mfa/enable-disable-vmfa-device.sh b/awscli-mfa/enable-disable-vmfa-device.sh index 029b654..57ae8a2 100755 --- a/awscli-mfa/enable-disable-vmfa-device.sh +++ b/awscli-mfa/enable-disable-vmfa-device.sh @@ -1160,6 +1160,7 @@ else exit 1 else echo -e "${BIGreen}vMFAd successfully enabled for the profile '${final_selection}' ${Green}(IAM user name '$aws_iam_user').${Color_Off}" + echo -e "${BIGreen}You can now use the 'awscli-mfa.sh' script to start an MFA session for this profile!${Color_Off}" echo fi From 5dd949a5f1e366cae45cbe7aa49d0eac5cf97735 Mon Sep 17 00:00:00 2001 From: Ville Walveranta Date: Thu, 5 Apr 2018 01:50:24 -0500 Subject: [PATCH 40/71] Updates to the example MFA enforcement policy. --- awscli-mfa/example-MFA-enforcement-policy.txt | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/awscli-mfa/example-MFA-enforcement-policy.txt b/awscli-mfa/example-MFA-enforcement-policy.txt index b4d2c75..a14d747 100644 --- a/awscli-mfa/example-MFA-enforcement-policy.txt +++ b/awscli-mfa/example-MFA-enforcement-policy.txt @@ -1,6 +1,16 @@ { "Version": "2012-10-17", "Statement": [ + { + "Sid": "AllowAllUsersToListAccountAliases", + "Effect": "Allow", + "Action": [ + "iam:ListAccountAliases" + ], + "Resource": [ + "*" + ] + }, { "Sid": "AllowAllUsersToListTheAvailbleMFADevices", "Effect": "Allow", @@ -8,7 +18,7 @@ "iam:ListVirtualMFADevices" ], "Resource": [ - "arn:aws:iam::REPLACE-WITH-YOUR-AWS-ACCOUNT-ID:*" + "arn:aws:iam::REPLACE-WITH-YOUR-AWS-ACCOUNT-ID:mfa/*" ] }, { From 8e3f10d8ffbc626a2a783345f26b11ce765afc1b Mon Sep 17 00:00:00 2001 From: Ville Walveranta Date: Thu, 5 Apr 2018 13:35:38 -0500 Subject: [PATCH 41/71] awscli-mfa.sh: Linux compatibility update --- awscli-mfa/awscli-mfa.sh | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/awscli-mfa/awscli-mfa.sh b/awscli-mfa/awscli-mfa.sh index 5691744..b9bb77d 100755 --- a/awscli-mfa/awscli-mfa.sh +++ b/awscli-mfa/awscli-mfa.sh @@ -298,7 +298,12 @@ addInitTime() { # update/add session init time if [[ $profile_time != "" ]]; then # time entry exists for the profile, update - sed -i '' -e "s/${profile_time}/${this_time}/g" "$CREDFILE" + + 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}\\]" From 899cde54e54ff997286e12be2004e6bbf610f685 Mon Sep 17 00:00:00 2001 From: Ville Walveranta Date: Tue, 10 Apr 2018 03:55:19 -0500 Subject: [PATCH 42/71] awscli-mfa.sh documentation.. --- awscli-mfa/README.md | 55 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 55 insertions(+) create mode 100644 awscli-mfa/README.md diff --git a/awscli-mfa/README.md b/awscli-mfa/README.md new file mode 100644 index 0000000..cb332c6 --- /dev/null +++ b/awscli-mfa/README.md @@ -0,0 +1,55 @@ + +# awscli-mfa and its companion scripts + +The `awscli-mfa.sh` and its companion scripts `enable-disable-vmfa-device.sh` `mfastatus.sh`, and `source-to-clear-AWS-envvars.sh` were created to make handling AWS MFA sessions easy on the command line. + +When the presence of multi-factor authentication session to execute AWS commands (i.e. not just the login to the web console) is enforced using an IAM policy, it cannot be limited to the web console operations. This is because the AWS web console is basially a front-end to the AWS APIs that 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 session credentials, and so beyond that point it is transparent to the user until the session eventually expires, and the AWS web console prompts you to log in again. On the command line it is different. To register a virtual MFA device, or to start a session, a complex sequence of `aws cli` commands would be required, followed by painstakingly saving the session credentials to the `~/.aws/credentials` file, and then referring to them using the `--profile` switch on each `aws cli` command. Furthermore, the only way to know that the session has ended would be when the `aws cli` commands would start failing, thus making it difficult to plan command execution, and potentially being confusing as to why such failures would occur. + +The `awscli-mfa.sh` and its companion scripts change all this making use of MFA sessions with `aws cli` a breeze. Let's first look at what each script does on the high level. + +### Overview + +All scripts provide significant interactive guidance as well as user-friendly failure information when something doesn't work as expected. + +All scripts have been tested in macOS (High Sierra with stock bash 3.2.x) as well as with Linux (Ubuntu 16.04 with modern default bash 4.3.x). The only dependency is `awscli`. + +* **awscli-mfa.sh** - Makes it easy to start MFA sessions with `aws cli`, and to switch between active sessions. 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, and as such it does not take arguments. 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 is named the same as their IAM username (i.e. the serial number of the vMFAd is of format `arn:aws:iam::XXXXXXXXXXXX:mfa/{IAMusername}` when the IAM user Arn is `arn:aws:iam::XXXXXXXXXXXX:user/{IAMusername}`). Disabling a vMFAd requires an active MFA session with that profile; if you no longer have acess to the vMFAd in your Google Authenticator or Authy app, you either need to have admin privileges to the AWS account, or contact the ops with a request to delete the vMFAd 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. + +* **mfastatus.sh** - Displays the currently active MFA sessions and their remaining activity period. Also indicates expired persistent (or in-environment) profiles in "EXPIRED" status. + +* **source-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 this like so: `source ./source-to-clear-AWS-envvars.sh`, or set an alias, like so: `alias clearaws='source ~/awscli-mfa/source-to-clear-AWS-envvars.sh` + +* **example-MFA-enforcement-policy.txt** - An example IAM policy to enforce active MFA session to allow 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) + +### 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 from AWS. To keep track of the remaining activity period, the following values 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, and it needs to equal to the length of an MFA session in seconds defined by your IAM policy (see the two `"aws:MultiFactorAuthAge": "32400"` entries in `example-MFA-enforcement-policy.txt` that you should fashion your MFA session enforcement policy after). 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 variable in the three scripts. + +* `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 +``` + +* `mfasec =` - An optional, user-configurable variable sets the profile-specific session length. If defined in `~/.aws/config` for a profile, it overrides the default `MFA_SESSION_LENGTH_IN_SECONDS`, and thus makes it possible for different AWS profiles (and thus often different AWS accounts) to have their MFA session enforcement policy to be set to different maximum session lengths. If you're not an AWS admin, ask your DevOps/admin contact what the MFA session lifetime is set to. There is no way to know it otherwise as it is an arbitrary value that cannot be queried via the `aws cli`. The optional `mfasec` value in `~/.aws/config` looks like this (here the `test-user-mfasession` MFA session is set to last 21600 seconds, or 6 hours (NOTE: `mfasec` is defined for the base profile for which you wish to start an MFA session; the MFA session profile names have the `-mfasession` postfix which the base profiles do not have): + +``` +[profile test-user] +region = us-east-1 +output = table +mfasec = 21600 +``` + +### Usage + +[coming soon] + + From 0c5a7a6859e297d9ecf093abc381d0b33deb2eea Mon Sep 17 00:00:00 2001 From: Ville Walveranta Date: Tue, 10 Apr 2018 15:57:46 -0500 Subject: [PATCH 43/71] awscli-mfa documentation update --- awscli-mfa/README.md | 268 +++++++++++++++++++++++++++++++++++---- awscli-mfa/awscli-mfa.sh | 1 + 2 files changed, 242 insertions(+), 27 deletions(-) diff --git a/awscli-mfa/README.md b/awscli-mfa/README.md index cb332c6..fa5a4c6 100644 --- a/awscli-mfa/README.md +++ b/awscli-mfa/README.md @@ -1,55 +1,269 @@ # awscli-mfa and its companion scripts -The `awscli-mfa.sh` and its companion scripts `enable-disable-vmfa-device.sh` `mfastatus.sh`, and `source-to-clear-AWS-envvars.sh` were created to make handling AWS MFA sessions easy on the command line. +The `awscli-mfa.sh` and its companion scripts `enable-disable-vmfa-device.sh` `mfastatus.sh`, and `source-to-clear-AWS-envvars.sh` were created to make handling AWS MFA sessions on the command line easy. -When the presence of multi-factor authentication session to execute AWS commands (i.e. not just the login to the web console) is enforced using an IAM policy, it cannot be limited to the web console operations. This is because the AWS web console is basially a front-end to the AWS APIs that 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 session credentials, and so beyond that point it is transparent to the user until the session eventually expires, and the AWS web console prompts you to log in again. On the command line it is different. To register a virtual MFA device, or to start a session, a complex sequence of `aws cli` commands would be required, followed by painstakingly saving the session credentials to the `~/.aws/credentials` file, and then referring to them using the `--profile` switch on each `aws cli` command. Furthermore, the only way to know that the session has ended would be when the `aws cli` commands would start failing, thus making it difficult to plan command execution, and potentially being confusing as to why such failures would occur. +### Usage, quick! -The `awscli-mfa.sh` and its companion scripts change all this making use of MFA sessions with `aws cli` a breeze. Let's first look at what each script does on the high level. +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: -### Overview +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). -All scripts provide significant interactive guidance as well as user-friendly failure information when something doesn't work as expected. +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: -All scripts have been tested in macOS (High Sierra with stock bash 3.2.x) as well as with Linux (Ubuntu 16.04 with modern default bash 4.3.x). The only dependency is `awscli`. + `aws configure --profile "SomeDescriptiveProfileName"` -* **awscli-mfa.sh** - Makes it easy to start MFA sessions with `aws cli`, and to switch between active sessions. 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, and as such it does not take arguments. 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. +.. 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 :-) -* **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 is named the same as their IAM username (i.e. the serial number of the vMFAd is of format `arn:aws:iam::XXXXXXXXXXXX:mfa/{IAMusername}` when the IAM user Arn is `arn:aws:iam::XXXXXXXXXXXX:user/{IAMusername}`). Disabling a vMFAd requires an active MFA session with that profile; if you no longer have acess to the vMFAd in your Google Authenticator or Authy app, you either need to have admin privileges to the AWS account, or contact the ops with a request to delete the vMFAd 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. +``` +AWS Access Key ID [None]: AKIAIL3VDLRPTXVU3ART +AWS Secret Access Key [None]: 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 +Default region name [None]: us-east-1 +Default output format [None]: table +``` -* **mfastatus.sh** - Displays the currently active MFA sessions and their remaining activity period. Also indicates expired persistent (or in-environment) profiles in "EXPIRED" status. -* **source-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 this like so: `source ./source-to-clear-AWS-envvars.sh`, or set an alias, like so: `alias clearaws='source ~/awscli-mfa/source-to-clear-AWS-envvars.sh` +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 '///'): -* **example-MFA-enforcement-policy.txt** - An example IAM policy to enforce active MFA session to allow 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) +``` +Executing this script as the AWS/IAM user 'mfa-test-user' (profile 'default'). -### Session Activity Period +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. -Because the MFA session expiration time is encoded in the encrypted AWS session token, there is no way to retrieve the expiration time from AWS. To keep track of the remaining activity period, the following values are used: +>>> 923558 212566 -* `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, and it needs to equal to the length of an MFA session in seconds defined by your IAM policy (see the two `"aws:MultiFactorAuthAge": "32400"` entries in `example-MFA-enforcement-policy.txt` that you should fashion your MFA session enforcement policy after). 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 variable in the three scripts. +vMFAd successfully enabled for the profile 'default' (IAM user name 'mfa-test-user'). -* `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: +You can now use the 'awscli-mfa.sh' script to start an MFA session for this profile! ``` -[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 + +3. If something goes wrong with the vMFAd activation process, the script gives a hopefully clear/obvious guidance.

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-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. + ``` -* `mfasec =` - An optional, user-configurable variable sets the profile-specific session length. If defined in `~/.aws/config` for a profile, it overrides the default `MFA_SESSION_LENGTH_IN_SECONDS`, and thus makes it possible for different AWS profiles (and thus often different AWS accounts) to have their MFA session enforcement policy to be set to different maximum session lengths. If you're not an AWS admin, ask your DevOps/admin contact what the MFA session lifetime is set to. There is no way to know it otherwise as it is an arbitrary value that cannot be queried via the `aws cli`. The optional `mfasec` value in `~/.aws/config` looks like this (here the `test-user-mfasession` MFA session is set to last 21600 seconds, or 6 hours (NOTE: `mfasec` is defined for the base profile for which you wish to start an MFA session; the MFA session profile names have the `-mfasession` postfix which the base profiles do not have): +Now you can execute `mfastatus.sh` to view the remaining activity period on the MFA session: ``` -[profile test-user] -region = us-east-1 -output = table -mfasec = 21600 + +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. + + ``` -### Usage +Finally, a sourceable `source-to-clear-AWS-envvars.sh` is provided to make it easy to clear out any any `AWS_*` envvars, like so: `source ./source-to-clear-AWS-envvars.sh`. This purges any secrets and/or references to persistent profiles from the local environment. + + +### 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 register a virtual MFA device (vMFAd), or to start a session, a complex sequence 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 set 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) as well as with Linux (Ubuntu 16.04 with modern default bash 4.3.x). The only dependency is `aws cli`, 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 does not take command line arguments. 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 it is named the same as their IAM username (i.e. the serial number, Arn, 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 an active MFA session with that profile; if you no longer have access to the vMFAd in your Google Authenticator or Authy app, you either need to have admin privileges to the AWS account, or 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-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-to-clear-AWS-envvars.sh`, or set an alias, like so: `alias clearaws='source ~/awscli-mfa/source-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. + +### 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 + ``` -[coming soon] diff --git a/awscli-mfa/awscli-mfa.sh b/awscli-mfa/awscli-mfa.sh index b9bb77d..f6cf851 100755 --- a/awscli-mfa/awscli-mfa.sh +++ b/awscli-mfa/awscli-mfa.sh @@ -1,6 +1,7 @@ #!/usr/bin/env bash # todo: handle roles with MFA +# todo: handle root account max session time @3600 & warn if present DEBUG="false" # uncomment below to enable the debug output From e1156926a47d25201d94379e181f2ebe60ab17b0 Mon Sep 17 00:00:00 2001 From: Ville Walveranta Date: Tue, 10 Apr 2018 16:30:40 -0500 Subject: [PATCH 44/71] awscli-mfa documentation update (formatting) --- awscli-mfa/README.md | 274 ++++++++++++++++++++----------------------- 1 file changed, 129 insertions(+), 145 deletions(-) diff --git a/awscli-mfa/README.md b/awscli-mfa/README.md index fa5a4c6..67dff0f 100644 --- a/awscli-mfa/README.md +++ b/awscli-mfa/README.md @@ -11,212 +11,196 @@ First make sure you have `aws cli` installed. AWS has details for [Mac](https:// 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"` - + `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 -``` - + 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 '///'): +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'). + Executing this script as the AWS/IAM user 'mfa-test-user' (profile 'default'). -Please wait. + Please wait. -You have one configured profile: default (IAM: mfa-test-user) -.. but it doesn't have a virtual MFA device attached/enabled. + 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 + Do you want to attach/enable a vMFAd? Y/N -/// -/// ANSWERED 'Y' -/// + /// + /// ANSWERED 'Y' + /// -Preparing to enable the vMFAd for the profile... + Preparing to enable the vMFAd for the profile... -No available vMFAd found; creating new... + 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. + 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! + 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" + /// 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. + 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. + 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 + 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. + /// 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. + QRCode file deleted securely. -Enabling the newly created virtual MFA device: -arn:aws:iam::123456789123:mfa/mfa-test-user + 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. + 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 + >>> 923558 212566 -vMFAd successfully enabled for the profile 'default' (IAM user name 'mfa-test-user'). + 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! + You can now use the 'awscli-mfa.sh' script to start an MFA session for this profile! -``` +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 '///'): -3. If something goes wrong with the vMFAd activation process, the script gives a hopefully clear/obvious guidance.

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'). -``` -Executing this script as the AWS/IAM user 'mfa-test-user' (profile 'default'). + Please wait. -Please wait. + You have one configured profile: default (IAM: mfa-test-user) + .. its vMFAd is enabled + .. but no active persistent MFA sessions exist -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)? -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' + /// -/// -/// ANSWERED '1' -/// + Starting an MFA session.. + SELECTED PROFILE: default -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. -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 ->>> 764257 + Acquiring MFA session token for the profile: default... + MFA session token acquired. -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 -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. -/// 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'). -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. + /// 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 + MFA profile name: 'default-mfasession' + Region is set to: us-east-1 + Output format is set to: table -*** Make sure to export/unset all the new values as instructed above to - make sure no conflicting profile/secrets remain in the envrionment! + Do you want to export the selected profile's secrets to the environment (for s3cmd, etc)? - Y/[N] -*** 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 + /// 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. -*** To easily remove any all AWS profile settings and secrets information - from the environment, simply source the included script, like so: - source ./source-to-clear-AWS-envvars.sh + *** 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! -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_DEFAULT_REGION + unset AWS_DEFAULT_OUTPUT + unset AWS_SESSION_INIT_TIME + unset AWS_SESSION_DURATION + unset AWS_SESSION_TOKEN -~$ 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. + *** 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 -Now you can execute `mfastatus.sh` to view the remaining activity period on the MFA session: + *** To easily remove any all AWS profile settings and secrets information + from the environment, simply source the included script, like so: + source ./source-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! -ENVIRONMENT -=========== + ~$ 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 -ENVVAR 'AWS_PROFILE' SELECTING A PERSISTENT MFA SESSION (as below): default-mfasession + /// 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. +4. Now you can execute `mfastatus.sh` to view the remaining activity period on the MFA session: -PERSISTENT MFA SESSIONS (in /Users/ville/.aws/credentials) -========================================================== + ENVIRONMENT + =========== -MFA SESSION IDENT: default-mfasession (IAM user: 'mfa-test-user') - MFA SESSION REMAINING TO EXPIRATION: 08h:13m:48s + ENVVAR 'AWS_PROFILE' SELECTING A PERSISTENT MFA SESSION (as below): default-mfasession -NOTE: Execute 'awscli-mfa.sh' to renew/start a new MFA session, - or to select (switch to) an existing active MFA session. + 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 -``` -Finally, a sourceable `source-to-clear-AWS-envvars.sh` is provided to make it easy to clear out any any `AWS_*` envvars, like so: `source ./source-to-clear-AWS-envvars.sh`. This purges any secrets and/or references to persistent profiles from the local environment. + NOTE: Execute 'awscli-mfa.sh' to renew/start a new MFA session, + or to select (switch to) an existing active MFA session. +5. Finally, a sourceable `source-to-clear-AWS-envvars.sh` is provided to make it easy to clear out any any `AWS_*` envvars, like so: `source ./source-to-clear-AWS-envvars.sh`. This purges any secrets and/or references to persistent profiles from the local environment. ### Rationale From 11b13360d35c7c57883b1a63fa010424f25e2749 Mon Sep 17 00:00:00 2001 From: Ville Walveranta Date: Tue, 10 Apr 2018 17:37:52 -0500 Subject: [PATCH 45/71] awscli-mfa documentation update --- awscli-mfa/README.md | 81 ++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 79 insertions(+), 2 deletions(-) diff --git a/awscli-mfa/README.md b/awscli-mfa/README.md index 67dff0f..b09c228 100644 --- a/awscli-mfa/README.md +++ b/awscli-mfa/README.md @@ -80,6 +80,27 @@ First make sure you have `aws cli` installed. AWS has details for [Mac](https:// 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'). @@ -200,7 +221,63 @@ First make sure you have `aws cli` installed. AWS has details for [Mac](https:// NOTE: Execute 'awscli-mfa.sh' to renew/start a new MFA session, or to select (switch to) an existing active MFA session. -5. Finally, a sourceable `source-to-clear-AWS-envvars.sh` is provided to make it easy to clear out any any `AWS_*` envvars, like so: `source ./source-to-clear-AWS-envvars.sh`. This purges any secrets and/or references to persistent profiles from the local environment. +5. A sourceable `source-to-clear-AWS-envvars.sh` is provided to make it easy to clear out any any `AWS_*` envvars, like so: `source ./source-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. ### Rationale @@ -220,7 +297,7 @@ The scripts have been tested in macOS (High Sierra with stock bash 3.2.x) as wel * **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-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-to-clear-AWS-envvars.sh`, or set an alias, like so: `alias clearaws='source ~/awscli-mfa/source-to-clear-AWS-envvars.sh` +* **source-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-to-clear-AWS-envvars.sh`, or set an alias, like so: `alias clearaws='source ~/awscli-mfa/source-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. From 2e1e305428eccd60d1a62e27b91205123080b21e Mon Sep 17 00:00:00 2001 From: Ville Walveranta Date: Tue, 10 Apr 2018 20:15:50 -0500 Subject: [PATCH 46/71] awscli-mfa documentation edits --- awscli-mfa/README.md | 61 +++++++++++++++++++++++++++----------------- 1 file changed, 38 insertions(+), 23 deletions(-) diff --git a/awscli-mfa/README.md b/awscli-mfa/README.md index b09c228..716b773 100644 --- a/awscli-mfa/README.md +++ b/awscli-mfa/README.md @@ -5,6 +5,42 @@ The `awscli-mfa.sh` and its companion scripts `enable-disable-vmfa-device.sh` `m ### 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 possible 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 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 activity 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 register a virtual MFA device (vMFAd), or to start a session, a complex sequence 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 set 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) as well as with Linux (Ubuntu 16.04 with modern default bash 4.3.x). The only dependency is `aws cli`, 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 does not take command line arguments. 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 it is named the same as their IAM username (i.e. the serial number, Arn, 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 an active MFA session with that profile; if you no longer have access to the vMFAd in your Google Authenticator or Authy app, you either need to have admin privileges to the AWS account, or 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-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-to-clear-AWS-envvars.sh`, or set an alias, like so: `alias clearaws='source ~/awscli-mfa/source-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). @@ -279,28 +315,6 @@ First make sure you have `aws cli` installed. AWS has details for [Mac](https:// use them outside this script to disable the vMFAd for this profile. -### 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 register a virtual MFA device (vMFAd), or to start a session, a complex sequence 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 set 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) as well as with Linux (Ubuntu 16.04 with modern default bash 4.3.x). The only dependency is `aws cli`, 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 does not take command line arguments. 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 it is named the same as their IAM username (i.e. the serial number, Arn, 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 an active MFA session with that profile; if you no longer have access to the vMFAd in your Google Authenticator or Authy app, you either need to have admin privileges to the AWS account, or 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-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-to-clear-AWS-envvars.sh`, or set an alias, like so: `alias clearaws='source ~/awscli-mfa/source-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. - ### 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: @@ -326,5 +340,6 @@ Because the MFA session expiration time is encoded in the encrypted AWS session 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`. From 033af37b66d3bcc252b182c1990019b126fd4075 Mon Sep 17 00:00:00 2001 From: Ville Walveranta Date: Wed, 11 Apr 2018 03:33:00 -0500 Subject: [PATCH 47/71] Updated repository home README.md --- README.md | 12 ++---------- 1 file changed, 2 insertions(+), 10 deletions(-) diff --git a/README.md b/README.md index 822551a..b9fc9e7 100644 --- a/README.md +++ b/README.md @@ -3,17 +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/awscli-mfa.sh** - Makes it easy to use MFA sessions with AWS CLI. Multiple profiles are supported, but if only a simple profile 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, 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/). - -* **awscli-mfa/enable-disable-vmfa-device.sh** - Makes it very easy to enable/attach and disable/detach (as well as delete) a virtual MFA device (vMFAd). Assumes that each IAM user can have one vMFAd configured at a time, and that is named the same as their IAM username (i.e. `.../mfa/{IAMusername}` instead of `.../user/{IAMusername}`). Disabling a vMFAd requires an active MFA session with that profile. +* **[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/mfastatus.sh** - Displays the currently active MFA sessions and their remaining activity period. - -* **awscli-mfa/example-MFA-enforcement-policy.txt** - An example IAM policy to enforce MFA usage. A policy similar to this can be used in conjunction with these MFA management scripts. - -* **awscli-mfa/source-to-clear-AWS-envvars.sh** - Simple sourceable script that removes any AWS secrets/settings that may have been set in the local environment by `awscli-mfa.sh` script (above). Source this like so: `source awscli-mfa/source-to-clear-AWS-envvars.sh` +* **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. From 9b7939ee87e19c15ebe76f1de0e973f8bdfc7dc6 Mon Sep 17 00:00:00 2001 From: Ville Walveranta Date: Wed, 11 Apr 2018 13:48:25 -0500 Subject: [PATCH 48/71] Minor documentation edits. --- awscli-mfa/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/awscli-mfa/README.md b/awscli-mfa/README.md index 716b773..1e6c0e2 100644 --- a/awscli-mfa/README.md +++ b/awscli-mfa/README.md @@ -19,7 +19,7 @@ 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 register a virtual MFA device (vMFAd), or to start a session, a complex sequence 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 set 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. +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. From 3db6ac6ecd1f6b3f9738df45735aed791614d72a Mon Sep 17 00:00:00 2001 From: Ville Walveranta Date: Thu, 12 Apr 2018 01:41:05 -0500 Subject: [PATCH 49/71] Extended debug output in awscli-mfa.sh (now displays 'aws' command raw output with '--debug/-d' arg) --- awscli-mfa/awscli-mfa.sh | 72 ++++++++++++++++++++++++++++++++++------ 1 file changed, 61 insertions(+), 11 deletions(-) diff --git a/awscli-mfa/awscli-mfa.sh b/awscli-mfa/awscli-mfa.sh index f6cf851..c92b2e3 100755 --- a/awscli-mfa/awscli-mfa.sh +++ b/awscli-mfa/awscli-mfa.sh @@ -3,8 +3,12 @@ # todo: handle roles with MFA # todo: handle root account max session time @3600 & warn if present +# NOTE: Debugging mode prints the secrets on the screen! DEBUG="false" -# uncomment below to enable the debug output + +# 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 @@ -18,7 +22,7 @@ DEBUG="false" # 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 +# BY ADDING A "mfafsec" 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 @@ -111,6 +115,11 @@ On_ICyan='\033[0;106m' # Cyan On_IWhite='\033[0;107m' # White +# DEBUG MODE WARNING ========================================================= + +[[ "$DEBUG" == "true" ]] && echo -e "\\n${BIWhite}${On_Red} DEBUG MODE ACTIVE ${Color_Off}\\n\\n${BIRed}NOTE: Debug output includes secrets!!!${Color_Off}\\n\\n" + + # FUNCTIONS ================================================================== # `exists` for commands @@ -683,7 +692,10 @@ else # 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}result for 'aws configure get region --profile default':\\n${ICyan}$default_region\\n\\n" + default_output=$(aws configure get output --profile default) + [[ "$DEBUG" == "true" ]] && echo -e "\\n${Cyan}result for 'aws configure get output --profile default':\\n${ICyan}$default_output\\n\\n" if [[ "$default_region" == "" ]]; then echo @@ -698,9 +710,12 @@ else echo - [[ "$AWS_ACCESS_KEY_ID" != "" ]] && - current_aws_access_key_id="${AWS_ACCESS_KEY_ID}" || + 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" @@ -715,21 +730,23 @@ else fi process_user_arn="$(aws sts get-caller-identity --output text --query 'Arn' 2>&1)" + [[ "$DEBUG" == "true" ]] && echo -e "\\n${Cyan}result for: 'aws sts get-caller-identity --query 'Arn' --output text':\\n${ICyan}$process_user_arn${Color_Off}\\n\\n" [[ "$process_user_arn" =~ ([^/]+)$ ]] && process_username="${BASH_REMATCH[1]}" - if [[ "$process_username" =~ ExpiredToken ]]; then + if [[ "$process_user_arn" =~ ExpiredToken ]]; then continue_maybe "invalid" currently_selected_profile_ident="'default'" process_user_arn="$(aws sts get-caller-identity --output text --query 'Arn' 2>&1)" + [[ "$DEBUG" == "true" ]] && echo -e "\\n${Cyan}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 - if [[ "$process_username" =~ error ]]; then + if [[ "$process_user_arn" =~ error ]]; then echo -e "${BIRed}The selected profile is not functional${Color_Off}; please check the 'default' profile\\nin your '${CREDFILE}' file, and purge any 'AWS_' environment variables by executing\\n${Green}source ./source-to-clear-AWS-envvars.sh${Color_Off}" exit 1 else @@ -776,11 +793,15 @@ else # store this profile region and output format profile_region[$cred_profilecounter]=$(aws --profile "$profile_ident" configure get region) + [[ "$DEBUG" == "true" ]] && echo -e "\\n${Cyan}result for: 'aws --profile \"$profile_ident\" configure get region':\\n${ICyan}${profile_region[$cred_profilecounter]}${Color_Off}\\n\\n" profile_output[$cred_profilecounter]=$(aws --profile "$profile_ident" configure get output) + [[ "$DEBUG" == "true" ]] && echo -e "\\n${Cyan}result for: 'aws --profile \"$profile_output\" configure get output':\\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" --output text --query 'Arn' 2>&1)" + [[ "$DEBUG" == "true" ]] && echo -e "\\n${Cyan}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 @@ -811,6 +832,8 @@ else # however if MFA enforcement is set, this should produce # a reasonably reliable result) profile_check="$(aws iam get-user --output text --query "User.Arn" --profile "$profile_ident" 2>&1)" + [[ "$DEBUG" == "true" ]] && echo -e "\\n${Cyan}result for: 'aws iam get-user --output text --query \"User.Arn\" --profile \"$profile_ident\"':\\n${ICyan}$profile_check${Color_Off}\\n\\n" + if [[ "$profile_check" =~ ^arn:aws ]]; then cred_profile_status[$cred_profilecounter]="OK" else @@ -820,7 +843,13 @@ else # 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)" + 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}result for: 'aws iam list-mfa-devices --profile \"$profile_ident\" --user-name \"${cred_profile_user[$cred_profilecounter]}\" --output text --query \"MFADevices[].SerialNumber\"':\\n${ICyan}$mfa_arn${Color_Off}\\n\\n" + if [[ "$mfa_arn" =~ ^arn:aws ]]; then mfa_arns[$cred_profilecounter]="$mfa_arn" else @@ -851,6 +880,9 @@ else # no timestamp; legacy or initialized outside of this utility mfa_profile_check="$(aws iam get-user --output text --query "User.Arn" --profile "$mfa_profile_ident" 2>&1)" + + [[ "$DEBUG" == "true" ]] && echo -e "\\n${Cyan}result for: 'aws iam get-user --output text --query \"User.Arn\" --profile \"$mfa_profile_ident\"':\\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 @@ -869,7 +901,7 @@ else echo "USER ARN: ${cred_profile_arn[$cred_profilecounter]}" echo "USER NAME: ${cred_profile_user[$cred_profilecounter]}" echo "MFA ARN: ${mfa_arns[$cred_profilecounter]}" - echo "MFA MAXSEC: ${mfa_mfasec[$cred_profilecounter]}" + echo "MFA SESSION CUSTOM LENGTH (MFASEC): ${mfa_mfasec[$cred_profilecounter]}" if [[ "${mfa_profiles[$cred_profilecounter]}" == "" ]]; then echo "MFA PROFILE IDENT:" else @@ -1127,12 +1159,22 @@ else getDuration AWS_SESSION_DURATION "$AWS_USER_PROFILE" - read -r AWS_ACCESS_KEY_ID AWS_SECRET_ACCESS_KEY AWS_SESSION_TOKEN <<< \ - $(aws --profile "$AWS_USER_PROFILE" sts get-session-token \ + mfa_credentials=$(aws --profile "$AWS_USER_PROFILE" sts get-session-token \ --duration "$AWS_SESSION_DURATION" \ --serial-number "$ARN_OF_MFA" \ --token-code $mfacode \ - --output text | awk '{ print $2, $4, $5 }') + --output text) + + if [[ "$DEBUG" == "true" ]]; then + echo -e "\\n${Cyan}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${Color_Off}\\n\\n" + fi + + if [[ "$mfa_credentials" =~ error ]]; then + echo -e "${BIRed}An error occurred while attempting to acquire the MFA session credentials; cannot continue!${Color_Off};\\nRun the script with '--debug' argument to diagnose the problem." + exit 1 + else + read -r AWS_ACCESS_KEY_ID AWS_SECRET_ACCESS_KEY AWS_SESSION_TOKEN <<< $(printf '%s' "$mfa_credentials" | awk '{ print $2, $4, $5 }') + fi if [ -z "$AWS_ACCESS_KEY_ID" ]; then echo @@ -1196,7 +1238,10 @@ else # get region and output format for the selected profile AWS_DEFAULT_REGION=$(aws configure get region --profile "${final_selection}") + [[ "$DEBUG" == "true" ]] && echo -e "\\n${Cyan}result for: 'aws configure get region --profile \"${final_selection}\"':\\n${ICyan}$AWS_DEFAULT_REGION${Color_Off}\\n\\n" + AWS_DEFAULT_OUTPUT=$(aws configure get output --profile "${final_selection}") + [[ "$DEBUG" == "true" ]] && echo -e "\\n${Cyan}result for: 'aws configure get output --profile \"${final_selection}\"':\\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 @@ -1244,10 +1289,15 @@ else 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}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}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}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}" From 2e1f5961edfa2566bc9df78000680d31a802550d Mon Sep 17 00:00:00 2001 From: Ville Walveranta Date: Sun, 15 Apr 2018 02:11:51 -0500 Subject: [PATCH 50/71] Added debugging to enable-disable-vmfa-device.sh; other cleanup, minor fixes. --- awscli-mfa/awscli-mfa.sh | 59 ++++++------ awscli-mfa/enable-disable-vmfa-device.sh | 111 +++++++++++++++++------ 2 files changed, 112 insertions(+), 58 deletions(-) diff --git a/awscli-mfa/awscli-mfa.sh b/awscli-mfa/awscli-mfa.sh index c92b2e3..9eeea3a 100755 --- a/awscli-mfa/awscli-mfa.sh +++ b/awscli-mfa/awscli-mfa.sh @@ -117,7 +117,7 @@ On_IWhite='\033[0;107m' # White # DEBUG MODE WARNING ========================================================= -[[ "$DEBUG" == "true" ]] && echo -e "\\n${BIWhite}${On_Red} DEBUG MODE ACTIVE ${Color_Off}\\n\\n${BIRed}NOTE: Debug output includes secrets!!!${Color_Off}\\n\\n" +[[ "$DEBUG" == "true" ]] && echo -e "\\n${BIWhite}${On_Red} DEBUG MODE ACTIVE ${Color_Off}\\n\\n${BIRed}NOTE: Debug output may include secrets!!!${Color_Off}\\n\\n" # FUNCTIONS ================================================================== @@ -692,10 +692,10 @@ else # 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}result for 'aws configure get region --profile default':\\n${ICyan}$default_region\\n\\n" + [[ "$DEBUG" == "true" ]] && echo -e "\\n${Cyan}result for 'aws configure get region --profile default':\\n${ICyan}${default_region}\\n\\n" default_output=$(aws configure get output --profile default) - [[ "$DEBUG" == "true" ]] && echo -e "\\n${Cyan}result for 'aws configure get output --profile default':\\n${ICyan}$default_output\\n\\n" + [[ "$DEBUG" == "true" ]] && echo -e "\\n${Cyan}result for 'aws configure get output --profile default':\\n${ICyan}${default_output}\\n\\n" if [[ "$default_region" == "" ]]; then echo @@ -714,7 +714,7 @@ else 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" + [[ "$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" @@ -730,7 +730,7 @@ else fi process_user_arn="$(aws sts get-caller-identity --output text --query 'Arn' 2>&1)" - [[ "$DEBUG" == "true" ]] && echo -e "\\n${Cyan}result for: 'aws sts get-caller-identity --query 'Arn' --output text':\\n${ICyan}$process_user_arn${Color_Off}\\n\\n" + [[ "$DEBUG" == "true" ]] && echo -e "\\n${Cyan}result for: 'aws sts get-caller-identity --query 'Arn' --output text':\\n${ICyan}${process_user_arn}${Color_Off}\\n\\n" [[ "$process_user_arn" =~ ([^/]+)$ ]] && process_username="${BASH_REMATCH[1]}" @@ -740,7 +740,7 @@ else currently_selected_profile_ident="'default'" process_user_arn="$(aws sts get-caller-identity --output text --query 'Arn' 2>&1)" - [[ "$DEBUG" == "true" ]] && echo -e "\\n${Cyan}result for: 'aws sts get-caller-identity --query 'Arn' --output text' \\(after profile reset\\):\\n${ICyan}$process_user_arn${Color_Off}\\n\\n" + [[ "$DEBUG" == "true" ]] && echo -e "\\n${Cyan}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]}" @@ -792,15 +792,15 @@ else cred_profiles[$cred_profilecounter]="$profile_ident" # store this profile region and output format - profile_region[$cred_profilecounter]=$(aws --profile "$profile_ident" configure get region) - [[ "$DEBUG" == "true" ]] && echo -e "\\n${Cyan}result for: 'aws --profile \"$profile_ident\" configure get region':\\n${ICyan}${profile_region[$cred_profilecounter]}${Color_Off}\\n\\n" + profile_region[$cred_profilecounter]=$(aws configure get region --profile "$profile_ident") + [[ "$DEBUG" == "true" ]] && echo -e "\\n${Cyan}result for: 'aws configure get region --profile \"$profile_ident\"':\\n${ICyan}${profile_region[$cred_profilecounter]}${Color_Off}\\n\\n" profile_output[$cred_profilecounter]=$(aws --profile "$profile_ident" configure get output) - [[ "$DEBUG" == "true" ]] && echo -e "\\n${Cyan}result for: 'aws --profile \"$profile_output\" configure get output':\\n${ICyan}${profile_output[$cred_profilecounter]}${Color_Off}\\n\\n" + [[ "$DEBUG" == "true" ]] && echo -e "\\n${Cyan}result for: 'aws configure get output --profile \"$profile_output\"':\\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" --output text --query 'Arn' 2>&1)" - [[ "$DEBUG" == "true" ]] && echo -e "\\n${Cyan}result for: 'aws sts get-caller-identity --profile \"$profile_ident\" --query 'Arn' --output text':\\n${ICyan}$user_arn${Color_Off}\\n\\n" + [[ "$DEBUG" == "true" ]] && echo -e "\\n${Cyan}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 @@ -831,8 +831,8 @@ else # (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 --output text --query "User.Arn" --profile "$profile_ident" 2>&1)" - [[ "$DEBUG" == "true" ]] && echo -e "\\n${Cyan}result for: 'aws iam get-user --output text --query \"User.Arn\" --profile \"$profile_ident\"':\\n${ICyan}$profile_check${Color_Off}\\n\\n" + profile_check="$(aws iam get-user --profile "$profile_ident" --query 'User.Arn' --output text 2>&1)" + [[ "$DEBUG" == "true" ]] && echo -e "\\n${Cyan}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" @@ -843,12 +843,13 @@ else # 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" \ + mfa_arn="$(aws iam list-mfa-devices \ + --profile "$profile_ident" \ --user-name "${cred_profile_user[$cred_profilecounter]}" \ --output text \ - --query "MFADevices[].SerialNumber" 2>&1)" + --query 'MFADevices[].SerialNumber' 2>&1)" - [[ "$DEBUG" == "true" ]] && echo -e "\\n${Cyan}result for: 'aws iam list-mfa-devices --profile \"$profile_ident\" --user-name \"${cred_profile_user[$cred_profilecounter]}\" --output text --query \"MFADevices[].SerialNumber\"':\\n${ICyan}$mfa_arn${Color_Off}\\n\\n" + [[ "$DEBUG" == "true" ]] && echo -e "\\n${Cyan}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" @@ -879,9 +880,8 @@ else elif [[ ${_ret_remaining} -eq -1 ]]; then # no timestamp; legacy or initialized outside of this utility - mfa_profile_check="$(aws iam get-user --output text --query "User.Arn" --profile "$mfa_profile_ident" 2>&1)" - - [[ "$DEBUG" == "true" ]] && echo -e "\\n${Cyan}result for: 'aws iam get-user --output text --query \"User.Arn\" --profile \"$mfa_profile_ident\"':\\n${ICyan}$mfa_profile_check${Color_Off}\\n\\n" + mfa_profile_check="$(aws iam get-user --profile "$mfa_profile_ident" --query 'User.Arn' --output text 2>&1)" + [[ "$DEBUG" == "true" ]] && echo -e "\\n${Cyan}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" @@ -1159,14 +1159,15 @@ else getDuration AWS_SESSION_DURATION "$AWS_USER_PROFILE" - mfa_credentials=$(aws --profile "$AWS_USER_PROFILE" sts get-session-token \ - --duration "$AWS_SESSION_DURATION" \ - --serial-number "$ARN_OF_MFA" \ - --token-code $mfacode \ - --output text) + mfa_credentials=$(aws sts get-session-token \ + --profile "$AWS_USER_PROFILE" \ + --duration "$AWS_SESSION_DURATION" \ + --serial-number "$ARN_OF_MFA" \ + --token-code $mfacode \ + --output text) if [[ "$DEBUG" == "true" ]]; then - echo -e "\\n${Cyan}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${Color_Off}\\n\\n" + echo -e "\\n${Cyan}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}${Color_Off}\\n\\n" fi if [[ "$mfa_credentials" =~ error ]]; then @@ -1238,10 +1239,10 @@ else # get region and output format for the selected profile AWS_DEFAULT_REGION=$(aws configure get region --profile "${final_selection}") - [[ "$DEBUG" == "true" ]] && echo -e "\\n${Cyan}result for: 'aws configure get region --profile \"${final_selection}\"':\\n${ICyan}$AWS_DEFAULT_REGION${Color_Off}\\n\\n" + [[ "$DEBUG" == "true" ]] && echo -e "\\n${Cyan}result for: 'aws configure get region --profile \"${final_selection}\"':\\n${ICyan}${AWS_DEFAULT_REGION}${Color_Off}\\n\\n" AWS_DEFAULT_OUTPUT=$(aws configure get output --profile "${final_selection}") - [[ "$DEBUG" == "true" ]] && echo -e "\\n${Cyan}result for: 'aws configure get output --profile \"${final_selection}\"':\\n${ICyan}$AWS_DEFAULT_OUTPUT${Color_Off}\\n\\n" + [[ "$DEBUG" == "true" ]] && echo -e "\\n${Cyan}result for: 'aws configure get output --profile \"${final_selection}\"':\\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 @@ -1289,14 +1290,14 @@ else 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}result for: 'aws configure --profile \"${final_selection}\" get aws_access_key_id':\\n${ICyan}$AWS_ACCESS_KEY_ID${Color_Off}\\n\\n" + [[ "$DEBUG" == "true" ]] && echo -e "\\n${Cyan}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}result for: 'aws configure --profile \"${final_selection}\" get aws_access_key_id':\\n${ICyan}$AWS_SECRET_ACCESS_KEY${Color_Off}\\n\\n" + [[ "$DEBUG" == "true" ]] && echo -e "\\n${Cyan}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}result for: 'aws configure --profile \"${final_selection}\" get aws_session_token':\\n${ICyan}$AWS_SESSION_TOKEN${Color_Off}\\n\\n" + [[ "$DEBUG" == "true" ]] && echo -e "\\n${Cyan}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} diff --git a/awscli-mfa/enable-disable-vmfa-device.sh b/awscli-mfa/enable-disable-vmfa-device.sh index 57ae8a2..17b305c 100755 --- a/awscli-mfa/enable-disable-vmfa-device.sh +++ b/awscli-mfa/enable-disable-vmfa-device.sh @@ -2,8 +2,12 @@ # todo: handle roles with MFA +# NOTE: Debugging mode prints the secrets on the screen! DEBUG="false" -# uncomment below to enable the debug output + +# 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 @@ -110,6 +114,11 @@ On_ICyan='\033[0;106m' # Cyan On_IWhite='\033[0;107m' # White +# DEBUG MODE WARNING ========================================================= + +[[ "$DEBUG" == "true" ]] && echo -e "\\n${BIWhite}${On_Red} DEBUG MODE ACTIVE ${Color_Off}\\n\\n${BIRed}NOTE: Debug output may include secrets!!!${Color_Off}\\n\\n" + + # FUNCTIONS ================================================================== # `exists` for commands @@ -647,7 +656,10 @@ else # 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}result for 'aws configure get region --profile default':\\n${ICyan}${default_region}\\n\\n" + default_output=$(aws configure get output --profile default) + [[ "$DEBUG" == "true" ]] && echo -e "\\n${Cyan}result for 'aws configure get output --profile default':\\n${ICyan}${default_output}\\n\\n" if [[ "$default_region" == "" ]]; then echo @@ -662,9 +674,12 @@ else echo - [[ "$AWS_ACCESS_KEY_ID" != "" ]] && - current_aws_access_key_id="${AWS_ACCESS_KEY_ID}" || + 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" @@ -678,26 +693,28 @@ else fi fi - process_user_arn="$(aws sts get-caller-identity --output text --query 'Arn' 2>&1)" + process_user_arn="$(aws sts get-caller-identity --query 'Arn' --output text 2>&1)" + [[ "$DEBUG" == "true" ]] && echo -e "\\n${Cyan}result for: 'aws sts get-caller-identity --query 'Arn' --output text':\\n${ICyan}$process_user_arn}${Color_Off}\\n\\n" [[ "$process_user_arn" =~ ([^/]+)$ ]] && process_username="${BASH_REMATCH[1]}" - if [[ "$process_username" =~ ExpiredToken ]]; then + if [[ "$process_user_arn" =~ ExpiredToken ]]; then continue_maybe "invalid" - currently_selected_profile_ident="\"default\"" - process_user_arn="$(aws sts get-caller-identity --output text --query 'Arn' 2>&1)" + currently_selected_profile_ident="'default'" + process_user_arn="$(aws sts get-caller-identity --query 'Arn' --output text 2>&1)" + [[ "$DEBUG" == "true" ]] && echo -e "\\n${Cyan}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 - if [[ "$process_username" =~ error ]]; then + if [[ "$process_user_arn" =~ error ]]; then echo -e "${BIRed}The selected profile is not functional${Color_Off}; please check the 'default' profile\\nin your '${CREDFILE}' file, and purge any 'AWS_' environment variables by executing\\n${Green}source ./source-to-clear-AWS-envvars.sh${Color_Off}" exit 1 else - echo "Executing this script as the AWS/IAM user '${process_username}' (profile ${currently_selected_profile_ident})." + echo "Executing this script as the AWS/IAM user '$process_username' (profile $currently_selected_profile_ident)." fi echo @@ -739,12 +756,16 @@ else cred_profiles[$cred_profilecounter]="$profile_ident" # store this profile region and output format - profile_region[$cred_profilecounter]=$(aws --profile "$profile_ident" configure get region) - profile_output[$cred_profilecounter]=$(aws --profile "$profile_ident" configure get output) + profile_region[$cred_profilecounter]=$(aws configure get region --profile "$profile_ident") + [[ "$DEBUG" == "true" ]] && echo -e "\\n${Cyan}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}result for: 'aws configure get output --profile \"$profile_output\"':\\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" --output text --query 'Arn' 2>&1)" + user_arn="$(aws sts get-caller-identity --profile "$profile_ident" --query 'Arn' --output text 2>&1)" + [[ "$DEBUG" == "true" ]] && echo -e "\\n${Cyan}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 @@ -774,7 +795,9 @@ else # (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 --output text --query "User.Arn" --profile "$profile_ident" 2>&1)" + profile_check="$(aws iam get-user --profile "$profile_ident" --query 'User.Arn' --output text 2>&1)" + [[ "$DEBUG" == "true" ]] && echo -e "\\n${Cyan}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 @@ -788,7 +811,9 @@ else --profile "$profile_ident" \ --user-name "${cred_profile_user[$cred_profilecounter]}" \ --output text \ - --query "MFADevices[].SerialNumber" 2>&1)" + --query 'MFADevices[].SerialNumber' 2>&1)" + + [[ "$DEBUG" == "true" ]] && echo -e "\\n${Cyan}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" @@ -819,7 +844,10 @@ else elif [[ ${_ret_remaining} -eq -1 ]]; then # no timestamp; legacy or initialized outside of this utility - mfa_profile_check="$(aws iam get-user --output text --query "User.Arn" --profile "$mfa_profile_ident" 2>&1)" + mfa_profile_check="$(aws iam get-user --profile "$mfa_profile_ident" --query 'User.Arn' --output text 2>&1)" + + [[ "$DEBUG" == "true" ]] && echo -e "\\n${Cyan}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 @@ -838,7 +866,7 @@ else echo "USER ARN: ${cred_profile_arn[$cred_profilecounter]}" echo "USER NAME: ${cred_profile_user[$cred_profilecounter]}" echo "MFA ARN: ${mfa_arns[$cred_profilecounter]}" - echo "MFA MAXSEC: ${mfa_mfasec[$cred_profilecounter]}" + echo "MFA SESSION CUSTOM LENGTH (MFASEC): ${mfa_mfasec[$cred_profilecounter]}" if [[ "${mfa_profiles[$cred_profilecounter]}" == "" ]]; then echo "MFA PROFILE IDENT:" else @@ -1003,12 +1031,16 @@ else exit 1 fi - available_user_vmfad=$(aws --profile "${final_selection}" \ - iam list-virtual-mfa-devices \ + 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}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 ]]; then echo -e "${BIRed}Could not execute list-virtual-mfa-devices. Cannot continue.${Color_Off}" @@ -1027,10 +1059,14 @@ else if [[ $REPLY =~ ^[Yy]$ ]]; then break; elif [[ $REPLY =~ ^[Nn]$ ]]; then - mfa_deletion_result=$(aws --profile "${final_selection}" \ - iam delete-virtual-mfa-device \ + 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}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 + if [[ "$mfa_deletion_result" =~ error ]]; then echo echo -e "${BIRed}Could not delete inaccessible vMFAd. Cannot continue.${Color_Off}" @@ -1066,12 +1102,16 @@ else echo echo "No available vMFAd found; creating new..." echo - vmfad_creation_status=$(aws --profile "${final_selection}" \ - iam create-virtual-mfa-device \ + 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}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 + if [[ "$vmfad_creation_status" =~ error ]]; then echo -e "${BIRed}Could not execute create-virtual-mfa-device.\\nNo virtual MFA device to enable. Cannot continue.${Color_Off}" echo @@ -1109,12 +1149,16 @@ else done echo - available_user_vmfad=$(aws --profile "${final_selection}" \ - iam list-virtual-mfa-devices \ + 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}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 + if [[ "$available_user_vmfad" =~ error ]]; then echo -e "${BIRed}Could not execute list-virtual-mfa-devices. Cannot continue.${Color_Off}" exit 1 @@ -1148,15 +1192,19 @@ else echo - vmfad_enablement_status=$(aws --profile "${final_selection}" \ - iam enable-mfa-device \ + 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}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 + if [[ "$vmfad_enablement_status" =~ error ]]; then - echo -e "${BIRed}Could not enable vMFAd. Cannot continue.${Color_Off}" + echo -e "${BIRed}Could not enable vMFAd. Cannot continue.\\n${Red}Mistyped authcodes, or wrong/old vMFAd?${Color_Off}" exit 1 else echo -e "${BIGreen}vMFAd successfully enabled for the profile '${final_selection}' ${Green}(IAM user name '$aws_iam_user').${Color_Off}" @@ -1168,6 +1216,7 @@ else echo -e "disable the vMFAd for the profile...\\n" transient_mfa_profile_check="$(aws sts get-caller-identity --output text --query 'Arn' 2>&1)" + [[ "$DEBUG" == "true" ]] && echo -e "\\n${Cyan}result for: 'aws sts get-caller-identity --query 'Arn' --output text':\\n${ICyan}${transient_mfa_profile_check}${Color_Off}\\n\\n" if [[ "$transient_mfa_profile_check" =~ ^arn:aws:iam::([[:digit:]]*):user/(.*)$ ]]; then aws_account_id="${BASH_REMATCH[1]}" # this AWS account @@ -1236,6 +1285,8 @@ else --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}result for: 'aws iam deactivate-mfa-device --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" + if [[ "$vmfad_deactivation_result" =~ error ]]; then echo -e "${BIRed}Could not disable/detach vMFAd for the profile '${final_selection}'. Cannot continue.${Color_Off}" exit 1 @@ -1249,10 +1300,12 @@ else do read -s -n 1 -r if [[ $REPLY =~ ^[Yy]$ ]]; then - vmfad_delete_result=$(aws --profile "${final_selection}" \ - iam delete-virtual-mfa-device \ + 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}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" + if [[ "$vmfad_delete_result" =~ error ]]; then echo -e "\\n${BIRed}Could not delete vMFAd for the profile '${final_selection}'. Cannot continue.${Color_Off}" exit 1 From fd01e8c4b41e3fed33ee0cc069b437348859c6c6 Mon Sep 17 00:00:00 2001 From: Ville Walveranta Date: Sun, 15 Apr 2018 02:20:21 -0500 Subject: [PATCH 51/71] Updated documentation for awscli-mfa --- awscli-mfa/README.md | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/awscli-mfa/README.md b/awscli-mfa/README.md index 1e6c0e2..26210e0 100644 --- a/awscli-mfa/README.md +++ b/awscli-mfa/README.md @@ -29,7 +29,7 @@ These scripts provide significant interactive guidance as well as user-friendly The scripts have been tested in macOS (High Sierra with stock bash 3.2.x) as well as with Linux (Ubuntu 16.04 with modern default bash 4.3.x). The only dependency is `aws cli`, 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 does not take command line arguments. 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. +* **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 `-d` / `--debug` 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 it is named the same as their IAM username (i.e. the serial number, Arn, 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 an active MFA session with that profile; if you no longer have access to the vMFAd in your Google Authenticator or Authy app, you either need to have admin privileges to the AWS account, or 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. @@ -343,3 +343,7 @@ Because the MFA session expiration time is encoded in the encrypted AWS session ### 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! From 33377304c446086330923b6da339eac0d67638ca Mon Sep 17 00:00:00 2001 From: Ville Walveranta Date: Mon, 16 Apr 2018 13:34:17 -0400 Subject: [PATCH 52/71] Documentation update for awscli-mfa.sh --- awscli-mfa/README.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/awscli-mfa/README.md b/awscli-mfa/README.md index 26210e0..3abf964 100644 --- a/awscli-mfa/README.md +++ b/awscli-mfa/README.md @@ -239,6 +239,10 @@ First make sure you have `aws cli` installed. AWS has details for [Mac](https:// /// 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 From cdfaf843e015f57098b71d78b8f918d6363ec2ca Mon Sep 17 00:00:00 2001 From: Ville Walveranta Date: Tue, 17 Apr 2018 20:07:22 -0400 Subject: [PATCH 53/71] awscli-mfa.sh: improved AWS error checking; fixed light colors on white background (forced black background) --- awscli-mfa/awscli-mfa.sh | 230 ++++++++++++++++++++++++--------------- 1 file changed, 143 insertions(+), 87 deletions(-) diff --git a/awscli-mfa/awscli-mfa.sh b/awscli-mfa/awscli-mfa.sh index 9eeea3a..88ec7b2 100755 --- a/awscli-mfa/awscli-mfa.sh +++ b/awscli-mfa/awscli-mfa.sh @@ -114,10 +114,9 @@ On_IPurple='\033[0;105m' # Purple On_ICyan='\033[0;106m' # Cyan On_IWhite='\033[0;107m' # White - # DEBUG MODE WARNING ========================================================= -[[ "$DEBUG" == "true" ]] && echo -e "\\n${BIWhite}${On_Red} DEBUG MODE ACTIVE ${Color_Off}\\n\\n${BIRed}NOTE: Debug output may include secrets!!!${Color_Off}\\n\\n" +[[ "$DEBUG" == "true" ]] && 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" # FUNCTIONS ================================================================== @@ -427,12 +426,12 @@ continue_maybe() { if [[ "$already_failed" == "false" ]]; then if [[ "${failtype}" == "expired" ]]; then - echo -e "\\n${BIRed}THE MFA SESSION SELECTED/CONFIGURED IN THE ENVIRONMENT HAS EXPIRED.${Color_Off}\\n" + echo -e "\\n${BIRed}${On_Black}THE MFA SESSION SELECTED/CONFIGURED IN THE ENVIRONMENT HAS EXPIRED.${Color_Off}\\n" else - echo -e "\\n${BIRed}THE AWS PROFILE SELECTED/CONFIGURED IN THE ENVIRONMENT IS INVALID.${Color_Off}\\n" + 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}Do you want to continue with the default profile?${Color_Off} - ${BIWhite}[Y]${Color_Off}/N ")" -n 1 -r + 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 @@ -471,6 +470,68 @@ continue_maybe() { fi } +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 +} + ## PREREQUISITES CHECK # is AWS CLI installed? @@ -489,7 +550,7 @@ if [[ "$AWS_CONFIG_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." + 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 @@ -499,20 +560,20 @@ if [[ "$AWS_CONFIG_FILE" != "" ]] && active_config_file=$AWS_CONFIG_FILE echo - echo -e "${BIWhite}** NOTE: A custom configuration file defined with AWS_CONFIG_FILE envvar in effect: '$AWS_CONFIG_FILE'${Color_Off}" + 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}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." + 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}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." + 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 @@ -522,20 +583,20 @@ if [[ "$AWS_SHARED_CREDENTIALS_FILE" != "" ]] && active_credentials_file=$AWS_SHARED_CREDENTIALS_FILE echo - echo -e "${BIWhite}** NOTE: A custom credentials file defined with AWS_SHARED_CREDENTIALS_FILE envvar in effect: '$AWS_SHARED_CREDENTIALS_FILE'${Color_Off}" + 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}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." + 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}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." + 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 @@ -561,7 +622,7 @@ done < "$CREDFILE" if [[ "$ONEPROFILE" == "false" ]]; then echo - echo -e "${BIRed}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 -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 @@ -692,14 +753,14 @@ else # 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}result for 'aws configure get region --profile default':\\n${ICyan}${default_region}\\n\\n" + [[ "$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}result for 'aws configure get output --profile default':\\n${ICyan}${default_output}\\n\\n" + [[ "$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}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 -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 @@ -714,46 +775,43 @@ else 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" + [[ "$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 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}' [transient]" + currently_selected_profile_ident_printable="'${PRECHECK_AWS_PROFILE}' [transient]" else - currently_selected_profile_ident="unknown/transient" + 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}result for: 'aws sts get-caller-identity --query 'Arn' --output text':\\n${ICyan}${process_user_arn}${Color_Off}\\n\\n" + [[ "$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" [[ "$process_user_arn" =~ ([^/]+)$ ]] && process_username="${BASH_REMATCH[1]}" - if [[ "$process_user_arn" =~ ExpiredToken ]]; then + # prompt to switch to default on any error + if [[ "$process_user_arn" =~ 'error occurred' ]]; then continue_maybe "invalid" - currently_selected_profile_ident="'default'" + 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}result for: 'aws sts get-caller-identity --query 'Arn' --output text' \\(after profile reset\\):\\n${ICyan}${process_user_arn}${Color_Off}\\n\\n" + [[ "$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 - if [[ "$process_user_arn" =~ error ]]; then - echo -e "${BIRed}The selected profile is not functional${Color_Off}; please check the 'default' profile\\nin your '${CREDFILE}' file, and purge any 'AWS_' environment variables by executing\\n${Green}source ./source-to-clear-AWS-envvars.sh${Color_Off}" - exit 1 - else - echo "Executing this script as the AWS/IAM user '$process_username' (profile $currently_selected_profile_ident)." - fi + # this bails out on errors + checkAWSErrors "true" "$process_user_arn" "$currently_selected_profile_ident_printable" - echo + echo -e "Executing this script as the AWS/IAM user '$process_username' (profile $currently_selected_profile_ident_printable).\\n" # declare the arrays for credentials loop declare -a cred_profiles @@ -768,7 +826,7 @@ else declare -a mfa_mfasec cred_profilecounter=0 - echo -ne "${BIWhite}Please wait" + echo -ne "${BIWhite}${On_Black}Please wait" # read the credentials file while IFS='' read -r line || [[ -n "$line" ]]; do @@ -793,14 +851,14 @@ else # store this profile region and output format profile_region[$cred_profilecounter]=$(aws configure get region --profile "$profile_ident") - [[ "$DEBUG" == "true" ]] && echo -e "\\n${Cyan}result for: 'aws configure get region --profile \"$profile_ident\"':\\n${ICyan}${profile_region[$cred_profilecounter]}${Color_Off}\\n\\n" + [[ "$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 --profile "$profile_ident" configure get output) - [[ "$DEBUG" == "true" ]] && echo -e "\\n${Cyan}result for: 'aws configure get output --profile \"$profile_output\"':\\n${ICyan}${profile_output[$cred_profilecounter]}${Color_Off}\\n\\n" + [[ "$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" --output text --query 'Arn' 2>&1)" - [[ "$DEBUG" == "true" ]] && echo -e "\\n${Cyan}result for: 'aws sts get-caller-identity --profile \"$profile_ident\" --query 'Arn' --output text':\\n${ICyan}${user_arn}${Color_Off}\\n\\n" + [[ "$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 @@ -813,7 +871,7 @@ else # (may be different from the arbitrary profile ident) [[ "$user_arn" =~ ([^/]+)$ ]] && profile_username="${BASH_REMATCH[1]}" - if [[ "$profile_username" =~ error ]]; then + if [[ "$profile_username" =~ 'error occurred' ]]; then cred_profile_user[$cred_profilecounter]="" else cred_profile_user[$cred_profilecounter]="$profile_username" @@ -832,7 +890,7 @@ else # 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}result for: 'aws iam get-user --profile \"$profile_ident\" --query 'User.Arn' --output text':\\n${ICyan}${profile_check}${Color_Off}\\n\\n" + [[ "$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" @@ -849,7 +907,7 @@ else --output text \ --query 'MFADevices[].SerialNumber' 2>&1)" - [[ "$DEBUG" == "true" ]] && echo -e "\\n${Cyan}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" + [[ "$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" @@ -881,7 +939,7 @@ else # 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}result for: 'aws iam get-user --profile \"$mfa_profile_ident\" --query 'User.Arn' --output text':\\n${ICyan}${mfa_profile_check}${Color_Off}\\n\\n" + [[ "$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" @@ -933,7 +991,7 @@ else if [[ ${#cred_profiles[@]} == 1 ]]; then echo [[ "${cred_profile_user[0]}" != "" ]] && prcpu="${cred_profile_user[0]}" || prcpu="unknown -- a bad profile?" - echo -e "${Green}You have one configured profile: ${BIGreen}${cred_profiles[0]} ${Green}(IAM: ${prcpu})${Color_Off}" + echo -e "${Green}${On_Black}You have one configured profile: ${BIGreen}${cred_profiles[0]} ${Green}(IAM: ${prcpu})${Color_Off}" mfa_session_status="false" if [[ "${mfa_arns[0]}" != "" ]]; then @@ -942,23 +1000,23 @@ else if [[ "${mfa_profile_status[0]}" != "EXPIRED" && "${mfa_profile_status[0]}" != "" ]]; then - echo -e ".. and it ${BIWhite}has an active MFA session with ${mfa_profile_status[0]}${Color_Off}" + echo -e ".. and it ${BIWhite}${On_Black}has an active MFA session with ${mfa_profile_status[0]}${Color_Off}" mfa_session_status="true" else echo -e ".. but no active persistent MFA sessions exist" fi else - echo -e "${BIRed}.. 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 -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}1${Color_Off}: Start/renew an MFA session for the profile mentioned above?" - echo -e "${BIWhite}2${Color_Off}: Use the above profile as-is (without MFA)?" - [[ "${mfa_session_status}" == "true" ]] && echo -e "${BIWhite}3${Color_Off}: Resume the existing active MFA session (${mfa_profile_status[0]})?" + 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 (${mfa_profile_status[0]})?" echo while : do @@ -1001,16 +1059,16 @@ else for i in "${cred_profiles[@]}" do if [[ "${mfa_arns[$SELECTR]}" != "" ]]; then - mfa_notify="; ${Green}vMFAd enabled${Color_Off}" + mfa_notify="; ${Green}${On_Black}vMFAd enabled${Color_Off}" else mfa_notify="; vMFAd not configured" fi [[ "${cred_profile_user[$SELECTR]}" != "" ]] && prcpu="${cred_profile_user[$SELECTR]}" || prcpu="unknown -- a bad profile?" - echo -en "${BIWhite}${ITER}: $i${Color_Off} (IAM: ${prcpu}${mfa_notify})\\n" + echo -en "${BIWhite}${On_Black}${ITER}: $i${Color_Off} (IAM: ${prcpu}${mfa_notify})\\n" if [[ "${mfa_profile_status[$SELECTR]}" != "EXPIRED" && "${mfa_profile_status[$SELECTR]}" != "" ]]; then - echo -e "${BIWhite}${ITER}m: $i MFA profile${Color_Off} (${mfa_profile_status[$SELECTR]})" + echo -e "${BIWhite}${On_Black}${ITER}m: $i MFA profile${Color_Off} (${mfa_profile_status[$SELECTR]})" fi echo @@ -1026,9 +1084,9 @@ else # 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}SELECT A PROFILE BY THE ID: " + echo -en "\\n${BIWhite}${On_Black}SELECT A PROFILE BY THE ID:${Color_Off} " read -r selprofile - echo -en "\\n${Color_Off}" + echo -en "\\n" fi # end profile selection @@ -1077,7 +1135,7 @@ else elif [[ "$selprofile_mfa_check" != "" && "${mfa_profile_status[$actual_selprofile]}" == "" ]]; then # mfa ('m') profile was selected for a profile that no mfa profile exists - echo -e "${BIRed}There is no profile '${selprofile}'.${Color_Off}" + echo -e "${BIRed}${On_Black}There is no profile '${selprofile}'.${Color_Off}" echo exit 1 @@ -1096,13 +1154,13 @@ else else # no numeric part in selection - echo -e "${BIRed}There is no profile '${selprofile}'.${Color_Off}" + echo -e "${BIRed}${On_Black}There is no profile '${selprofile}'.${Color_Off}" echo exit 1 fi else # empty selection - echo -e "${BIRed}There is no profile '${selprofile}'.${Color_Off}" + echo -e "${BIRed}${On_Black}There is no profile '${selprofile}'.${Color_Off}" echo exit 1 fi @@ -1114,16 +1172,16 @@ else # prompt for the MFA code echo - echo -e "${BIWhite}Enter the current MFA one time pass code for the profile '${cred_profiles[$actual_selprofile]}'${Color_Off} to start/renew an MFA session," + echo -e "${BIWhite}${On_Black}Enter the current MFA one time pass code for the profile '${cred_profiles[$actual_selprofile]}'${Color_Off} to start/renew an MFA session," echo "or leave empty (just press [ENTER]) to use the selected profile without the MFA." echo while : do - echo -en "${BIWhite}" + echo -en "${BIWhite}${On_Black}" read -p ">>> " -r mfacode echo -en "${Color_Off}" if ! [[ "$mfacode" =~ ^$ || "$mfacode" =~ [0-9]{6} ]]; then - echo -e "${BIRed}The MFA pass code must be exactly six digits, or blank to bypass (to use the profile without an MFA session).${Color_Off}" + 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 @@ -1155,11 +1213,11 @@ else fi echo - echo -e "Acquiring MFA session token for the profile: ${BIWhite}${AWS_USER_PROFILE}${Color_Off}..." + echo -e "Acquiring MFA session token for the profile: ${BIWhite}${On_Black}${AWS_USER_PROFILE}${Color_Off}..." getDuration AWS_SESSION_DURATION "$AWS_USER_PROFILE" - mfa_credentials=$(aws sts get-session-token \ + mfa_credentials_result=$(aws sts get-session-token \ --profile "$AWS_USER_PROFILE" \ --duration "$AWS_SESSION_DURATION" \ --serial-number "$ARN_OF_MFA" \ @@ -1167,25 +1225,23 @@ else --output text) if [[ "$DEBUG" == "true" ]]; then - echo -e "\\n${Cyan}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}${Color_Off}\\n\\n" + 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 - if [[ "$mfa_credentials" =~ error ]]; then - echo -e "${BIRed}An error occurred while attempting to acquire the MFA session credentials; cannot continue!${Color_Off};\\nRun the script with '--debug' argument to diagnose the problem." - exit 1 - else - read -r AWS_ACCESS_KEY_ID AWS_SECRET_ACCESS_KEY AWS_SESSION_TOKEN <<< $(printf '%s' "$mfa_credentials" | awk '{ print $2, $4, $5 }') - 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!" + + 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 - echo -e "${BIRed}Could not initialize the requested MFA session.${Color_Off}" + echo -e "${BIRed}${On_Black}Could not initialize the requested MFA session.${Color_Off}" echo exit 1 else # this is used to determine whether to print MFA questions/details mfaprofile="true" - echo -e "${Green}MFA session token acquired.${Color_Off}" + echo -e "${Green}${On_Black}MFA session token acquired.${Color_Off}" echo # export the selection to the remaining subshell commands in this script @@ -1199,8 +1255,8 @@ else # for the MFA profile getPrintableTimeRemaining _ret "$AWS_SESSION_DURATION" validity_period=${_ret} - echo -e "${BIWhite}Make this MFA session persistent?${Color_Off} (Saves the session in $CREDFILE\\nso that you can return to it during its validity period, ${validity_period}.)" - read -s -p "$(echo -e "${BIWhite}Yes (default) - make peristent${Color_Off}; No - only the envvars will be used ${BIWhite}[Y]${Color_Off}/N ")" -n 1 -r + echo -e "${BIWhite}${On_Black}Make this MFA session persistent?${Color_Off} (Saves the session in $CREDFILE\\nso 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 @@ -1239,10 +1295,10 @@ else # get region and output format for the selected profile AWS_DEFAULT_REGION=$(aws configure get region --profile "${final_selection}") - [[ "$DEBUG" == "true" ]] && echo -e "\\n${Cyan}result for: 'aws configure get region --profile \"${final_selection}\"':\\n${ICyan}${AWS_DEFAULT_REGION}${Color_Off}\\n\\n" + [[ "$DEBUG" == "true" ]] && echo -e "\\n${Cyan}${On_Black}result for: 'aws configure get region --profile \"${final_selection}\"':\\n${ICyan}${AWS_DEFAULT_REGION}${Color_Off}\\n\\n" AWS_DEFAULT_OUTPUT=$(aws configure get output --profile "${final_selection}") - [[ "$DEBUG" == "true" ]] && echo -e "\\n${Cyan}result for: 'aws configure get output --profile \"${final_selection}\"':\\n${ICyan}${AWS_DEFAULT_OUTPUT}${Color_Off}\\n\\n" + [[ "$DEBUG" == "true" ]] && echo -e "\\n${Cyan}${On_Black}result for: 'aws configure get output --profile \"${final_selection}\"':\\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 @@ -1290,14 +1346,14 @@ else 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}result for: 'aws configure --profile \"${final_selection}\" get aws_access_key_id':\\n${ICyan}${AWS_ACCESS_KEY_ID}${Color_Off}\\n\\n" + [[ "$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}result for: 'aws configure --profile \"${final_selection}\" get aws_access_key_id':\\n${ICyan}${AWS_SECRET_ACCESS_KEY}${Color_Off}\\n\\n" + [[ "$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}result for: 'aws configure --profile \"${final_selection}\" get aws_session_token':\\n${ICyan}${AWS_SESSION_TOKEN}${Color_Off}\\n\\n" + [[ "$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} @@ -1311,21 +1367,21 @@ else echo -e "${BIWhite}${On_DGreen} * * * PROFILE DETAILS * * * ${Color_Off}" echo if [[ "$mfaprofile" == "true" ]]; then - echo -e "${BIWhite}MFA profile name: '${final_selection}'${Color_Off}" + echo -e "${BIWhite}${On_Black}MFA profile name: '${final_selection}'${Color_Off}" echo else - echo -e "${BIWhite}Profile name '${final_selection}'${Color_Off}" - echo -e "\\n${BIWhite}NOTE: This is not an MFA session!${Color_Off}" + 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}${AWS_DEFAULT_REGION}${Color_Off}" - echo -e "Output format is set to: ${BIWhite}${AWS_DEFAULT_OUTPUT}${Color_Off}" + 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}Do you want to export the selected profile's secrets to the environment${Color_Off} (for s3cmd, etc)? - Y/${BIWhite}[N]${Color_Off} ")" -n 1 -r + 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 @@ -1342,14 +1398,14 @@ else fi if [[ "$mfacode" != "" ]] && [[ "$persistent_MFA" == "false" ]]; then - echo -e "${BIWhite}*** 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 -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}*** 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 -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, @@ -1437,20 +1493,20 @@ else echo if [[ "$OS" == "Linux" ]]; then if exists xclip; then - echo "${BIGreen}*** NOTE: xclip found; the envvar configuration command is now on your X PRIMARY clipboard -- just paste on the command line, and press [ENTER])${Color_Off}" + echo "${BIGreen}${On_Black}*** NOTE: xclip found; the envvar configuration command is now on your X PRIMARY clipboard -- just paste on the command line, and press [ENTER])${Color_Off}" else echo echo "*** NOTE: If you're using an X GUI on Linux, install 'xclip' to have the activation command copied to the clipboard automatically!" fi fi echo - echo -e "${Green}*** Make sure to export/unset all the new values as instructed above to\\n make sure no conflicting profile/secrets remain in the envrionment!" + echo -e "${Green}${On_Black}*** Make sure to export/unset all the new values as instructed above to\\n make sure no conflicting profile/secrets remain in the envrionment!" echo - echo -e "*** 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}aws sts get-caller-identity --profile default${Color_Off}" + echo -e "*** 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 sts get-caller-identity --profile default${Color_Off}" echo - echo -e "${Green}*** 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}source ./source-to-clear-AWS-envvars.sh" + 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-to-clear-AWS-envvars.sh" echo - echo -e "${BIWhite}PASTE THE PROFILE ACTIVATION COMMAND FROM THE CLIPBOARD\\nON THE COMMAND LINE NOW, AND PRESS ENTER! THEN YOU'RE DONE!${Color_Off}" + echo -e "${BIWhite}${On_Black}PASTE THE PROFILE ACTIVATION COMMAND FROM THE CLIPBOARD\\nON THE COMMAND LINE NOW, AND PRESS ENTER! THEN YOU'RE DONE!${Color_Off}" echo else # not macOS, not Linux, so some other weird OS like Windows.. From d1a36e215a042221a244a6159e76fb02ce322727 Mon Sep 17 00:00:00 2001 From: Ville Walveranta Date: Wed, 18 Apr 2018 00:44:18 -0400 Subject: [PATCH 54/71] Added better support for terminal with white background. Other fixes. --- awscli-mfa/awscli-mfa.sh | 2 + awscli-mfa/enable-disable-vmfa-device.sh | 321 +++++++++++++---------- awscli-mfa/mfastatus.sh | 26 +- 3 files changed, 204 insertions(+), 145 deletions(-) diff --git a/awscli-mfa/awscli-mfa.sh b/awscli-mfa/awscli-mfa.sh index 88ec7b2..0f53ad0 100755 --- a/awscli-mfa/awscli-mfa.sh +++ b/awscli-mfa/awscli-mfa.sh @@ -811,6 +811,7 @@ else # this bails out on errors checkAWSErrors "true" "$process_user_arn" "$currently_selected_profile_ident_printable" + # we didn't bail out; continuing... echo -e "Executing this script as the AWS/IAM user '$process_username' (profile $currently_selected_profile_ident_printable).\\n" # declare the arrays for credentials loop @@ -1231,6 +1232,7 @@ else # 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 diff --git a/awscli-mfa/enable-disable-vmfa-device.sh b/awscli-mfa/enable-disable-vmfa-device.sh index 17b305c..9b7fdaa 100755 --- a/awscli-mfa/enable-disable-vmfa-device.sh +++ b/awscli-mfa/enable-disable-vmfa-device.sh @@ -116,7 +116,7 @@ On_IWhite='\033[0;107m' # White # DEBUG MODE WARNING ========================================================= -[[ "$DEBUG" == "true" ]] && echo -e "\\n${BIWhite}${On_Red} DEBUG MODE ACTIVE ${Color_Off}\\n\\n${BIRed}NOTE: Debug output may include secrets!!!${Color_Off}\\n\\n" +[[ "$DEBUG" == "true" ]] && 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" # FUNCTIONS ================================================================== @@ -391,12 +391,12 @@ continue_maybe() { if [[ "$already_failed" == "false" ]]; then if [[ "${failtype}" == "expired" ]]; then - echo -e "\\n${BIRed}THE MFA SESSION SELECTED/CONFIGURED IN THE ENVIRONMENT HAS EXPIRED.${Color_Off}\\n" + echo -e "\\n${BIRed}${On_Black}THE MFA SESSION SELECTED/CONFIGURED IN THE ENVIRONMENT HAS EXPIRED.${Color_Off}\\n" else - echo -e "\\n${BIRed}THE AWS PROFILE SELECTED/CONFIGURED IN THE ENVIRONMENT IS INVALID.${Color_Off}\\n" + 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}Do you want to continue with the default profile?${Color_Off} - ${BIWhite}[Y]${Color_Off}/N ")" -n 1 -r + 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 @@ -433,6 +433,68 @@ continue_maybe() { fi } +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 +} + print_mfa_notice() { echo -e "To disable/detach a vMFAd from the profile, you must have\\nan active MFA session established with it. Use the 'awscli-mfa.sh'\\nscript to establish an MFA session for the profile first, then\\nrun this script again.\\n" echo -e "If you do not have possession of the vMFAd for this profile\\n(in GA/Authy app), please request ops to disable the vMFAd\\nfor your profile, or if you have admin credentials for AWS,\\nuse them outside this script to disable the vMFAd for this\\nprofile." @@ -456,7 +518,7 @@ if [[ "$AWS_CONFIG_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." + 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 @@ -466,20 +528,20 @@ if [[ "$AWS_CONFIG_FILE" != "" ]] && active_config_file=$AWS_CONFIG_FILE echo - echo -e "${BIWhite}** NOTE: A custom configuration file defined with AWS_CONFIG_FILE envvar in effect: '$AWS_CONFIG_FILE'${Color_Off}" + 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}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." + 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}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." + 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 @@ -489,20 +551,20 @@ if [[ "$AWS_SHARED_CREDENTIALS_FILE" != "" ]] && active_credentials_file=$AWS_SHARED_CREDENTIALS_FILE echo - echo -e "${BIWhite}** NOTE: A custom credentials file defined with AWS_SHARED_CREDENTIALS_FILE envvar in effect: '$AWS_SHARED_CREDENTIALS_FILE'${Color_Off}" + 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}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." + 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}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." + 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 @@ -527,7 +589,7 @@ done < "$CREDFILE" if [[ "$ONEPROFILE" == "false" ]]; then echo - echo -e "${BIRed}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 -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 @@ -656,14 +718,14 @@ else # 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}result for 'aws configure get region --profile default':\\n${ICyan}${default_region}\\n\\n" + [[ "$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}result for 'aws configure get output --profile default':\\n${ICyan}${default_output}\\n\\n" + [[ "$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}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 -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 @@ -694,7 +756,7 @@ else fi process_user_arn="$(aws sts get-caller-identity --query 'Arn' --output text 2>&1)" - [[ "$DEBUG" == "true" ]] && echo -e "\\n${Cyan}result for: 'aws sts get-caller-identity --query 'Arn' --output text':\\n${ICyan}$process_user_arn}${Color_Off}\\n\\n" + [[ "$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" [[ "$process_user_arn" =~ ([^/]+)$ ]] && process_username="${BASH_REMATCH[1]}" @@ -704,18 +766,17 @@ else currently_selected_profile_ident="'default'" process_user_arn="$(aws sts get-caller-identity --query 'Arn' --output text 2>&1)" - [[ "$DEBUG" == "true" ]] && echo -e "\\n${Cyan}result for: 'aws sts get-caller-identity --query 'Arn' --output text' \\(after profile reset\\):\\n${ICyan}${process_user_arn}${Color_Off}\\n\\n" + [[ "$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 - if [[ "$process_user_arn" =~ error ]]; then - echo -e "${BIRed}The selected profile is not functional${Color_Off}; please check the 'default' profile\\nin your '${CREDFILE}' file, and purge any 'AWS_' environment variables by executing\\n${Green}source ./source-to-clear-AWS-envvars.sh${Color_Off}" - exit 1 - else - echo "Executing this script as the AWS/IAM user '$process_username' (profile $currently_selected_profile_ident)." - fi + # this bails out on errors + checkAWSErrors "true" "$process_user_arn" "$currently_selected_profile_ident_printable" + + # we didn't bail out; continuing... + echo "Executing this script as the AWS/IAM user '$process_username' (profile $currently_selected_profile_ident)." echo @@ -732,7 +793,7 @@ else declare -a mfa_mfasec cred_profilecounter=0 - echo -ne "${BIWhite}Please wait" + echo -ne "${BIWhite}${On_Black}Please wait" # read the credentials file while IFS='' read -r line || [[ -n "$line" ]]; do @@ -757,14 +818,14 @@ else # store this profile region and output format profile_region[$cred_profilecounter]=$(aws configure get region --profile "$profile_ident") - [[ "$DEBUG" == "true" ]] && echo -e "\\n${Cyan}result for: 'aws configure get region --profile \"$profile_ident\"':\\n${ICyan}${profile_region[$cred_profilecounter]}${Color_Off}\\n\\n" + [[ "$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}result for: 'aws configure get output --profile \"$profile_output\"':\\n${ICyan}${profile_output[$cred_profilecounter]}${Color_Off}\\n\\n" + [[ "$DEBUG" == "true" ]] && echo -e "\\n${Cyan}${On_Black}result for: 'aws configure get output --profile \"$profile_output\"':\\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}result for: 'aws sts get-caller-identity --profile \"$profile_ident\" --query 'Arn' --output text':\\n${ICyan}${user_arn}${Color_Off}\\n\\n" + [[ "$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 @@ -777,7 +838,7 @@ else # (may be different from the arbitrary profile ident) [[ "$user_arn" =~ ([^/]+)$ ]] && profile_username="${BASH_REMATCH[1]}" - if [[ "$profile_username" =~ error ]]; then + if [[ "$profile_username" =~ 'error occurred' ]]; then cred_profile_user[$cred_profilecounter]="" else cred_profile_user[$cred_profilecounter]="$profile_username" @@ -796,7 +857,7 @@ else # 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}result for: 'aws iam get-user --profile \"$profile_ident\" --query 'User.Arn' --output text':\\n${ICyan}${profile_check}${Color_Off}\\n\\n" + [[ "$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" @@ -813,7 +874,7 @@ else --output text \ --query 'MFADevices[].SerialNumber' 2>&1)" - [[ "$DEBUG" == "true" ]] && echo -e "\\n${Cyan}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" + [[ "$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" @@ -846,7 +907,7 @@ else mfa_profile_check="$(aws iam get-user --profile "$mfa_profile_ident" --query 'User.Arn' --output text 2>&1)" - [[ "$DEBUG" == "true" ]] && echo -e "\\n${Cyan}result for: 'aws iam get-user --profile \"$mfa_profile_ident\" --query 'User.Arn' --output text':\\n${ICyan}${mfa_profile_check}${Color_Off}\\n\\n" + [[ "$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" @@ -897,10 +958,10 @@ else if [[ ${#cred_profiles[@]} == 1 ]]; then echo [[ "${cred_profile_user[0]}" != "" ]] && prcpu="${cred_profile_user[0]}" || prcpu="unknown -- a bad profile?" - echo -e "${Green}You have one configured profile: ${BIGreen}${cred_profiles[0]} ${Green}(IAM: ${prcpu})${Color_Off}" + echo -e "${Green}${On_Black}You have one configured profile: ${BIGreen}${cred_profiles[0]} ${Green}(IAM: ${prcpu})${Color_Off}" if [[ "${mfa_arns[0]}" != "" ]]; then - echo -en ".. and its virtual MFA device is already enabled.\\n\\n${BIWhite}Do you want to disable its vMFAd? Y/N${Color_Off} " + 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 @@ -917,7 +978,7 @@ else echo else - echo -en ".. but it doesn't have a virtual MFA device attached/enabled.\\n\\n${BIWhite}Do you want to attach/enable a vMFAd? Y/N${Color_Off} " + 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 @@ -941,7 +1002,7 @@ else # 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}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 -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 @@ -950,7 +1011,7 @@ else if [[ "${mfa_arns[$SELECTR]}" == "" ]]; then # no vMFAd configured [[ "${cred_profile_user[$SELECTR]}" != "" ]] && prcpu="${cred_profile_user[$SELECTR]}" || prcpu="unknown -- a bad profile?" - echo -en "${BIWhite}${ITER}: $i${Color_Off} (IAM: ${prcpu})\\n\\n" + echo -en "${BIWhite}${On_Black}${ITER}: $i${Color_Off} (IAM: ${prcpu})\\n\\n" # add to the translation table for the selection iter_to_profile[$ITER]=$SELECTR @@ -961,7 +1022,7 @@ else echo echo -e "${BIWhite}${On_DGreen} AWS PROFILES WITH ACTIVE (ENABLED) VIRTUAL MFA DEVICE (vMFAd): ${Color_Off}" - echo -e " ${BIWhite}Select a profile whose vMFAd you want to detach/disable.${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 -e " ${BIWhite}${On_Black}Select a profile whose vMFAd you want to detach/disable.${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[@]}" @@ -969,7 +1030,7 @@ else if [[ "${mfa_arns[$SELECTR]}" != "" ]]; then # vMFAd configured [[ "${cred_profile_user[$SELECTR]}" != "" ]] && prcpu="${cred_profile_user[$SELECTR]}" || prcpu="unknown -- a bad profile?" - echo -en "${BIWhite}${ITER}: $i${Color_Off} (IAM: ${prcpu})\\n\\n" + echo -en "${BIWhite}${On_Black}${ITER}: $i${Color_Off} (IAM: ${prcpu})\\n\\n" # add to the translation table for the selection iter_to_profile[$ITER]=$SELECTR ((ITER++)) @@ -978,9 +1039,8 @@ else done # prompt for profile selection - echo -en "\\n${BIWhite}SELECT A PROFILE BY THE NUMBER: " + echo -en "\\n${BIWhite}${On_Black}SELECT A PROFILE BY THE NUMBER:${Color_Off} " read -r selprofile - echo -en "\\n${Color_Off}" fi # end profile selection @@ -1002,7 +1062,7 @@ else $selprofile_check -lt 1 ]]; then # a selection outside of the existing range was specified - echo -e "${BIRed}There is no profile with the ID '${selprofile}'.${Color_Off}" + echo -e "${BIRed}${On_Black}There is no profile with the ID '${selprofile}'.${Color_Off}" echo exit 1 else @@ -1026,7 +1086,7 @@ else aws_account_id="${BASH_REMATCH[1]}" aws_iam_user="${BASH_REMATCH[2]}" else - echo -e "${BIRed}Could not acquire AWS account ID or current IAM user name. A bad profile? Cannot continue.${Color_Off}" + 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 @@ -1038,20 +1098,19 @@ else --query 'VirtualMFADevices[?SerialNumber==`arn:aws:iam::'"${aws_account_id}"':mfa/'"${aws_iam_user}"'`].SerialNumber' 2>&1) if [[ "$DEBUG" == "true" ]]; then - echo -e "\\n${Cyan}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" + 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 ]]; then - echo -e "${BIRed}Could not execute list-virtual-mfa-devices. Cannot continue.${Color_Off}" + 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}Unassigned vMFAd found for the profile:\\n${BIGreen}$available_user_vmfad${Color_Off}" - echo - echo -en "${BIWhite}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}Y/N${Color_Off} " + 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 @@ -1064,16 +1123,14 @@ else --serial-number "${available_user_vmfad}" 2>&1) if [[ "$DEBUG" == "true" ]]; then - echo -e "\\n${Cyan}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" + 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 - if [[ "$mfa_deletion_result" =~ error ]]; then - echo - echo -e "${BIRed}Could not delete inaccessible vMFAd. Cannot continue.${Color_Off}" - echo - exit 1 - fi + # this bails out on errors + checkAWSErrors "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 @@ -1108,20 +1165,18 @@ else --outfile "${qr_with_path}" \ --bootstrap-method QRCodePNG 2>&1) - if [[ "$DEBUG" == "true" ]]; then - echo -e "\\n${Cyan}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 - - if [[ "$vmfad_creation_status" =~ error ]]; then - echo -e "${BIRed}Could not execute create-virtual-mfa-device.\\nNo virtual MFA device to enable. Cannot continue.${Color_Off}" - echo - exit 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 - echo -e "${BIGreen}A new vMFAd has been created. ${BIWhite}Please scan\\nthe QRCode with Authy to add the vMFAd on\\nyour portable device.${Color_Off}" - echo -e "\\nNOTE: The QRCode file, \"${qr_file_name}\",\\nis $qr_file_target!" + # this bails out on errors + checkAWSErrors "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}Press 'x' once you have scanned the QRCode to proceed.${Color_Off}" + echo -e "${BIWhite}${On_Black}Press 'x' once you have scanned the QRCode to proceed.${Color_Off}" while : do read -s -n 1 -r @@ -1131,7 +1186,7 @@ else 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}Do you want to delete the QRCode securely? Y/N${Color_Off} " + 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 @@ -1139,11 +1194,11 @@ else if [[ $REPLY =~ ^[Yy]$ ]]; then rm -fP "${qr_with_path}" echo - echo -e "${BIWhite}QRCode file deleted securely.${Color_Off}" + echo -e "${BIWhite}${On_Black}QRCode file deleted securely.${Color_Off}" break; elif [[ $REPLY =~ ^[Nn]$ ]]; then echo - echo -e "${BIWhite}You chose not to delete the vMFAd initializer QRCode;\\nplease store it securely as if it were a password!${Color_Off}" + 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 @@ -1156,19 +1211,18 @@ else --query 'VirtualMFADevices[?SerialNumber==`arn:aws:iam::'"${aws_account_id}"':mfa/'"${aws_iam_user}"'`].SerialNumber' 2>&1) if [[ "$DEBUG" == "true" ]]; then - echo -e "\\n${Cyan}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" + 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 - if [[ "$available_user_vmfad" =~ error ]]; then - echo -e "${BIRed}Could not execute list-virtual-mfa-devices. Cannot continue.${Color_Off}" - exit 1 - fi + # this bails out on errors + checkAWSErrors "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}No virtual MFA device to enable. Cannot continue.${Color_Off}" + 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" @@ -1176,7 +1230,7 @@ else fi echo - echo -e "${BIWhite}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" + 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 @@ -1186,7 +1240,7 @@ else authcode2="${BASH_REMATCH[2]}" break; else - echo -e "${BIRed}Bad authcodes.${Color_Off} Please enter two consecutively generated six-digit numbers separated by a space." + echo -e "${BIRed}${On_Black}Bad authcodes.${Color_Off} Please enter two consecutively generated six-digit numbers separated by a space." fi done @@ -1200,29 +1254,34 @@ else --authentication-code-2 "${authcode2}" 2>&1) if [[ "$DEBUG" == "true" ]]; then - echo -e "\\n${Cyan}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" + 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 - if [[ "$vmfad_enablement_status" =~ error ]]; then - echo -e "${BIRed}Could not enable vMFAd. Cannot continue.\\n${Red}Mistyped authcodes, or wrong/old vMFAd?${Color_Off}" - exit 1 - else - echo -e "${BIGreen}vMFAd successfully enabled for the profile '${final_selection}' ${Green}(IAM user name '$aws_iam_user').${Color_Off}" - echo -e "${BIGreen}You can now use the 'awscli-mfa.sh' script to start an MFA session for this profile!${Color_Off}" - echo - fi + # this bails out on errors + checkAWSErrors "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 --output text --query 'Arn' 2>&1)" - [[ "$DEBUG" == "true" ]] && echo -e "\\n${Cyan}result for: 'aws sts get-caller-identity --query 'Arn' --output text':\\n${ICyan}${transient_mfa_profile_check}${Color_Off}\\n\\n" + [[ "$DEBUG" == "true" ]] && echo -e "\\n${Cyan}${On_Black}result for: 'aws sts get-caller-identity --query 'Arn' --output text':\\n${ICyan}${transient_mfa_profile_check}${Color_Off}\\n\\n" + # this bails out on errors + checkAWSErrors "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 - echo -e "${BIRed}Could not acquire AWS account ID or current IAM user name. A bad profile? Cannot continue.${Color_Off}\\n" + # .. 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 @@ -1265,13 +1324,13 @@ else getRemaining _ret "$PRECHECK_AWS_SESSION_INIT_TIME" "$PRECHECK_AWS_SESSION_DURATION" else - echo -e "${BIRed}This is an unknown in-env MFA session. Cannot continue.${Color_Off}\\n" + echo -e "${BIRed}${On_Black}This is an unknown in-env MFA session. Cannot continue.${Color_Off}\\n" print_mfa_notice exit 1 fi else # no valid MFA profile (in-env or functional reference) found - echo -e "${BIRed}No active MFA session found for the profile '${final_selection}'.${Color_Off}\\n" + echo -e "${BIRed}${On_Black}No active MFA session found for the profile '${final_selection}'.${Color_Off}\\n" print_mfa_notice echo exit 1 @@ -1285,47 +1344,45 @@ else --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}result for: 'aws iam deactivate-mfa-device --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" + [[ "$DEBUG" == "true" ]] && echo -e "\\n${Cyan}${On_Black}result for: 'aws iam deactivate-mfa-device --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 "true" "$vmfad_deactivation_result" "$final_selection" "Could not disable/detach vMFAd for the profile '${final_selection}'. Cannot continue!" + + # we didn't bail out; continuing... + echo + echo -e "${BIGreen}${On_Black}vMFAd 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 "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 detached/disabled, 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 - if [[ "$vmfad_deactivation_result" =~ error ]]; then - echo -e "${BIRed}Could not disable/detach vMFAd for the profile '${final_selection}'. Cannot continue.${Color_Off}" - exit 1 - else - echo - echo -e "${BIGreen}vMFAd disabled/detached for the profile '${final_selection}'.${Color_Off}" - echo - - echo -en "${BIWhite}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}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" - - if [[ "$vmfad_delete_result" =~ error ]]; then - echo -e "\\n${BIRed}Could not delete vMFAd for the profile '${final_selection}'. Cannot continue.${Color_Off}" - exit 1 - else - echo -e "\\n${BIGreen}vMFAd deleted for the profile '${final_selection}'.${Color_Off}" - echo - echo "To set up a new vMFAd, run this script again." - echo - fi - - break; - elif [[ $REPLY =~ ^[Nn]$ ]]; then - echo -e "\\n\\n${BIWhite}The following vMFAd was detached/disabled, 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 - fi else - echo -e "\\n${BIRed}The MFA session for the profile \"${final_selection}\" has expired.${Color_Off}\\n" + 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 @@ -1334,19 +1391,19 @@ else fi else # non-acceptable characters were present in the selection - echo -e "${BIRed}There is no profile '${selprofile}'.${Color_Off}" + 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}There is no profile '${selprofile}'.${Color_Off}" + echo -e "${BIRed}${On_Black}There is no profile '${selprofile}'.${Color_Off}" echo exit 1 fi else # empty selection - echo -e "${BIRed}There is no profile '${selprofile}'.${Color_Off}" + echo -e "${BIRed}${On_Black}There is no profile '${selprofile}'.${Color_Off}" echo exit 1 fi diff --git a/awscli-mfa/mfastatus.sh b/awscli-mfa/mfastatus.sh index c71ff1e..1ba2fff 100755 --- a/awscli-mfa/mfastatus.sh +++ b/awscli-mfa/mfastatus.sh @@ -309,20 +309,20 @@ if [[ "$AWS_CONFIG_FILE" != "" ]] && active_config_file=$AWS_CONFIG_FILE echo - echo -e "${BIWhite}** NOTE: A custom configuration file defined with AWS_CONFIG_FILE envvar in effect: '$AWS_CONFIG_FILE'${Color_Off}" + 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}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." + 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}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." + 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 @@ -332,20 +332,20 @@ if [[ "$AWS_SHARED_CREDENTIALS_FILE" != "" ]] && active_credentials_file=$AWS_SHARED_CREDENTIALS_FILE echo - echo -e "${BIWhite}** NOTE: A custom credentials file defined with AWS_SHARED_CREDENTIALS_FILE envvar in effect: '$AWS_SHARED_CREDENTIALS_FILE'${Color_Off}" + 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}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." + 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}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." + 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 @@ -472,8 +472,8 @@ done < "$CREDFILE" ## PRESENTATION echo -echo -e "${BIWhite}ENVIRONMENT" -echo -e "===========${Color_Off}" +echo -e "${BIWhite}${On_Black}ENVIRONMENT${Color_Off}" +echo -e "===========" echo if [[ "$AWS_PROFILE" != "" ]]; then @@ -488,11 +488,11 @@ if [[ "$AWS_PROFILE" != "" ]]; then fi if [[ "${profile_type}" == "profile" ]]; then - echo -e "${Green}ENVVAR 'AWS_PROFILE' SELECTING A BASE PROFILE (not an MFA session): ${BIGreen}${AWS_PROFILE}${Color_Off}" + 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}ENVVAR 'AWS_PROFILE' SELECTING A PERSISTENT MFA SESSION (as below): ${BIGreen}${AWS_PROFILE}${Color_Off}" + echo -e "${Green}${On_Black}ENVVAR 'AWS_PROFILE' SELECTING A PERSISTENT MFA SESSION (as below): ${BIGreen}${AWS_PROFILE}${Color_Off}" else - echo -e "${BIRed}INVALID ENVIRONMENT CONFIGURATION!\\nExecute ${Red}source ./source-to-clear-AWS-envvars.sh${BIRed} to clear the environment.\\n${Color_Off}" + echo -e "${BIRed}${On_Black}INVALID ENVIRONMENT CONFIGURATION!\\nExecute ${Red}source ./source-to-clear-AWS-envvars.sh${BIRed} to clear the environment.\\n${Color_Off}" fi fi else @@ -506,7 +506,7 @@ fi echo echo -echo -e "${BIWhite}PERSISTENT MFA SESSIONS (in $CREDFILE)" +echo -e "${BIWhite}${On_Black}PERSISTENT MFA SESSIONS (in $CREDFILE)${Color_Off}" repeatr "=" 29 ${#CREDFILE} echo -e "${Color_Off}" echo @@ -533,7 +533,7 @@ do fi if [[ "$bad_profile" == "false" ]]; then - echo -e "${Green}MFA SESSION IDENT: ${BIGreen}${profiles_ident[$z]} ${Green}(IAM user: '$for_iam')${Color_Off}" + echo -e "${Green}}MFA SESSION IDENT: ${BIGreen}${profiles_ident[$z]} ${Green}(IAM user: '$for_iam')${Color_Off}" else echo -e "${Green}MFA SESSION IDENT: ${BIGreen}${profiles_ident[$z]} ${Red}(IAM user: $for_iam)${Color_Off}" fi From 9929383998ad328a0da291d1aec9d3639e74aef8 Mon Sep 17 00:00:00 2001 From: Ville Walveranta Date: Wed, 18 Apr 2018 15:06:25 -0400 Subject: [PATCH 55/71] Added account reference to the output in awscli-mfa.sh and enable-disable-vmfa-device.sh --- awscli-mfa/awscli-mfa.sh | 23 ++++++++++++++++++--- awscli-mfa/enable-disable-vmfa-device.sh | 26 ++++++++++++++++++++---- 2 files changed, 42 insertions(+), 7 deletions(-) diff --git a/awscli-mfa/awscli-mfa.sh b/awscli-mfa/awscli-mfa.sh index 0f53ad0..b72086e 100755 --- a/awscli-mfa/awscli-mfa.sh +++ b/awscli-mfa/awscli-mfa.sh @@ -819,6 +819,7 @@ else 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 @@ -878,6 +879,18 @@ else cred_profile_user[$cred_profilecounter]="$profile_username" fi + # get the account alias (if any) for the user/profile + account_alias_result="$(aws iam list-account-aliases --profile "$profile_ident" --output text --query 'AccountAliases' 2>&1)" + [[ "$DEBUG" == "true" ]] && echo -e "\\n${Cyan}${On_Black}result for: 'aws iam list-account-aliases --profile \"$profile_ident\" --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 + cred_profile_account_alias[$cred_profilecounter]="" + else + # must be a bad profile + cred_profile_account_alias[$cred_profilecounter]="$account_alias_result" + fi + # 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 @@ -959,6 +972,7 @@ else 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 @@ -975,6 +989,7 @@ else # erase variables & increase iterator for the next iteration mfa_arn="" user_arn="" + account_alias_result="" profile_ident="" profile_check="" profile_username="" @@ -991,8 +1006,9 @@ else mfa_req="false" if [[ ${#cred_profiles[@]} == 1 ]]; then echo - [[ "${cred_profile_user[0]}" != "" ]] && prcpu="${cred_profile_user[0]}" || prcpu="unknown -- a bad profile?" - echo -e "${Green}${On_Black}You have one configured profile: ${BIGreen}${cred_profiles[0]} ${Green}(IAM: ${prcpu})${Color_Off}" + [[ "${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}" mfa_session_status="false" if [[ "${mfa_arns[0]}" != "" ]]; then @@ -1065,7 +1081,8 @@ else mfa_notify="; vMFAd not configured" fi [[ "${cred_profile_user[$SELECTR]}" != "" ]] && prcpu="${cred_profile_user[$SELECTR]}" || prcpu="unknown -- a bad profile?" - echo -en "${BIWhite}${On_Black}${ITER}: $i${Color_Off} (IAM: ${prcpu}${mfa_notify})\\n" + [[ "${cred_profile_account_alias[$SELECTR]}" != "" ]] && prcpaa=" @${cred_profile_account_alias[$SELECTR]}" || prcpaa="" + echo -en "${BIWhite}${On_Black}${ITER}: $i${Color_Off} (IAM: ${prcpu}${prcpaa}${mfa_notify})\\n" if [[ "${mfa_profile_status[$SELECTR]}" != "EXPIRED" && "${mfa_profile_status[$SELECTR]}" != "" ]]; then diff --git a/awscli-mfa/enable-disable-vmfa-device.sh b/awscli-mfa/enable-disable-vmfa-device.sh index 9b7fdaa..bb1d9e1 100755 --- a/awscli-mfa/enable-disable-vmfa-device.sh +++ b/awscli-mfa/enable-disable-vmfa-device.sh @@ -785,6 +785,7 @@ else 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 @@ -820,7 +821,7 @@ else 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_output\"':\\n${ICyan}${profile_output[$cred_profilecounter]}${Color_Off}\\n\\n" + [[ "$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 @@ -844,6 +845,18 @@ else cred_profile_user[$cred_profilecounter]="$profile_username" fi + # get the account alias (if any) for the user/profile + account_alias_result="$(aws iam list-account-aliases --profile "$profile_ident" --output text --query 'AccountAliases' 2>&1)" + [[ "$DEBUG" == "true" ]] && echo -e "\\n${Cyan}${On_Black}result for: 'aws iam list-account-aliases --profile \"$profile_ident\" --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 + cred_profile_account_alias[$cred_profilecounter]="" + else + # must be a bad profile + cred_profile_account_alias[$cred_profilecounter]="$account_alias_result" + fi + # 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 @@ -926,6 +939,7 @@ else 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 @@ -942,6 +956,7 @@ else # erase variables & increase iterator for the next iteration mfa_arn="" user_arn="" + account_alias_result="" profile_ident="" profile_check="" profile_username="" @@ -958,7 +973,8 @@ else if [[ ${#cred_profiles[@]} == 1 ]]; then echo [[ "${cred_profile_user[0]}" != "" ]] && prcpu="${cred_profile_user[0]}" || prcpu="unknown -- a bad profile?" - echo -e "${Green}${On_Black}You have one configured profile: ${BIGreen}${cred_profiles[0]} ${Green}(IAM: ${prcpu})${Color_Off}" + [[ "${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} " @@ -1011,7 +1027,8 @@ else if [[ "${mfa_arns[$SELECTR]}" == "" ]]; then # no vMFAd configured [[ "${cred_profile_user[$SELECTR]}" != "" ]] && prcpu="${cred_profile_user[$SELECTR]}" || prcpu="unknown -- a bad profile?" - echo -en "${BIWhite}${On_Black}${ITER}: $i${Color_Off} (IAM: ${prcpu})\\n\\n" + [[ "${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 @@ -1030,7 +1047,8 @@ else if [[ "${mfa_arns[$SELECTR]}" != "" ]]; then # vMFAd configured [[ "${cred_profile_user[$SELECTR]}" != "" ]] && prcpu="${cred_profile_user[$SELECTR]}" || prcpu="unknown -- a bad profile?" - echo -en "${BIWhite}${On_Black}${ITER}: $i${Color_Off} (IAM: ${prcpu})\\n\\n" + [[ "${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++)) From 9ebbafef0ab82a7e0d13405cfc9221f548ef3fd9 Mon Sep 17 00:00:00 2001 From: Ville Walveranta Date: Wed, 18 Apr 2018 20:35:05 -0400 Subject: [PATCH 56/71] awscli-mfa scripts: various fixes; added account alias/ID to output --- awscli-mfa/awscli-mfa.sh | 92 ++++++++++++++++++------ awscli-mfa/enable-disable-vmfa-device.sh | 60 ++++++++++++---- awscli-mfa/mfastatus.sh | 40 ++++++++++- 3 files changed, 152 insertions(+), 40 deletions(-) diff --git a/awscli-mfa/awscli-mfa.sh b/awscli-mfa/awscli-mfa.sh index b72086e..f46aa73 100755 --- a/awscli-mfa/awscli-mfa.sh +++ b/awscli-mfa/awscli-mfa.sh @@ -532,6 +532,32 @@ checkAWSErrors() { [[ "$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 + 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? @@ -793,9 +819,6 @@ else 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" - [[ "$process_user_arn" =~ ([^/]+)$ ]] && - process_username="${BASH_REMATCH[1]}" - # prompt to switch to default on any error if [[ "$process_user_arn" =~ 'error occurred' ]]; then continue_maybe "invalid" @@ -803,22 +826,34 @@ else 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" - - [[ "$process_user_arn" =~ ([^/]+)$ ]] && - process_username="${BASH_REMATCH[1]}" fi # this bails out on errors checkAWSErrors "true" "$process_user_arn" "$currently_selected_profile_ident_printable" # we didn't bail out; continuing... - echo -e "Executing this script as the AWS/IAM user '$process_username' (profile $currently_selected_profile_ident_printable).\\n" + # 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" # 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_acc declare -a cred_profile_account_alias declare -a profile_region declare -a profile_output @@ -871,25 +906,22 @@ else # 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 + if [[ "$user_arn" =~ ([[:digit:]]+):user/([^/]+)$ ]]; then + profile_user_acc="${BASH_REMATCH[1]}" + profile_username="${BASH_REMATCH[2]}" + fi + + if [[ "$user_arn" =~ 'error occurred' ]]; then cred_profile_user[$cred_profilecounter]="" + cred_profile_acc[$cred_profilecounter]="" else cred_profile_user[$cred_profilecounter]="$profile_username" + cred_profile_acc[$cred_profilecounter]="$profile_user_acc" fi # get the account alias (if any) for the user/profile - account_alias_result="$(aws iam list-account-aliases --profile "$profile_ident" --output text --query 'AccountAliases' 2>&1)" - [[ "$DEBUG" == "true" ]] && echo -e "\\n${Cyan}${On_Black}result for: 'aws iam list-account-aliases --profile \"$profile_ident\" --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 - cred_profile_account_alias[$cred_profilecounter]="" - else - # must be a bad profile - cred_profile_account_alias[$cred_profilecounter]="$account_alias_result" - fi + 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) @@ -989,7 +1021,6 @@ else # erase variables & increase iterator for the next iteration mfa_arn="" user_arn="" - account_alias_result="" profile_ident="" profile_check="" profile_username="" @@ -1007,7 +1038,14 @@ else 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="" + + if [[ "${cred_profile_account_alias[0]}" != "" ]]; then + prcpaa="@${cred_profile_account_alias[0]}" + else + # use the AWS account number if no alias has been defined + prcpaa="${cred_profile_acc[0]}" + fi + echo -e "${Green}${On_Black}You have one configured profile: ${BIGreen}${cred_profiles[0]} ${Green}(IAM: ${prcpu}${prcpaa})${Color_Off}" mfa_session_status="false" @@ -1080,8 +1118,16 @@ else else mfa_notify="; vMFAd not configured" fi + [[ "${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="" + + if [[ "${cred_profile_account_alias[$SELECTR]}" != "" ]]; then + prcpaa="@${cred_profile_account_alias[$SELECTR]}" + else + # use the AWS account number if no alias has been defined + prcpaa="${cred_profile_acc[$SELECTR]}" + fi + echo -en "${BIWhite}${On_Black}${ITER}: $i${Color_Off} (IAM: ${prcpu}${prcpaa}${mfa_notify})\\n" if [[ "${mfa_profile_status[$SELECTR]}" != "EXPIRED" && diff --git a/awscli-mfa/enable-disable-vmfa-device.sh b/awscli-mfa/enable-disable-vmfa-device.sh index bb1d9e1..000e7db 100755 --- a/awscli-mfa/enable-disable-vmfa-device.sh +++ b/awscli-mfa/enable-disable-vmfa-device.sh @@ -500,6 +500,32 @@ print_mfa_notice() { echo -e "If you do not have possession of the vMFAd for this profile\\n(in GA/Authy app), please request ops to disable the vMFAd\\nfor your profile, or if you have admin credentials for AWS,\\nuse them outside this script to disable the vMFAd for this\\nprofile." } +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? @@ -758,10 +784,7 @@ else 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" - [[ "$process_user_arn" =~ ([^/]+)$ ]] && - process_username="${BASH_REMATCH[1]}" - - if [[ "$process_user_arn" =~ ExpiredToken ]]; then + if [[ "$process_user_arn" =~ 'error occurred' ]]; then continue_maybe "invalid" currently_selected_profile_ident="'default'" @@ -776,7 +799,22 @@ else checkAWSErrors "true" "$process_user_arn" "$currently_selected_profile_ident_printable" # we didn't bail out; continuing... - echo "Executing this script as the AWS/IAM user '$process_username' (profile $currently_selected_profile_ident)." + # 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)." echo @@ -846,16 +884,8 @@ else fi # get the account alias (if any) for the user/profile - account_alias_result="$(aws iam list-account-aliases --profile "$profile_ident" --output text --query 'AccountAliases' 2>&1)" - [[ "$DEBUG" == "true" ]] && echo -e "\\n${Cyan}${On_Black}result for: 'aws iam list-account-aliases --profile \"$profile_ident\" --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 - cred_profile_account_alias[$cred_profilecounter]="" - else - # must be a bad profile - cred_profile_account_alias[$cred_profilecounter]="$account_alias_result" - fi + 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) diff --git a/awscli-mfa/mfastatus.sh b/awscli-mfa/mfastatus.sh index 1ba2fff..cbb300e 100755 --- a/awscli-mfa/mfastatus.sh +++ b/awscli-mfa/mfastatus.sh @@ -287,6 +287,32 @@ repeatr() { 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 @@ -527,15 +553,23 @@ do 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')${Color_Off}" + 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)${Color_Off}" + 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 @@ -553,6 +587,8 @@ do fi echo fi + + bad_profile="false" _ret="" _ret_duration="" _ret_remaining="" From c4b060c40f3ed18e9988e2429510abf3c9525460 Mon Sep 17 00:00:00 2001 From: Ville Walveranta Date: Thu, 26 Apr 2018 13:24:28 -0500 Subject: [PATCH 57/71] Documentation update; small output standardization. --- awscli-mfa/awscli-mfa.sh | 8 ++++---- awscli-mfa/enable-disable-vmfa-device.sh | 4 ++-- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/awscli-mfa/awscli-mfa.sh b/awscli-mfa/awscli-mfa.sh index f46aa73..474b930 100755 --- a/awscli-mfa/awscli-mfa.sh +++ b/awscli-mfa/awscli-mfa.sh @@ -841,9 +841,9 @@ else getAccountAlias _ret if [[ "${_ret}" != "" ]]; then - account_alias_if_any="@ ${_ret}" + account_alias_if_any="@${_ret}" else - account_alias_if_any="@ ${profile_user_acc}" + 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" @@ -1040,7 +1040,7 @@ else [[ "${cred_profile_user[0]}" != "" ]] && prcpu="${cred_profile_user[0]}" || prcpu="unknown -- a bad profile? " if [[ "${cred_profile_account_alias[0]}" != "" ]]; then - prcpaa="@${cred_profile_account_alias[0]}" + prcpaa=" @${cred_profile_account_alias[0]}" else # use the AWS account number if no alias has been defined prcpaa="${cred_profile_acc[0]}" @@ -1122,7 +1122,7 @@ else [[ "${cred_profile_user[$SELECTR]}" != "" ]] && prcpu="${cred_profile_user[$SELECTR]}" || prcpu="unknown -- a bad profile?" if [[ "${cred_profile_account_alias[$SELECTR]}" != "" ]]; then - prcpaa="@${cred_profile_account_alias[$SELECTR]}" + prcpaa=" @${cred_profile_account_alias[$SELECTR]}" else # use the AWS account number if no alias has been defined prcpaa="${cred_profile_acc[$SELECTR]}" diff --git a/awscli-mfa/enable-disable-vmfa-device.sh b/awscli-mfa/enable-disable-vmfa-device.sh index 000e7db..7852f5e 100755 --- a/awscli-mfa/enable-disable-vmfa-device.sh +++ b/awscli-mfa/enable-disable-vmfa-device.sh @@ -808,9 +808,9 @@ else getAccountAlias _ret if [[ "${_ret}" != "" ]]; then - account_alias_if_any="@ ${_ret}" + account_alias_if_any="@${_ret}" else - account_alias_if_any="@ ${profile_user_acc}" + account_alias_if_any="@${profile_user_acc}" fi # we didn't bail out; continuing... From 6e225caaefd2900be7747a62af2d751f68b03fe7 Mon Sep 17 00:00:00 2001 From: Ville Walveranta Date: Mon, 30 Apr 2018 03:21:21 -0500 Subject: [PATCH 58/71] awscli-mfa: output verbiage updates, bug fixes, documentation updates, sourceable file renamed --- awscli-mfa/README.md | 10 +- awscli-mfa/awscli-mfa.sh | 4 +- awscli-mfa/enable-disable-vmfa-device.sh | 126 ++++++++++++------ awscli-mfa/example-MFA-enforcement-policy.txt | 35 +++-- ...sh => source-this-to-clear-AWS-envvars.sh} | 0 5 files changed, 111 insertions(+), 64 deletions(-) rename awscli-mfa/{source-to-clear-AWS-envvars.sh => source-this-to-clear-AWS-envvars.sh} (100%) diff --git a/awscli-mfa/README.md b/awscli-mfa/README.md index 3abf964..dda7cc5 100644 --- a/awscli-mfa/README.md +++ b/awscli-mfa/README.md @@ -1,13 +1,13 @@ # awscli-mfa and its companion scripts -The `awscli-mfa.sh` and its companion scripts `enable-disable-vmfa-device.sh` `mfastatus.sh`, and `source-to-clear-AWS-envvars.sh` were created to make handling AWS MFA sessions on the command line easy. +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 possible 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 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. +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. @@ -35,7 +35,7 @@ The scripts have been tested in macOS (High Sierra with stock bash 3.2.x) as wel * **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-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-to-clear-AWS-envvars.sh`, or set an alias, like so: `alias clearaws='source ~/awscli-mfa/source-to-clear-AWS-envvars.sh'` +* **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. @@ -228,7 +228,7 @@ First make sure you have `aws cli` installed. AWS has details for [Mac](https:// *** To easily remove any all AWS profile settings and secrets information from the environment, simply source the included script, like so: - source ./source-to-clear-AWS-envvars.sh + 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! @@ -261,7 +261,7 @@ First make sure you have `aws cli` installed. AWS has details for [Mac](https:// 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-to-clear-AWS-envvars.sh` is provided to make it easy to clear out any any `AWS_*` envvars, like so: `source ./source-to-clear-AWS-envvars.sh`. This purges any secrets and/or references to persistent profiles from the local environment. +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: diff --git a/awscli-mfa/awscli-mfa.sh b/awscli-mfa/awscli-mfa.sh index 474b930..92caaab 100755 --- a/awscli-mfa/awscli-mfa.sh +++ b/awscli-mfa/awscli-mfa.sh @@ -1043,7 +1043,7 @@ else prcpaa=" @${cred_profile_account_alias[0]}" else # use the AWS account number if no alias has been defined - prcpaa="${cred_profile_acc[0]}" + prcpaa=" @${cred_profile_acc[0]}" fi echo -e "${Green}${On_Black}You have one configured profile: ${BIGreen}${cred_profiles[0]} ${Green}(IAM: ${prcpu}${prcpaa})${Color_Off}" @@ -1125,7 +1125,7 @@ else prcpaa=" @${cred_profile_account_alias[$SELECTR]}" else # use the AWS account number if no alias has been defined - prcpaa="${cred_profile_acc[$SELECTR]}" + prcpaa=" @${cred_profile_acc[$SELECTR]}" fi echo -en "${BIWhite}${On_Black}${ITER}: $i${Color_Off} (IAM: ${prcpu}${prcpaa}${mfa_notify})\\n" diff --git a/awscli-mfa/enable-disable-vmfa-device.sh b/awscli-mfa/enable-disable-vmfa-device.sh index 7852f5e..2ec61cb 100755 --- a/awscli-mfa/enable-disable-vmfa-device.sh +++ b/awscli-mfa/enable-disable-vmfa-device.sh @@ -434,19 +434,20 @@ continue_maybe() { } 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; + # $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 'default' keyword 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=$1 - local aws_raw_return=$2 + local exit_on_error=$2 + local aws_raw_return=$3 local profile_in_use local custom_error - [[ "$3" == "" ]] && profile_in_use="selected" || profile_in_use="$3" - [[ "$4" == "" ]] && custom_error="" || custom_error="${4}\\n" + [[ "$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 @@ -461,8 +462,8 @@ checkAWSErrors() { 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" + 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" @@ -470,7 +471,7 @@ checkAWSErrors() { 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 + 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' ]] || @@ -491,13 +492,31 @@ checkAWSErrors() { is_error="true" fi - # do not exit on profile ingest loop - [[ "$is_error" == "true" && "$exit_on_error" == "true" ]] && exit 1 + 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 "To disable/detach a vMFAd from the profile, you must have\\nan active MFA session established with it. Use the 'awscli-mfa.sh'\\nscript to establish an MFA session for the profile first, then\\nrun this script again.\\n" - echo -e "If you do not have possession of the vMFAd for this profile\\n(in GA/Authy app), please request ops to disable the vMFAd\\nfor your profile, or if you have admin credentials for AWS,\\nuse them outside this script to disable the vMFAd for this\\nprofile." + 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() { @@ -796,7 +815,7 @@ else fi # this bails out on errors - checkAWSErrors "true" "$process_user_arn" "$currently_selected_profile_ident_printable" + 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 @@ -893,7 +912,7 @@ else [[ "$line" =~ \[(${profile_ident}-mfasession)\]$ ]] && mfa_profile_ident="${BASH_REMATCH[1]}" done < "$CREDFILE" - mfa_profiles[$cred_profilecounter]="$mfa_profile_ident" + 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; @@ -1016,7 +1035,7 @@ else selprofile="-1" break; elif [[ $REPLY =~ ^[Nn]$ ]]; then - echo -e "\\n\\nA vMFAd not disabled. Exiting.\\n" + echo -e "\\n\\nA vMFAd not disabled/detached. Exiting.\\n" exit 1 break; fi @@ -1069,7 +1088,7 @@ else 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 detach/disable.${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 -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[@]}" @@ -1175,7 +1194,7 @@ else fi # this bails out on errors - checkAWSErrors "true" "$mfa_deletion_result" "$final_selection" "Could not delete the inaccessible vMFAd. Cannot continue!" + 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." @@ -1218,7 +1237,7 @@ else fi # this bails out on errors - checkAWSErrors "true" "$vmfad_creation_status" "$final_selection" "Could not execute create-virtual-mfa-device. No virtual MFA device to enable. Cannot continue!" + 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" @@ -1263,7 +1282,7 @@ else fi # this bails out on errors - checkAWSErrors "true" "$available_user_vmfad" "$final_selection" "Could not execute list-virtual-mfa-devices. Cannot continue!" + 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 @@ -1306,7 +1325,7 @@ else fi # this bails out on errors - checkAWSErrors "true" "$vmfad_enablement_status" "$final_selection" "Could not enable vMFAd. Cannot continue.\\n${Red}Mistyped authcodes, or wrong/old vMFAd?" + 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}" @@ -1316,11 +1335,11 @@ else else echo -e "disable the vMFAd for the profile...\\n" - transient_mfa_profile_check="$(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}${transient_mfa_profile_check}${Color_Off}\\n\\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 "true" "$transient_mfa_profile_check" "transient/unknown" "Could not acquire AWS account ID or current IAM user name. A bad profile? Cannot continue!" + 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 @@ -1334,14 +1353,38 @@ else exit 1 fi + _ret_remaining="undefined" # First checking the envvars - if [[ "$PRECHECK_AWS_PROFILE" =~ ^${final_selection}-mfasession$ ]] && + if [[ "$PRECHECK_AWS_PROFILE" =~ ^${final_selection}$ || + "$PRECHECK_AWS_PROFILE" == "" ]] && + + [[ "$PRECHECK_AWS_SESSION_TOKEN" == "" ]] && + [[ "$PRECHECK_AWS_SESSION_INIT_TIME" == "" ]] && + [[ "$PRECHECK_AWS_SESSION_DURATION" == "" ]]; then + # this is a authorized (?) base profile or 'default' + + echo -en "${BIWhite}${On_Black}A base profile ${currently_selected_profile_ident} (IAM: ${process_username} ${account_alias_if_any})\\nis currently in effect. Do you want to attempt deactivation\\nwith this profile? 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 MFA profile in the environment - getRemaining _ret "$PRECHECK_AWS_SESSION_INIT_TIME" "$PRECHECK_AWS_SESSION_DURATION" + getRemaining _ret_remaining "$PRECHECK_AWS_SESSION_INIT_TIME" "$PRECHECK_AWS_SESSION_DURATION" elif [[ "$PRECHECK_AWS_PROFILE" =~ ^${final_selection}-mfasession$ ]] && [[ "$profiles_idx" != "" ]]; then @@ -1355,7 +1398,7 @@ else # the parent/base profile's duration if [[ "$profile_time" != "" ]]; then getDuration parent_duration "$PRECHECK_AWS_PROFILE" - getRemaining _ret "$profile_time" "$parent_duration" + getRemaining _ret_remaining "$profile_time" "$parent_duration" fi elif [[ "$PRECHECK_AWS_PROFILE" == "" ]] && @@ -1369,7 +1412,7 @@ else # IAM user of the transient in-env MFA session matches # the IAM user of the selected persistent base profile - getRemaining _ret "$PRECHECK_AWS_SESSION_INIT_TIME" "$PRECHECK_AWS_SESSION_DURATION" + getRemaining _ret_remaining "$PRECHECK_AWS_SESSION_INIT_TIME" "$PRECHECK_AWS_SESSION_DURATION" else echo -e "${BIRed}${On_Black}This is an unknown in-env MFA session. Cannot continue.${Color_Off}\\n" @@ -1377,25 +1420,30 @@ else exit 1 fi - else # no valid MFA profile (in-env or functional reference) found - echo -e "${BIRed}${On_Black}No active MFA session found for the profile '${final_selection}'.${Color_Off}\\n" + echo -e "${BIRed}${On_Black}No active MFA session found for the profile '${final_selection}'.\\nAn active MFA session for the profile, or an authorized\\nbase profile is required for this action.${Color_Off}\\n" print_mfa_notice echo exit 1 fi - if [[ ${_ret} -gt 120 ]]; then # at least 120 seconds of the session remain - - # below profile is not defined because an active MFA must be used + 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 admin profile must be used 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 --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" + [[ "$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 "true" "$vmfad_deactivation_result" "$final_selection" "Could not disable/detach vMFAd for the profile '${final_selection}'. Cannot continue!" + 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 @@ -1414,7 +1462,7 @@ else [[ "$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 "true" "$vmfad_delete_result" "$final_selection" "Could not delete vMFAd for the profile '${final_selection}'. Cannot continue!" + 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}" @@ -1423,7 +1471,7 @@ else echo break; elif [[ $REPLY =~ ^[Nn]$ ]]; then - echo -e "\\n\\n${BIWhite}${On_Black}The following vMFAd was detached/disabled, 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" + 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 diff --git a/awscli-mfa/example-MFA-enforcement-policy.txt b/awscli-mfa/example-MFA-enforcement-policy.txt index a14d747..f9dc05d 100644 --- a/awscli-mfa/example-MFA-enforcement-policy.txt +++ b/awscli-mfa/example-MFA-enforcement-policy.txt @@ -26,11 +26,9 @@ "Effect": "Allow", "Action": [ "iam:GetAccountPasswordPolicy", - "iam:ListAccountAliases", "iam:ListUsers" ], "Resource": [ - "arn:aws:iam::REPLACE-WITH-YOUR-AWS-ACCOUNT-ID:mfa/*", "arn:aws:iam::REPLACE-WITH-YOUR-AWS-ACCOUNT-ID:user/*" ] }, @@ -75,22 +73,6 @@ "arn:aws:iam::REPLACE-WITH-YOUR-AWS-ACCOUNT-ID:user/${aws:username}" ] }, - { - "Sid": "RequireActiveMFASessionToDeactivateMFADevice", - "Effect": "Allow", - "Action": [ - "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": { - "Bool": { - "aws:MultiFactorAuthPresent": true - } - } - }, { "Sid": "DenyEverythingExceptForBelowUnlessMFAd", "Effect": "Deny", @@ -120,6 +102,23 @@ } } }, + { + "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", diff --git a/awscli-mfa/source-to-clear-AWS-envvars.sh b/awscli-mfa/source-this-to-clear-AWS-envvars.sh similarity index 100% rename from awscli-mfa/source-to-clear-AWS-envvars.sh rename to awscli-mfa/source-this-to-clear-AWS-envvars.sh From 0a7f803473afe3ad358270af4adf6376029fd629 Mon Sep 17 00:00:00 2001 From: Ville Walveranta Date: Tue, 1 May 2018 13:02:44 -0500 Subject: [PATCH 59/71] awscli-mfa: Documentation/guidance updates. --- awscli-mfa/README.md | 4 ++-- awscli-mfa/enable-disable-vmfa-device.sh | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/awscli-mfa/README.md b/awscli-mfa/README.md index dda7cc5..c4e5053 100644 --- a/awscli-mfa/README.md +++ b/awscli-mfa/README.md @@ -29,9 +29,9 @@ These scripts provide significant interactive guidance as well as user-friendly The scripts have been tested in macOS (High Sierra with stock bash 3.2.x) as well as with Linux (Ubuntu 16.04 with modern default bash 4.3.x). The only dependency is `aws cli`, 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 `-d` / `--debug` 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. +* **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 it is named the same as their IAM username (i.e. the serial number, Arn, 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 an active MFA session with that profile; if you no longer have access to the vMFAd in your Google Authenticator or Authy app, you either need to have admin privileges to the AWS account, or 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. +* **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. diff --git a/awscli-mfa/enable-disable-vmfa-device.sh b/awscli-mfa/enable-disable-vmfa-device.sh index 2ec61cb..71a0672 100755 --- a/awscli-mfa/enable-disable-vmfa-device.sh +++ b/awscli-mfa/enable-disable-vmfa-device.sh @@ -1363,7 +1363,7 @@ else [[ "$PRECHECK_AWS_SESSION_DURATION" == "" ]]; then # this is a authorized (?) base profile or 'default' - echo -en "${BIWhite}${On_Black}A base profile ${currently_selected_profile_ident} (IAM: ${process_username} ${account_alias_if_any})\\nis currently in effect. Do you want to attempt deactivation\\nwith this profile? Y/N${Color_Off} " + echo -en "${BIWhite}${On_Black}A base profile ${currently_selected_profile_ident} (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\\nand/or for the IAM users other than itself)? Y/N${Color_Off} " while : do From 65b6021f0df1f26c90817a073654d0c8f064f1cb Mon Sep 17 00:00:00 2001 From: Ville Walveranta Date: Wed, 2 May 2018 02:39:10 -0500 Subject: [PATCH 60/71] awscli-mfa: enable-disable-vmfa-device.sh - Added secondary MFA session handling, improved guidance. --- awscli-mfa/enable-disable-vmfa-device.sh | 132 +++++++++++++++++------ 1 file changed, 101 insertions(+), 31 deletions(-) diff --git a/awscli-mfa/enable-disable-vmfa-device.sh b/awscli-mfa/enable-disable-vmfa-device.sh index 71a0672..acfddec 100755 --- a/awscli-mfa/enable-disable-vmfa-device.sh +++ b/awscli-mfa/enable-disable-vmfa-device.sh @@ -216,9 +216,9 @@ checkEnvSession() { # 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 + # 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" @@ -433,11 +433,28 @@ continue_maybe() { 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 'default' keyword if present + # $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) @@ -791,12 +808,15 @@ else idxLookup idx profiles_key_id[@] "$current_aws_access_key_id" if [[ $idx != "" ]]; then - currently_selected_profile_ident="'${profiles_ident[$idx]}'" + 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}' [transient]" + currently_selected_profile_ident="${PRECHECK_AWS_PROFILE}" + currently_selected_profile_ident_printable="'${PRECHECK_AWS_PROFILE}' [transient]" else - currently_selected_profile_ident="unknown/transient" + currently_selected_profile_ident="" + currently_selected_profile_ident_printable="transient/unknown" fi fi @@ -806,7 +826,7 @@ else if [[ "$process_user_arn" =~ 'error occurred' ]]; then continue_maybe "invalid" - currently_selected_profile_ident="'default'" + 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" @@ -833,7 +853,7 @@ else 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)." + echo "Executing this script as the AWS/IAM user $process_username $account_alias_if_any (profile $currently_selected_profile_ident_printable)." echo @@ -1355,15 +1375,23 @@ else _ret_remaining="undefined" # First checking the envvars - if [[ "$PRECHECK_AWS_PROFILE" =~ ^${final_selection}$ || - "$PRECHECK_AWS_PROFILE" == "" ]] && + if ( [[ "$PRECHECK_AWS_PROFILE" == "" ]] || + [[ ! "$PRECHECK_AWS_PROFILE" =~ -mfasession$ ]] ) && [[ "$PRECHECK_AWS_SESSION_TOKEN" == "" ]] && [[ "$PRECHECK_AWS_SESSION_INIT_TIME" == "" ]] && [[ "$PRECHECK_AWS_SESSION_DURATION" == "" ]]; then - # this is a authorized (?) base profile or 'default' + # 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} (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\\nand/or for the IAM users other than itself)? Y/N${Color_Off} " + 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 @@ -1382,13 +1410,13 @@ else [[ "$PRECHECK_AWS_SESSION_TOKEN" != "" ]] && [[ "$PRECHECK_AWS_SESSION_INIT_TIME" != "" ]] && [[ "$PRECHECK_AWS_SESSION_DURATION" != "" ]]; then - # this is a MFA profile in the environment + # 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 selected persistent MFA profile + # 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]} @@ -1405,31 +1433,73 @@ else [[ "$PRECHECK_AWS_SESSION_TOKEN" != "" ]] && [[ "$PRECHECK_AWS_SESSION_INIT_TIME" != "" ]] && [[ "$PRECHECK_AWS_SESSION_DURATION" != "" ]]; then - # this is a transient profile, check for which base profile + # this is an unknown/transient in-env profile, the base profile is in $currently_selected_profile_ident - idxLookup persistent_equivalent_idx cred_profile_user[@] "$aws_iam_user" - if [[ "${persistent_equivalent_idx}" != "" ]]; then - # IAM user of the transient in-env MFA session matches - # the IAM user of the selected persistent base profile - - getRemaining _ret_remaining "$PRECHECK_AWS_SESSION_INIT_TIME" "$PRECHECK_AWS_SESSION_DURATION" + 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}This is an unknown in-env MFA session. Cannot continue.${Color_Off}\\n" + 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 + fi - echo -e "${BIRed}${On_Black}No active MFA session found for the profile '${final_selection}'.\\nAn active MFA session for the profile, or an authorized\\nbase profile is required for this action.${Color_Off}\\n" - print_mfa_notice - echo - exit 1 + 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 admin profile must be used + # 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}" \ @@ -1447,7 +1517,7 @@ else # we didn't bail out; continuing... echo - echo -e "${BIGreen}${On_Black}vMFAd disabled/detached for the profile '${final_selection}'.${Color_Off}" + 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} " From 892f16f5968169f48ceee09c4fea9e9302a2cba8 Mon Sep 17 00:00:00 2001 From: Ville Walveranta Date: Wed, 2 May 2018 18:40:55 -0500 Subject: [PATCH 61/71] awscli-mfa scripts: reflected the source-this-to-clear-AWS-envvars.sh script name change in the utility scripts. --- awscli-mfa/awscli-mfa.sh | 6 +++--- awscli-mfa/enable-disable-vmfa-device.sh | 2 +- awscli-mfa/mfastatus.sh | 4 ++-- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/awscli-mfa/awscli-mfa.sh b/awscli-mfa/awscli-mfa.sh index 92caaab..b26624b 100755 --- a/awscli-mfa/awscli-mfa.sh +++ b/awscli-mfa/awscli-mfa.sh @@ -464,7 +464,7 @@ continue_maybe() { export AWS_PROFILE=default echo else - echo -e "\\n\\nExecute \"source ./source-to-clear-AWS-envvars.sh\", and try again to proceed.\\n" + echo -e "\\n\\nExecute \"source ./source-this-to-clear-AWS-envvars.sh\", and try again to proceed.\\n" exit 1 fi fi @@ -1569,7 +1569,7 @@ else echo echo -e "*** 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 sts get-caller-identity --profile default${Color_Off}" echo - 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-to-clear-AWS-envvars.sh" + 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" echo echo -e "${BIWhite}${On_Black}PASTE THE PROFILE ACTIVATION COMMAND FROM THE CLIPBOARD\\nON THE COMMAND LINE NOW, AND PRESS ENTER! THEN YOU'RE DONE!${Color_Off}" echo @@ -1634,7 +1634,7 @@ else echo echo "*** To easily remove any all AWS profile settings and secrets information" echo " from the environment, simply source the included script, like so:" - echo " source ./source-to-clear-AWS-envvars.sh" + echo " source ./source-this-to-clear-AWS-envvars.sh" echo fi diff --git a/awscli-mfa/enable-disable-vmfa-device.sh b/awscli-mfa/enable-disable-vmfa-device.sh index acfddec..a0a7b36 100755 --- a/awscli-mfa/enable-disable-vmfa-device.sh +++ b/awscli-mfa/enable-disable-vmfa-device.sh @@ -427,7 +427,7 @@ continue_maybe() { export AWS_PROFILE=default echo else - echo -e "\\n\\nExecute \"source ./source-to-clear-AWS-envvars.sh\", and try again to proceed.\\n" + echo -e "\\n\\nExecute \"source ./source-this-to-clear-AWS-envvars.sh\", and try again to proceed.\\n" exit 1 fi fi diff --git a/awscli-mfa/mfastatus.sh b/awscli-mfa/mfastatus.sh index cbb300e..5ad518d 100755 --- a/awscli-mfa/mfastatus.sh +++ b/awscli-mfa/mfastatus.sh @@ -266,7 +266,7 @@ sessionData() { 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-to-clear-AWS-envvars.sh'${Color_Off}" + 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 @@ -518,7 +518,7 @@ if [[ "$AWS_PROFILE" != "" ]]; then 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-to-clear-AWS-envvars.sh${BIRed} to clear the environment.\\n${Color_Off}" + 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 From dd5ceb824aa0039624352779c1bf6ac238c9d543 Mon Sep 17 00:00:00 2001 From: Ville Walveranta Date: Thu, 3 May 2018 15:41:56 -0500 Subject: [PATCH 62/71] Typo fixes. Added bash version to debug output. --- awscli-mfa/awscli-mfa.sh | 10 ++++++---- awscli-mfa/enable-disable-vmfa-device.sh | 8 +++++--- awscli-mfa/mfastatus.sh | 4 ++-- 3 files changed, 13 insertions(+), 9 deletions(-) diff --git a/awscli-mfa/awscli-mfa.sh b/awscli-mfa/awscli-mfa.sh index b26624b..5cb59ea 100755 --- a/awscli-mfa/awscli-mfa.sh +++ b/awscli-mfa/awscli-mfa.sh @@ -22,7 +22,7 @@ DEBUG="false" # about how long a token will continue to be valid. # # THIS VALUE CAN BE OPTIONALLY OVERRIDDEN PER EACH PROFILE -# BY ADDING A "mfafsec" ENTRY FOR THE PROFILE IN ~/.aws/config +# 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 @@ -114,10 +114,12 @@ On_IPurple='\033[0;105m' # Purple On_ICyan='\033[0;106m' # Cyan On_IWhite='\033[0;107m' # White -# DEBUG MODE WARNING ========================================================= - -[[ "$DEBUG" == "true" ]] && 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" +# 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 ================================================================== diff --git a/awscli-mfa/enable-disable-vmfa-device.sh b/awscli-mfa/enable-disable-vmfa-device.sh index a0a7b36..3d8f81b 100755 --- a/awscli-mfa/enable-disable-vmfa-device.sh +++ b/awscli-mfa/enable-disable-vmfa-device.sh @@ -114,10 +114,12 @@ On_ICyan='\033[0;106m' # Cyan On_IWhite='\033[0;107m' # White -# DEBUG MODE WARNING ========================================================= - -[[ "$DEBUG" == "true" ]] && 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" +# 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 ================================================================== diff --git a/awscli-mfa/mfastatus.sh b/awscli-mfa/mfastatus.sh index 5ad518d..5c9f89e 100755 --- a/awscli-mfa/mfastatus.sh +++ b/awscli-mfa/mfastatus.sh @@ -1,4 +1,4 @@ -#!/bin/bash +#!/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 @@ -567,7 +567,7 @@ do 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}" + 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 From 39c524d1ee4dc226416b896dcef075b900876a9d Mon Sep 17 00:00:00 2001 From: Ville Walveranta Date: Sat, 5 May 2018 03:02:20 -0500 Subject: [PATCH 63/71] awscli-mfa: documentation updates; added support for grouped IAM usernames --- awscli-mfa/README.md | 6 +++--- awscli-mfa/awscli-mfa.sh | 4 ++-- awscli-mfa/enable-disable-vmfa-device.sh | 10 +++++----- awscli-mfa/mfastatus.sh | 4 ++-- 4 files changed, 12 insertions(+), 12 deletions(-) diff --git a/awscli-mfa/README.md b/awscli-mfa/README.md index c4e5053..dde993c 100644 --- a/awscli-mfa/README.md +++ b/awscli-mfa/README.md @@ -5,13 +5,13 @@ The `awscli-mfa.sh` and its companion scripts `enable-disable-vmfa-device.sh` `m ### 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 possible 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). +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 activity periods for the current MFA sessions using the `mfastatus.sh` 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. @@ -27,7 +27,7 @@ The `awscli-mfa.sh` and its companion scripts change all this by making use of t 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) as well as with Linux (Ubuntu 16.04 with modern default bash 4.3.x). The only dependency is `aws cli`, and the scripts will notify the user if `aws cli` is not present. +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. diff --git a/awscli-mfa/awscli-mfa.sh b/awscli-mfa/awscli-mfa.sh index 5cb59ea..46deebc 100755 --- a/awscli-mfa/awscli-mfa.sh +++ b/awscli-mfa/awscli-mfa.sh @@ -836,7 +836,7 @@ else # 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 + if [[ "$process_user_arn" =~ ([[:digit:]]+):user.*/([^/]+)$ ]]; then profile_user_acc="${BASH_REMATCH[1]}" process_username="${BASH_REMATCH[2]}" fi @@ -908,7 +908,7 @@ else # get the actual username # (may be different from the arbitrary profile ident) - if [[ "$user_arn" =~ ([[:digit:]]+):user/([^/]+)$ ]]; then + if [[ "$user_arn" =~ ([[:digit:]]+):user.*/([^/]+)$ ]]; then profile_user_acc="${BASH_REMATCH[1]}" profile_username="${BASH_REMATCH[2]}" fi diff --git a/awscli-mfa/enable-disable-vmfa-device.sh b/awscli-mfa/enable-disable-vmfa-device.sh index 3d8f81b..6a416d6 100755 --- a/awscli-mfa/enable-disable-vmfa-device.sh +++ b/awscli-mfa/enable-disable-vmfa-device.sh @@ -441,7 +441,7 @@ yesno() { old_stty_cfg=$(stty -g) stty raw -echo answer=$( while ! head -c 1 | grep -i '[yn]' ;do true ;done ) - stty $old_stty_cfg + stty "$old_stty_cfg" if echo "$answer" | grep -iq "^n" ; then _ret="no" @@ -842,7 +842,7 @@ else # 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 + if [[ "$process_user_arn" =~ ([[:digit:]]+):user.*/([^/]+)$ ]]; then profile_user_acc="${BASH_REMATCH[1]}" process_username="${BASH_REMATCH[2]}" fi @@ -1171,7 +1171,7 @@ else selected_profile_arn=${cred_profile_arn[idx]} - if [[ "$selected_profile_arn" =~ ^arn:aws:iam::([[:digit:]]*):user/(.*)$ ]]; then + if [[ "$selected_profile_arn" =~ ^arn:aws:iam::([[:digit:]]+):user.*/([^/]+)$ ]]; then aws_account_id="${BASH_REMATCH[1]}" aws_iam_user="${BASH_REMATCH[2]}" else @@ -1300,7 +1300,7 @@ else --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" + 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 @@ -1364,7 +1364,7 @@ else 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 + 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 diff --git a/awscli-mfa/mfastatus.sh b/awscli-mfa/mfastatus.sh index 5c9f89e..3ae422d 100755 --- a/awscli-mfa/mfastatus.sh +++ b/awscli-mfa/mfastatus.sh @@ -240,7 +240,7 @@ sessionData() { 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 + 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'" @@ -549,7 +549,7 @@ do 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 + 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" From 21738a9182e116629d64f2ccae7a9703bcc5e44c Mon Sep 17 00:00:00 2001 From: Ville Walveranta Date: Mon, 14 May 2018 03:30:37 -0500 Subject: [PATCH 64/71] Started role handling. Minor bug fixes. --- awscli-mfa/awscli-mfa.sh | 29 ++++++++++++++++++------ awscli-mfa/enable-disable-vmfa-device.sh | 6 ++--- awscli-mfa/mfastatus.sh | 2 +- 3 files changed, 26 insertions(+), 11 deletions(-) diff --git a/awscli-mfa/awscli-mfa.sh b/awscli-mfa/awscli-mfa.sh index 46deebc..4a48c38 100755 --- a/awscli-mfa/awscli-mfa.sh +++ b/awscli-mfa/awscli-mfa.sh @@ -690,6 +690,8 @@ else # define profiles arrays, variables declare -a profiles_ident declare -a profiles_type + declare -a profiles_role_arn + declare -a profiles_role_source declare -a profiles_key_id declare -a profiles_secret_key declare -a profiles_session_token @@ -725,17 +727,25 @@ else fi fi - [[ "$line" =~ ^aws_access_key_id[[:space:]]*=[[:space:]]*(.*)$ ]] && + [[ "$line" =~ ^[[:space:]]*aws_access_key_id[[:space:]]*=[[:space:]]*(.*)$ ]] && profiles_key_id[$profiles_iterator]="${BASH_REMATCH[1]}" - [[ "$line" =~ ^aws_secret_access_key[[:space:]]*=[[:space:]]*(.*)$ ]] && + [[ "$line" =~ ^[[:space:]]*aws_secret_access_key[[:space:]]*=[[:space:]]*(.*)$ ]] && profiles_secret_key[$profiles_iterator]="${BASH_REMATCH[1]}" - [[ "$line" =~ ^aws_session_token[[:space:]]*=[[:space:]]*(.*)$ ]] && + [[ "$line" =~ ^[[:space:]]*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]} + [[ "$line" =~ ^[[:space:]]*aws_session_init_time[[:space:]]*=[[:space:]]*(.*)$ ]] && + profiles_session_init_time[$profiles_iterator]="${BASH_REMATCH[1]}" + + if [[ "$line" =~ ^[[:space:]]*role_arn[[:space:]]*=[[:space:]]*(.*)$ ]]; then + profiles_type[$profiles_iterator]="role" + profiles_role_arn[$profiles_iterator]="${BASH_REMATCH[1]}" + fi + + [[ "$line" =~ ^[[:space:]]*source_profile[[:space:]]*=[[:space:]]*(.*)$ ]] && + profiles_role_source[$profiles_iterator]="${BASH_REMATCH[1]}" done < "$CREDFILE" @@ -883,7 +893,8 @@ else # and if it's not a mfasession profile # (mfasession profiles have '-mfasession' postfix) if [[ "$profile_ident" != "" ]] && - ! [[ "$profile_ident" =~ -mfasession$ ]]; then + [[ ! "$profile_ident" =~ -mfasession$ ]] && + [[ ! "$profile_ident" =~ -rolesession$ ]] ; then # store this profile ident cred_profiles[$cred_profilecounter]="$profile_ident" @@ -1031,6 +1042,10 @@ else ((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}" @@ -1560,7 +1575,7 @@ else echo if [[ "$OS" == "Linux" ]]; then if exists xclip; then - echo "${BIGreen}${On_Black}*** NOTE: xclip found; the envvar configuration command is now on your X PRIMARY clipboard -- just paste on the command line, and press [ENTER])${Color_Off}" + echo -e "${BIGreen}${On_Black}*** NOTE: xclip found; the envvar configuration command is now on your X PRIMARY clipboard -- just paste on the command line, and press [ENTER])${Color_Off}" else echo echo "*** NOTE: If you're using an X GUI on Linux, install 'xclip' to have the activation command copied to the clipboard automatically!" diff --git a/awscli-mfa/enable-disable-vmfa-device.sh b/awscli-mfa/enable-disable-vmfa-device.sh index 6a416d6..fc8391c 100755 --- a/awscli-mfa/enable-disable-vmfa-device.sh +++ b/awscli-mfa/enable-disable-vmfa-device.sh @@ -714,7 +714,7 @@ else fi if [[ "$_ret" != "" ]] && - ! [[ "$_ret" =~ -mfasession$ ]]; then + [[ ! "$_ret" =~ -mfasession$ ]]; then profiles_type[$profiles_iterator]="profile" else @@ -823,7 +823,7 @@ else 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" + [[ "$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" @@ -891,7 +891,7 @@ else # and if it's not a mfasession profile # (mfasession profiles have '-mfasession' postfix) if [[ "$profile_ident" != "" ]] && - ! [[ "$profile_ident" =~ -mfasession$ ]]; then + [[ ! "$profile_ident" =~ -mfasession$ ]]; then # store this profile ident cred_profiles[$cred_profilecounter]="$profile_ident" diff --git a/awscli-mfa/mfastatus.sh b/awscli-mfa/mfastatus.sh index 3ae422d..ad8b8b8 100755 --- a/awscli-mfa/mfastatus.sh +++ b/awscli-mfa/mfastatus.sh @@ -476,7 +476,7 @@ while IFS='' read -r line || [[ -n "$line" ]]; do fi if [[ "$_ret" != "" ]] && - ! [[ "$_ret" =~ -mfasession$ ]]; then + [[ ! "$_ret" =~ -mfasession$ ]]; then profiles_type[$profiles_iterator]="profile" else From 1d8fc8ccdcec09d66f67195cc3e08068977ee395 Mon Sep 17 00:00:00 2001 From: Ville Walveranta Date: Mon, 14 May 2018 03:44:03 -0500 Subject: [PATCH 65/71] Linux compatibility fix (xclip) --- awscli-mfa/awscli-mfa.sh | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/awscli-mfa/awscli-mfa.sh b/awscli-mfa/awscli-mfa.sh index 4a48c38..38659c8 100755 --- a/awscli-mfa/awscli-mfa.sh +++ b/awscli-mfa/awscli-mfa.sh @@ -1509,6 +1509,8 @@ else exists xclip; then echo -n "$envvar_config" | xclip -i + xclip -o | xclip -sel clip + echo fi echo "unset AWS_PROFILE" @@ -1520,6 +1522,7 @@ else exists xclip; then echo -n "$envvar_config" | xclip -i + xclip -o | xclip -sel clip fi echo "export AWS_PROFILE=\"${final_selection}\"" fi @@ -1555,6 +1558,7 @@ else exists xclip; then echo -n "$envvar_config" | xclip -i + xclip -o | xclip -sel clip fi else echo "unset AWS_SESSION_INIT_TIME" @@ -1569,6 +1573,7 @@ else exists xclip; then echo -n "$envvar_config" | xclip -i + xclip -o | xclip -sel clip fi fi fi From e786e42b0dd63faf9dbe9153d9a3f69bf2f0b123 Mon Sep 17 00:00:00 2001 From: Ville Walveranta Date: Tue, 15 May 2018 02:11:37 -0500 Subject: [PATCH 66/71] awscli-mfa.sh: improvement for clarity; example-MFA-enforcement-policy.txt: allow GetRole without MFA, sorted API lists --- awscli-mfa/awscli-mfa.sh | 2 +- awscli-mfa/example-MFA-enforcement-policy.txt | 47 ++++++++++++------- 2 files changed, 30 insertions(+), 19 deletions(-) diff --git a/awscli-mfa/awscli-mfa.sh b/awscli-mfa/awscli-mfa.sh index 38659c8..6b7cb79 100755 --- a/awscli-mfa/awscli-mfa.sh +++ b/awscli-mfa/awscli-mfa.sh @@ -1289,7 +1289,7 @@ else # 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 >> "$CONFFILE" + echo -en "\\n\\n">> "$CONFFILE" echo "[profile ${AWS_2AUTH_PROFILE}]" >> "$CONFFILE" fi diff --git a/awscli-mfa/example-MFA-enforcement-policy.txt b/awscli-mfa/example-MFA-enforcement-policy.txt index f9dc05d..d3f83f1 100644 --- a/awscli-mfa/example-MFA-enforcement-policy.txt +++ b/awscli-mfa/example-MFA-enforcement-policy.txt @@ -32,6 +32,16 @@ "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", @@ -77,23 +87,24 @@ "Sid": "DenyEverythingExceptForBelowUnlessMFAd", "Effect": "Deny", "NotAction": [ - "iam:ListMFADevices", - "iam:ListVirtualMFADevices", - "iam:CreateVirtualMFADevice", - "iam:DeleteVirtualMFADevice", - "iam:EnableMFADevice", - "iam:ResyncMFADevice", - "iam:ListUsers", - "iam:ListAccountAliases", "iam:ChangePassword", + "iam:CreateAccessKey", "iam:CreateLoginProfile", + "iam:CreateVirtualMFADevice", "iam:DeleteLoginProfile", + "iam:DeleteVirtualMFADevice", + "iam:EnableMFADevice", "iam:GetAccountPasswordPolicy", "iam:GetAccountSummary", "iam:GetLoginProfile", - "iam:UpdateLoginProfile", - "iam:CreateAccessKey", - "iam:ListAccessKeys" + "iam:GetRole", + "iam:ListAccessKeys", + "iam:ListAccountAliases", + "iam:ListMFADevices", + "iam:ListUsers", + "iam:ListVirtualMFADevices", + "iam:ResyncMFADevice", + "iam:UpdateLoginProfile" ], "Resource": "*", "Condition": { @@ -123,19 +134,19 @@ "Sid": "DenyIamAccessToOtherAccountsUnlessMFAd", "Effect": "Deny", "Action": [ + "iam:ChangePassword", + "iam:CreateAccessKey", + "iam:CreateLoginProfile", "iam:CreateVirtualMFADevice", "iam:DeactivateMFADevice", + "iam:DeleteLoginProfile", "iam:DeleteVirtualMFADevice", "iam:EnableMFADevice", - "iam:ResyncMFADevice", - "iam:ChangePassword", - "iam:CreateLoginProfile", - "iam:DeleteLoginProfile", "iam:GetAccountPasswordPolicy", "iam:GetLoginProfile", - "iam:UpdateLoginProfile", - "iam:CreateAccessKey", - "iam:ListAccessKeys" + "iam:ListAccessKeys", + "iam:ResyncMFADevice", + "iam:UpdateLoginProfile" ], "NotResource": [ "arn:aws:iam::REPLACE-WITH-YOUR-AWS-ACCOUNT-ID:mfa/${aws:username}", From b6d8fd69eff852482996088ddfb0482ea8220ee0 Mon Sep 17 00:00:00 2001 From: Ville Walveranta Date: Sat, 26 May 2018 16:06:26 -0500 Subject: [PATCH 67/71] awscli-mfa: bug fixes; adding support for credentials in the config file (valid by AWS docs). --- awscli-mfa/awscli-mfa.sh | 187 +++++++++++++++++++++++++++++++-------- awscli-mfa/test.sh | 28 ++++++ 2 files changed, 177 insertions(+), 38 deletions(-) create mode 100755 awscli-mfa/test.sh diff --git a/awscli-mfa/awscli-mfa.sh b/awscli-mfa/awscli-mfa.sh index 6b7cb79..a601626 100755 --- a/awscli-mfa/awscli-mfa.sh +++ b/awscli-mfa/awscli-mfa.sh @@ -264,7 +264,6 @@ checkEnvSession() { [[ "$PRECHECK_AWS_CONFIG_FILE" != "" ]] && echo " AWS_CONFIG_FILE: $PRECHECK_AWS_CONFIG_FILE" echo fi - } # workaround function for lack of @@ -571,14 +570,14 @@ This script requires the AWS CLI. See the details here: http://docs.aws.amazon.c 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" == "" ]] && +# 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}\\nMake sure it exists, and that you have at least one profile configured\\nusing the 'config' and 'credentials' files within that directory." + 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/or 'credentials' files within that directory." filexit="true" fi @@ -594,14 +593,25 @@ 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." + 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}\\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." + 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 +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 @@ -617,15 +627,22 @@ 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." + 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 "${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" + echo -e "${BIWhite}${On_Black}** NOTE: A shared credentials file (~/.aws/credentials) was not found.\\nAssuming credentials are stored in the config file (~/.aws/config).${Color_Off}" fi if [[ "$filexit" == "true" ]]; then @@ -637,21 +654,86 @@ CONFFILE="$active_config_file" CREDFILE="$active_credentials_file" custom_configfiles_reset="false" -# read the credentials file and make sure that at least one profile is configured +# read the config and/or credentials files, +# 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]}" +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 -done < "$CREDFILE" + + if [[ "$line" =~ ^[[:space:]]*output.* ]] || + [[ "$line" =~ ^[[:space:]]*region.* ]] || + [[ "$line" =~ ^[[:space:]]*role_arn.* ]] || + [[ "$line" =~ ^[[:space:]]*source_profile.* ]] || + [[ "$line" =~ ^[[:space:]]*credential_source.* ]] || + [[ "$line" =~ ^[[:space:]]*cli_timestamp_format.* ]] || + [[ "$line" =~ ^[[:space:]]*ca_bundle.* ]] || + [[ "$line" =~ ^[[:space:]]*parameter_validation.* ]] || + [[ "$line" =~ ^[[:space:]]*external_id.* ]] || + [[ "$line" =~ ^[[:space:]]*mfa_serial.* ]] || + [[ "$line" =~ ^[[:space:]]*role_session_name.* ]]; 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\ + 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}\\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 + 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 @@ -672,13 +754,21 @@ else ;; esac - # make sure ~/.aws/credentials has a linefeed in the end - c=$(tail -c 1 "$CREDFILE") - if [[ "$c" != "" ]]; then - echo "" >> "$CREDFILE" + # 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 ~/.aws/config has a linefeed in the end + # make sure the selected CONFFILE has a linefeed in the end c=$(tail -c 1 "$CONFFILE") if [[ "$c" != "" ]]; then echo "" >> "$CONFFILE" @@ -690,8 +780,6 @@ else # define profiles arrays, variables declare -a profiles_ident declare -a profiles_type - declare -a profiles_role_arn - declare -a profiles_role_source declare -a profiles_key_id declare -a profiles_secret_key declare -a profiles_session_token @@ -700,10 +788,11 @@ else profiles_iterator=0 profiles_init=0 - # ugly hack to relate different values because + # 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]}" @@ -740,21 +829,35 @@ else profiles_session_init_time[$profiles_iterator]="${BASH_REMATCH[1]}" if [[ "$line" =~ ^[[:space:]]*role_arn[[:space:]]*=[[:space:]]*(.*)$ ]]; then - profiles_type[$profiles_iterator]="role" - profiles_role_arn[$profiles_iterator]="${BASH_REMATCH[1]}" - fi + this_role="${BASH_REMATCH[1]}" - [[ "$line" =~ ^[[:space:]]*source_profile[[:space:]]*=[[:space:]]*(.*)$ ]] && - profiles_role_source[$profiles_iterator]="${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\ + You can only define roles in the config file ($CONFFILE).\\n" - done < "$CREDFILE" + fi + done < "$CREDFILE" - # init arrays to hold ident<->mfasec detail + # init arrays to hold profile configuration detail + # (may also include credentials) declare -a confs_ident - declare -a confs_region declare -a confs_output + declare -a confs_region declare -a confs_mfasec + declare -a confs_role_arn + declare -a confs_role_source + declare -a confs_key_id + declare -a confs_secret_key + declare -a confs_session_token + declare -a confs_credential_source + declare -a confs_cli_timestamp_format + declare -a confs_ca_bundle + declare -a confs_parameter_validation + declare -a confs_external_id + declare -a confs_mfa_serial + declare -a confs_role_session_name confs_init=0 confs_iterator=0 @@ -782,6 +885,8 @@ else [[ "$line" =~ ^[[:space:]]*mfasec[[:space:]]*=[[:space:]]*(.*)$ ]] && confs_mfasec[$confs_iterator]=${BASH_REMATCH[1]} +#todo: add here the rest of the var read-ins + done < "$CONFFILE" # make sure environment has either no config or a functional config @@ -1054,13 +1159,16 @@ else mfa_req="false" if [[ ${#cred_profiles[@]} == 1 ]]; then echo - [[ "${cred_profile_user[0]}" != "" ]] && prcpu="${cred_profile_user[0]}" || prcpu="unknown -- a bad profile? " + [[ "${cred_profile_user[0]}" != "" ]] && prcpu="${cred_profile_user[0]}" || prcpu="unknown (a bad profile?)" if [[ "${cred_profile_account_alias[0]}" != "" ]]; then prcpaa=" @${cred_profile_account_alias[0]}" - else + elif [[ "${cred_profile_acc[0]}" != "" ]]; then # use the AWS account number if no alias has been defined prcpaa=" @${cred_profile_acc[0]}" + else + # or nothing for a bad profile + prcpaa="" fi echo -e "${Green}${On_Black}You have one configured profile: ${BIGreen}${cred_profiles[0]} ${Green}(IAM: ${prcpu}${prcpaa})${Color_Off}" @@ -1136,13 +1244,16 @@ else mfa_notify="; vMFAd not configured" fi - [[ "${cred_profile_user[$SELECTR]}" != "" ]] && prcpu="${cred_profile_user[$SELECTR]}" || prcpu="unknown -- a bad profile?" + [[ "${cred_profile_user[$SELECTR]}" != "" ]] && prcpu="${cred_profile_user[$SELECTR]}" || prcpu="unknown (a bad profile?)" if [[ "${cred_profile_account_alias[$SELECTR]}" != "" ]]; then prcpaa=" @${cred_profile_account_alias[$SELECTR]}" - else + elif [[ "${cred_profile_acc[$SELECTR]}" != "" ]]; then # use the AWS account number if no alias has been defined prcpaa=" @${cred_profile_acc[$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" 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 From 034374c0249b084a7911e6e69190fe8a56eb15ef Mon Sep 17 00:00:00 2001 From: Ville Walveranta Date: Mon, 28 May 2018 04:16:15 -0500 Subject: [PATCH 68/71] awscli-mfa: massive refactoring for roles support, clarity (WIP); eliminated the dependency for the default profile --- awscli-mfa/awscli-mfa.sh | 724 +++++++++++++++++++++++++-------------- 1 file changed, 460 insertions(+), 264 deletions(-) diff --git a/awscli-mfa/awscli-mfa.sh b/awscli-mfa/awscli-mfa.sh index a601626..0faf55d 100755 --- a/awscli-mfa/awscli-mfa.sh +++ b/awscli-mfa/awscli-mfa.sh @@ -2,6 +2,7 @@ # 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" @@ -185,7 +186,7 @@ checkEnvSession() { # AWS_PROFILE is set to an unknown value!) if [[ "$PRECHECK_AWS_PROFILE" != "" ]]; then - idxLookup profiles_idx profiles_ident[@] "$PRECHECK_AWS_PROFILE" + idxLookup profiles_idx creds_ident[@] "$PRECHECK_AWS_PROFILE" idxLookup confs_idx confs_ident[@] "$PRECHECK_AWS_PROFILE" if [[ "$profiles_idx" == "" ]] && [[ "$confs_idx" == "" ]]; then @@ -216,7 +217,7 @@ checkEnvSession() { # 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]} + 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 @@ -244,7 +245,7 @@ checkEnvSession() { [[ "${AWS_CONFIG_FILE}" != "" ]]; then echo - echo "** NOTE: THE FOLLOWING AWS_* ENVIRONMENT VARIABLES ARE CURRENTLY IN EFFECT:" + echo "NOTE: THE FOLLOWING AWS_* ENVIRONMENT VARIABLES ARE CURRENTLY IN EFFECT:" echo if [[ "$PRECHECK_AWS_PROFILE" != "$AWS_PROFILE" ]]; then env_notice=" (overridden to 'default')" @@ -315,7 +316,8 @@ addInitTime() { 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}]" + # 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}" @@ -323,8 +325,8 @@ addInitTime() { # update the selected profile's existing # init time entry in this script - idxLookup idx profiles_ident[@] "$this_ident" - profiles_session_init_time[$idx]=$this_time + idxLookup idx creds_ident[@] "$this_ident" + creds_aws_mfasession_init_time[$idx]=$this_time } # return the MFA session init time for the given profile @@ -336,8 +338,8 @@ getInitTime() { 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]} + idxLookup idx creds_ident[@] "$this_ident" + profile_time=${creds_aws_mfasession_init_time[$idx]} eval "$1=${profile_time}" } @@ -417,6 +419,27 @@ getPrintableTimeRemaining() { 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() { @@ -427,18 +450,20 @@ continue_maybe() { 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" + 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}THE AWS PROFILE SELECTED/CONFIGURED IN THE ENVIRONMENT IS INVALID.${Color_Off}\\n" + 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 defaut profile is already selected + # 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 @@ -468,8 +493,11 @@ continue_maybe() { 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) @@ -537,17 +565,20 @@ getAccountAlias() { # $1 is _ret (returns the index) # $2 is the profile_ident - local local_profile_ident=$2 + local local_profile_ident="$2" - if [[ "$local_profile_ident" != "" ]]; then - profile_param="--profile $local_profile_ident" - else - profile_param="" + 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 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" + 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 @@ -561,6 +592,8 @@ getAccountAlias() { ## PREREQUISITES CHECK +#todo: add awscli *version* check + # is AWS CLI installed? if ! exists aws ; then printf "\\n******************************************************************************************************************************\\n\ @@ -577,7 +610,10 @@ if ( [[ "$AWS_CONFIG_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/or 'credentials' files within that directory." + 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 @@ -587,7 +623,8 @@ if [[ "$AWS_CONFIG_FILE" != "" ]] && 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}" + 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 @@ -609,7 +646,7 @@ else 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 +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" @@ -619,9 +656,10 @@ fi if [[ "$AWS_SHARED_CREDENTIALS_FILE" != "" ]] && [ -f "$AWS_SHARED_CREDENTIALS_FILE" ]; then - active_credentials_file=$AWS_SHARED_CREDENTIALS_FILE + 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}" + 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 @@ -630,7 +668,7 @@ elif [[ "$AWS_SHARED_CREDENTIALS_FILE" != "" ]] && 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\ +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." @@ -642,7 +680,9 @@ 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.\\nAssuming credentials are stored in the config file (~/.aws/config).${Color_Off}" + 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 @@ -654,7 +694,7 @@ CONFFILE="$active_config_file" CREDFILE="$active_credentials_file" custom_configfiles_reset="false" -# read the config and/or credentials files, +# read the credentials and/or config files, # and make sure that at least one profile is configured ONEPROFILE="false" conffile_vars_in_credfile="false" @@ -668,17 +708,17 @@ if [[ $CREDFILE != "" ]]; then ONEPROFILE="true" fi - if [[ "$line" =~ ^[[:space:]]*output.* ]] || - [[ "$line" =~ ^[[:space:]]*region.* ]] || - [[ "$line" =~ ^[[:space:]]*role_arn.* ]] || - [[ "$line" =~ ^[[:space:]]*source_profile.* ]] || - [[ "$line" =~ ^[[:space:]]*credential_source.* ]] || + if [[ "$line" =~ ^[[:space:]]*ca_bundle.* ]] || [[ "$line" =~ ^[[:space:]]*cli_timestamp_format.* ]] || - [[ "$line" =~ ^[[:space:]]*ca_bundle.* ]] || - [[ "$line" =~ ^[[:space:]]*parameter_validation.* ]] || + [[ "$line" =~ ^[[:space:]]*credential_source.* ]] || [[ "$line" =~ ^[[:space:]]*external_id.* ]] || [[ "$line" =~ ^[[:space:]]*mfa_serial.* ]] || - [[ "$line" =~ ^[[:space:]]*role_session_name.* ]]; then + [[ "$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 @@ -689,8 +729,9 @@ 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\ - Please see https://docs.aws.amazon.com/cli/latest/userguide/cli-config-files.html\\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 @@ -749,7 +790,7 @@ else *) OS='unknown' echo - echo "** NOTE: THIS SCRIPT HAS NOT BEEN TESTED ON YOUR CURRENT PLATFORM." + echo "NOTE: THIS SCRIPT HAS NOT BEEN TESTED ON YOUR CURRENT PLATFORM." echo ;; esac @@ -778,12 +819,13 @@ else ## 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 + 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 @@ -798,43 +840,53 @@ else _ret="${BASH_REMATCH[1]}" if [[ $profiles_init -eq 0 ]]; then - profiles_ident[$profiles_iterator]=$_ret + creds_ident[$profiles_iterator]="${_ret}" profiles_init=1 fi if [[ "$_ret" != "" ]] && - ! [[ "$_ret" =~ -mfasession$ ]]; then + [[ "$_ret" =~ -mfasession$ ]]; then - profiles_type[$profiles_iterator]="profile" + creds_type[$profiles_iterator]="mfasession" + elif [[ "$_ret" != "" ]] && + [[ "$_ret" =~ -rolesession$ ]]; then + + creds_type[$profiles_iterator]="rolesession" else - profiles_type[$profiles_iterator]="session" + creds_type[$profiles_iterator]="baseprofile" fi - if [[ "${profiles_ident[$profiles_iterator]}" != "$_ret" ]]; then + if [[ "${creds_ident[$profiles_iterator]}" != "$_ret" ]]; then ((profiles_iterator++)) - profiles_ident[$profiles_iterator]=$_ret + creds_ident[$profiles_iterator]=$_ret fi fi - [[ "$line" =~ ^[[:space:]]*aws_access_key_id[[:space:]]*=[[:space:]]*(.*)$ ]] && - profiles_key_id[$profiles_iterator]="${BASH_REMATCH[1]}" + # aws_access_key_id + [[ "$line" =~ ^[[:space:]]*aws_access_key_id[[:space:]]*=[[:space:]]*(.*)[[:space:]]*$ ]] && + creds_aws_access_key_id[$profiles_iterator]="${BASH_REMATCH[1]}" - [[ "$line" =~ ^[[:space:]]*aws_secret_access_key[[:space:]]*=[[:space:]]*(.*)$ ]] && - profiles_secret_key[$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]}" - [[ "$line" =~ ^[[:space:]]*aws_session_token[[:space:]]*=[[:space:]]*(.*)$ ]] && - profiles_session_token[$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]}" - [[ "$line" =~ ^[[:space:]]*aws_session_init_time[[:space:]]*=[[:space:]]*(.*)$ ]] && - profiles_session_init_time[$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]}" - if [[ "$line" =~ ^[[:space:]]*role_arn[[:space:]]*=[[:space:]]*(.*)$ ]]; then + # 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\ - You can only define roles in the config file ($CONFFILE).\\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 @@ -843,21 +895,29 @@ NOTE: The role '${BASH_REMATCH[1]}' is defined in\\n\ # init arrays to hold profile configuration detail # (may also include credentials) declare -a confs_ident - declare -a confs_output - declare -a confs_region - declare -a confs_mfasec - declare -a confs_role_arn - declare -a confs_role_source - declare -a confs_key_id - declare -a confs_secret_key - declare -a confs_session_token - declare -a confs_credential_source - declare -a confs_cli_timestamp_format + +#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_parameter_validation + 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 @@ -868,52 +928,130 @@ NOTE: The role '${BASH_REMATCH[1]}' is defined in\\n\ _ret="${BASH_REMATCH[1]}" if [[ $confs_init -eq 0 ]]; then - confs_ident[$confs_iterator]=$_ret + confs_ident[$confs_iterator]="${_ret}" confs_init=1 elif [[ "${confs_ident[$confs_iterator]}" != "$_ret" ]]; then ((confs_iterator++)) - confs_ident[$confs_iterator]=$_ret + confs_ident[$confs_iterator]="${_ret}" fi + + # assume baseprofile type; this is overridden for roles + confs_type[$confs_iterator]="baseprofile" fi - [[ "$line" =~ ^[[:space:]]*region[[:space:]]*=[[:space:]]*(.*)$ ]] && - confs_region[$confs_iterator]=${BASH_REMATCH[1]} + # aws_access_key_id + [[ "$line" =~ ^[[:space:]]*aws_access_key_id[[:space:]]*=[[:space:]]*(.*)[[:space:]]*$ ]] && + confs_aws_access_key_id[$confs_iterator]="${BASH_REMATCH[1]}" - [[ "$line" =~ ^[[:space:]]*output[[:space:]]*=[[:space:]]*(.*)$ ]] && - confs_output[$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]} - [[ "$line" =~ ^[[:space:]]*mfasec[[:space:]]*=[[:space:]]*(.*)$ ]] && + # 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]} -#todo: add here the rest of the var read-ins + # 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 + # 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" + # (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 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" + 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_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 + 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_output" == "" ]]; then - aws configure set output "table" + 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 @@ -921,10 +1059,10 @@ NOTE: The role '${BASH_REMATCH[1]}' is defined in\\n\ [[ "$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 profiles_key_id[@] "$current_aws_access_key_id" + idxLookup idx creds_aws_access_key_id[@] "$current_aws_access_key_id" if [[ $idx != "" ]]; then - currently_selected_profile_ident_printable="'${profiles_ident[$idx]}'" + currently_selected_profile_ident_printable="'${creds_ident[$idx]}'" else if [[ "${PRECHECK_AWS_PROFILE}" != "" ]]; then currently_selected_profile_ident_printable="'${PRECHECK_AWS_PROFILE}' [transient]" @@ -965,23 +1103,31 @@ NOTE: The role '${BASH_REMATCH[1]}' is defined in\\n\ echo -e "Executing this script as the AWS/IAM user $process_username $account_alias_if_any (profile $currently_selected_profile_ident_printable).\\n" - # 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_acc - 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 +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 @@ -991,9 +1137,9 @@ NOTE: The role '${BASH_REMATCH[1]}' is defined in\\n\ # transfer possible MFA mfasec from config array idxLookup idx confs_ident[@] "$profile_ident" if [[ $idx != "" ]]; then - mfa_mfasec[$cred_profilecounter]=${confs_mfasec[$idx]} + 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) @@ -1002,24 +1148,25 @@ NOTE: The role '${BASH_REMATCH[1]}' is defined in\\n\ [[ ! "$profile_ident" =~ -rolesession$ ]] ; then # store this profile ident - cred_profiles[$cred_profilecounter]="$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 - 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 --profile "$profile_ident" configure get output) - [[ "$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" + 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 sts get-caller-identity --profile "$profile_ident" --output text --query 'Arn' 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" + 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 - cred_profile_arn[$cred_profilecounter]=$user_arn + baseprofile_arn[$cred_profilecounter]=$user_arn else # must be a bad profile - cred_profile_arn[$cred_profilecounter]="" + baseprofile_arn[$cred_profilecounter]="" fi # get the actual username @@ -1030,60 +1177,61 @@ NOTE: The role '${BASH_REMATCH[1]}' is defined in\\n\ fi if [[ "$user_arn" =~ 'error occurred' ]]; then - cred_profile_user[$cred_profilecounter]="" - cred_profile_acc[$cred_profilecounter]="" + baseprofile_user[$cred_profilecounter]="" + baseprofile_account[$cred_profilecounter]="" else - cred_profile_user[$cred_profilecounter]="$profile_username" - cred_profile_acc[$cred_profilecounter]="$profile_user_acc" + 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" - cred_profile_account_alias[$cred_profilecounter]="${_ret}" + 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" - mfa_profiles[$cred_profilecounter]="$mfa_profile_ident" + 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, 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" + # 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 - cred_profile_status[$cred_profilecounter]="OK" + baseprofile_status[$cred_profilecounter]="OK" else - cred_profile_status[$cred_profilecounter]="LIMITED" + baseprofile_status[$cred_profilecounter]="LIMITED" fi # get MFA ARN if available - # (obviously not available if a MFA device + # (obviously not available if a vMFA device # isn't configured for the profile) - mfa_arn="$(aws iam list-mfa-devices \ - --profile "$profile_ident" \ - --user-name "${cred_profile_user[$cred_profilecounter]}" \ + 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 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" + [[ "$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 - mfa_arns[$cred_profilecounter]="$mfa_arn" + baseprofile_mfa_arn[$cred_profilecounter]="$mfa_arn" else - mfa_arns[$cred_profilecounter]="" + 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 reliable) + # it's still not as reliable) if [[ "$mfa_profile_ident" != "" ]]; then getInitTime _ret_timestamp "$mfa_profile_ident" @@ -1093,42 +1241,42 @@ NOTE: The role '${BASH_REMATCH[1]}' is defined in\\n\ if [[ ${_ret_remaining} -eq 0 ]]; then # session has expired - mfa_profile_status[$cred_profilecounter]="EXPIRED" + baseprofile_mfa_status[$cred_profilecounter]="EXPIRED" elif [[ ${_ret_remaining} -gt 0 ]]; then # session time remains getPrintableTimeRemaining _ret "${_ret_remaining}" - mfa_profile_status[$cred_profilecounter]="${_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 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" + 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 - mfa_profile_status[$cred_profilecounter]="OK" + baseprofile_mfa_status[$cred_profilecounter]="OK" elif [[ "$mfa_profile_check" =~ ExpiredToken ]]; then - mfa_profile_status[$cred_profilecounter]="EXPIRED" + baseprofile_mfa_status[$cred_profilecounter]="EXPIRED" else - mfa_profile_status[$cred_profilecounter]="LIMITED" + 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 (${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 "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: ${mfa_profiles[$cred_profilecounter]} (${mfa_profile_status[$cred_profilecounter]})" + echo "MFA PROFILE IDENT: ${baseprofile_mfa[$cred_profilecounter]} (${baseprofile_mfa_status[$cred_profilecounter]})" fi echo ## END DEBUG @@ -1157,30 +1305,30 @@ NOTE: The role '${BASH_REMATCH[1]}' is defined in\\n\ # select the profile (first, single profile + a possible persistent MFA session) mfa_req="false" - if [[ ${#cred_profiles[@]} == 1 ]]; then + if [[ ${#baseprofile_ident[@]} == 1 ]]; then echo - [[ "${cred_profile_user[0]}" != "" ]] && prcpu="${cred_profile_user[0]}" || prcpu="unknown (a bad profile?)" + [[ "${baseprofile_user[0]}" != "" ]] && prcpu="${baseprofile_user[0]}" || prcpu="unknown — a bad profile?" - if [[ "${cred_profile_account_alias[0]}" != "" ]]; then - prcpaa=" @${cred_profile_account_alias[0]}" - elif [[ "${cred_profile_acc[0]}" != "" ]]; then + 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=" @${cred_profile_acc[0]}" + prcpaa=" @${baseprofile_account[0]}" else # or nothing for a bad profile prcpaa="" fi - echo -e "${Green}${On_Black}You have one configured profile: ${BIGreen}${cred_profiles[0]} ${Green}(IAM: ${prcpu}${prcpaa})${Color_Off}" + 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 [[ "${mfa_arns[0]}" != "" ]]; then + if [[ "${baseprofile_mfa_arn[0]}" != "" ]]; then echo ".. its vMFAd is enabled" - if [[ "${mfa_profile_status[0]}" != "EXPIRED" && - "${mfa_profile_status[0]}" != "" ]]; then + if [[ "${baseprofile_mfa_status[0]}" != "EXPIRED" && + "${baseprofile_mfa_status[0]}" != "" ]]; then - echo -e ".. and it ${BIWhite}${On_Black}has an active MFA session with ${mfa_profile_status[0]}${Color_Off}" + echo -e ".. and it ${BIWhite}${On_Black}has an active MFA session with ${baseprofile_mfa_status[0]}${Color_Off}" mfa_session_status="true" else @@ -1196,7 +1344,7 @@ NOTE: The role '${BASH_REMATCH[1]}' is defined in\\n\ 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 (${mfa_profile_status[0]})?" + [[ "${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 @@ -1236,21 +1384,21 @@ NOTE: The role '${BASH_REMATCH[1]}' is defined in\\n\ echo SELECTR=0 ITER=1 - for i in "${cred_profiles[@]}" + for i in "${baseprofile_ident[@]}" do - if [[ "${mfa_arns[$SELECTR]}" != "" ]]; then + if [[ "${baseprofile_mfa_arn[$SELECTR]}" != "" ]]; then mfa_notify="; ${Green}${On_Black}vMFAd enabled${Color_Off}" else mfa_notify="; vMFAd not configured" fi - [[ "${cred_profile_user[$SELECTR]}" != "" ]] && prcpu="${cred_profile_user[$SELECTR]}" || prcpu="unknown (a bad profile?)" + [[ "${baseprofile_user[$SELECTR]}" != "" ]] && prcpu="${baseprofile_user[$SELECTR]}" || prcpu="unknown — a bad profile?" - if [[ "${cred_profile_account_alias[$SELECTR]}" != "" ]]; then - prcpaa=" @${cred_profile_account_alias[$SELECTR]}" - elif [[ "${cred_profile_acc[$SELECTR]}" != "" ]]; then + 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=" @${cred_profile_acc[$SELECTR]}" + prcpaa=" @${baseprofile_account[$SELECTR]}" else # or nothing for a bad profile prcpaa="" @@ -1258,9 +1406,9 @@ NOTE: The role '${BASH_REMATCH[1]}' is defined in\\n\ echo -en "${BIWhite}${On_Black}${ITER}: $i${Color_Off} (IAM: ${prcpu}${prcpaa}${mfa_notify})\\n" - if [[ "${mfa_profile_status[$SELECTR]}" != "EXPIRED" && - "${mfa_profile_status[$SELECTR]}" != "" ]]; then - echo -e "${BIWhite}${On_Black}${ITER}m: $i MFA profile${Color_Off} (${mfa_profile_status[$SELECTR]})" + 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 @@ -1293,12 +1441,12 @@ NOTE: The role '${BASH_REMATCH[1]}' is defined in\\n\ # translate it to the array index and validate ((actual_selprofile=selprofile_check-1)) - profilecount=${#cred_profiles[@]} + profilecount=${#baseprofile_ident[@]} 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 + echo -e "There is no profile '${selprofile}'.\\n" + exit 1 fi @@ -1308,14 +1456,14 @@ NOTE: The role '${BASH_REMATCH[1]}' is defined in\\n\ # if this is an MFA profile, it must be in OK or LIMITED status to select if [[ "$selprofile_mfa_check" != "" && - "${mfa_profile_status[$actual_selprofile]}" != "EXPIRED" && - "${mfa_profile_status[$actual_selprofile]}" != "" ]]; then + "${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="${cred_profiles[$actual_selprofile]}" + mfa_parent_profile_ident="${baseprofile_ident[$actual_selprofile]}" - final_selection="${mfa_profiles[$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 @@ -1325,48 +1473,44 @@ NOTE: The role '${BASH_REMATCH[1]}' is defined in\\n\ active_mfa="true" elif [[ "$selprofile_mfa_check" != "" && - "${mfa_profile_status[$actual_selprofile]}" == "" ]]; then + "${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}" - echo + 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: ${cred_profiles[$actual_selprofile]}" - final_selection="${cred_profiles[$actual_selprofile]}" + 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}" - echo + 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}" - echo + 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}" - echo + 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 ( [[ "${mfa_arns[$actual_selprofile]}" != "" && + 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 - echo -e "${BIWhite}${On_Black}Enter the current MFA one time pass code for the profile '${cred_profiles[$actual_selprofile]}'${Color_Off} to start/renew an MFA session," - echo "or leave empty (just press [ENTER]) to use the selected profile without the MFA." - echo + 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}" @@ -1387,15 +1531,14 @@ NOTE: The role '${BASH_REMATCH[1]}' is defined in\\n\ # reset entered MFA code (just to be safe) mfacode="" - echo - echo -e "A vMFAd has not been set up for this profile (run 'enable-disable-vmfa-device.sh' script to configure the vMFAd)." + 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="${cred_profiles[$actual_selprofile]}" + AWS_USER_PROFILE="${baseprofile_ident[$actual_selprofile]}" AWS_2AUTH_PROFILE="${AWS_USER_PROFILE}-mfasession" - ARN_OF_MFA=${mfa_arns[$actual_selprofile]} + 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:]]*$')" @@ -1404,13 +1547,11 @@ NOTE: The role '${BASH_REMATCH[1]}' is defined in\\n\ echo "[profile ${AWS_2AUTH_PROFILE}]" >> "$CONFFILE" fi - echo - echo -e "Acquiring MFA session token for the profile: ${BIWhite}${On_Black}${AWS_USER_PROFILE}${Color_Off}..." + 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 sts get-session-token \ - --profile "$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 \ @@ -1427,17 +1568,16 @@ NOTE: The role '${BASH_REMATCH[1]}' is defined in\\n\ 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 - echo -e "${BIRed}${On_Black}Could not initialize the requested MFA session.${Color_Off}" - echo + 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}" - echo + 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) @@ -1448,17 +1588,26 @@ NOTE: The role '${BASH_REMATCH[1]}' is defined in\\n\ # 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\\nso that you can return to it during its validity period, ${validity_period}.)" + + 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" - # set init time in the static MFA profile (a custom key in ~/.aws/credentials) + + # 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) @@ -1487,25 +1636,41 @@ NOTE: The role '${BASH_REMATCH[1]}' is defined in\\n\ export AWS_PROFILE=$final_selection # get region and output format for the selected profile - AWS_DEFAULT_REGION=$(aws configure get region --profile "${final_selection}") - [[ "$DEBUG" == "true" ]] && echo -e "\\n${Cyan}${On_Black}result for: 'aws configure get region --profile \"${final_selection}\"':\\n${ICyan}${AWS_DEFAULT_REGION}${Color_Off}\\n\\n" + 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 configure get output --profile "${final_selection}") - [[ "$DEBUG" == "true" ]] && echo -e "\\n${Cyan}${On_Black}result for: 'aws configure get output --profile \"${final_selection}\"':\\n${ICyan}${AWS_DEFAULT_OUTPUT}${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 [[ "${profile_region[$actual_selprofile]}" != "" && + if [[ "${baseprofile_region[$actual_selprofile]}" != "" && "${mfaprofile}" == "true" ]]; then - set_new_region=${profile_region[$actual_selprofile]} - echo -e "\\nNOTE: Region had not been configured for the selected MFA profile;\\n it has been set to same as the parent profile ('$set_new_region')." + 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 - set_new_region=${default_region} - echo -e "\\nNOTE: Region had not been configured for the selected profile;\\n it has been set to the default region ('${default_region}')." + 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}" @@ -1518,15 +1683,20 @@ NOTE: The role '${BASH_REMATCH[1]}' is defined in\\n\ if [[ "${AWS_DEFAULT_OUTPUT}" == "" ]]; then # retrieve parent profile output format if an MFA profile - if [[ "${profile_output[$actual_selprofile]}" != "" && + if [[ "${baseprofile_output[$actual_selprofile]}" != "" && "${mfaprofile}" == "true" ]]; then - set_new_output=${profile_output[$actual_selprofile]} - echo -e "NOTE: 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')." + 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 "Output format had not been configured for the selected profile;\\n it has been set to the default output format ('${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" == "" ]] || @@ -1555,10 +1725,8 @@ NOTE: The role '${BASH_REMATCH[1]}' is defined in\\n\ fi fi - echo - echo - echo -e "${BIWhite}${On_DGreen} * * * PROFILE DETAILS * * * ${Color_Off}" - echo + 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 @@ -1598,7 +1766,12 @@ NOTE: The role '${BASH_REMATCH[1]}' is defined in\\n\ 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 -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, @@ -1691,31 +1864,52 @@ NOTE: The role '${BASH_REMATCH[1]}' is defined in\\n\ echo if [[ "$OS" == "Linux" ]]; then if exists xclip; then - echo -e "${BIGreen}${On_Black}*** NOTE: xclip found; the envvar configuration command is now on your X PRIMARY clipboard -- just paste on the command line, and press [ENTER])${Color_Off}" + 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 - echo "*** NOTE: If you're using an X GUI on Linux, install 'xclip' to have the activation command copied to the clipboard automatically!" + + 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 - echo -e "${Green}${On_Black}*** Make sure to export/unset all the new values as instructed above to\\n make sure no conflicting profile/secrets remain in the envrionment!" - echo - echo -e "*** 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 sts get-caller-identity --profile default${Color_Off}" - echo - 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" - echo - echo -e "${BIWhite}${On_Black}PASTE THE PROFILE ACTIVATION COMMAND FROM THE CLIPBOARD\\nON THE COMMAND LINE NOW, AND PRESS ENTER! THEN YOU'RE DONE!${Color_Off}" - echo + + 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 "It is imperative that the following environment variables are exported/unset to activate the selected profile!" - echo - echo "Execute the following on the command line to activate this profile for the 'aws', 's3cmd', etc. commands." - echo - echo "NOTE: Even if you only use a named profile ('AWS_PROFILE'), it's important to execute all of the export/unset" - echo " commands to make sure previously set environment variables won't override the selected configuration." - echo + 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 @@ -1757,18 +1951,20 @@ NOTE: The role '${BASH_REMATCH[1]}' is defined in\\n\ echo "unset AWS_SESSION_TOKEN" fi fi - echo - echo "*** Make sure to export/unset all the new values as instructed above to" - echo " make sure no conflicting profile/secrets remain in the envrionment!" - echo - echo "*** You can temporarily override the profile set/selected in the environment" - echo " using the \"--profile AWS_PROFILE_NAME\" switch with awscli. For example:" - echo " aws sts get-caller-identity --profile default" - echo - echo "*** To easily remove any all AWS profile settings and secrets information" - echo " from the environment, simply source the included script, like so:" - echo " source ./source-this-to-clear-AWS-envvars.sh" - echo + + 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 From 4e194d9f85aaf5597d9e26b92cc2f56fad7e0c0a Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 21 Jan 2026 18:21:36 +0000 Subject: [PATCH 69/71] Add script to delete SageMaker Notebook instances across all regions This script searches for and deletes Amazon SageMaker Notebook instances across all AWS regions, including opt-in regions. Features: - Scans all regions (including opt-in regions) - Lists all SageMaker notebook instances in each region - Stops running notebooks before deletion - Provides detailed progress and summary statistics - Includes error handling and prerequisite checks --- delete-sagemaker-notebooks.sh | 208 ++++++++++++++++++++++++++++++++++ 1 file changed, 208 insertions(+) create mode 100755 delete-sagemaker-notebooks.sh diff --git a/delete-sagemaker-notebooks.sh b/delete-sagemaker-notebooks.sh new file mode 100755 index 0000000..abd4d5c --- /dev/null +++ b/delete-sagemaker-notebooks.sh @@ -0,0 +1,208 @@ +#!/usr/bin/env bash + +# Script to search and delete Amazon SageMaker Notebook instances across all opt-in regions +# Uses AWS CLI to manage SageMaker notebooks + +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 all regions (including opt-in regions) +get_all_regions() { + aws ec2 describe-regions --all-regions --query "Regions[].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 get notebook instance status +get_notebook_status() { + local region=$1 + local notebook_name=$2 + aws sagemaker describe-notebook-instance --region "$region" --notebook-instance-name "$notebook_name" --query "NotebookInstanceStatus" --output text 2>/dev/null || echo "Unknown" +} + +# Function to stop a notebook instance +stop_notebook() { + local region=$1 + local notebook_name=$2 + echo " → Stopping notebook instance: $notebook_name" + aws sagemaker stop-notebook-instance --region "$region" --notebook-instance-name "$notebook_name" 2>/dev/null + + # Wait for the notebook to stop (max 5 minutes) + local max_wait=60 # 60 * 5 seconds = 5 minutes + local count=0 + while [ $count -lt $max_wait ]; do + local status=$(get_notebook_status "$region" "$notebook_name") + if [ "$status" = "Stopped" ]; then + echo " ✓ Notebook stopped: $notebook_name" + return 0 + elif [ "$status" = "Failed" ]; then + echo " ✗ Notebook failed to stop: $notebook_name" + return 1 + fi + sleep 5 + count=$((count + 1)) + if [ $((count % 6)) -eq 0 ]; then + echo " (Still waiting for $notebook_name to stop... status: $status)" + fi + done + echo " ⚠ Timeout waiting for notebook to stop: $notebook_name" + return 1 +} + +# 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 all AWS regions (including opt-in regions)..." +REGIONS=$(get_all_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 regions 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" + + # Get current status + status=$(get_notebook_status "$region" "$notebook") + echo " Current status: $status" + + # Stop the notebook if it's not already stopped + if [ "$status" != "Stopped" ] && [ "$status" != "Stopping" ]; then + if ! stop_notebook "$region" "$notebook"; then + echo " ⚠ Skipping deletion due to stop failure: $notebook" + FAILED_DELETIONS=$((FAILED_DELETIONS + 1)) + continue + fi + elif [ "$status" = "Stopping" ]; then + echo " → Notebook is already stopping, waiting for it to stop..." + # Wait for it to finish stopping + local max_wait=60 + local count=0 + while [ $count -lt $max_wait ]; do + status=$(get_notebook_status "$region" "$notebook") + if [ "$status" = "Stopped" ]; then + echo " ✓ Notebook stopped: $notebook" + break + fi + sleep 5 + count=$((count + 1)) + done + else + echo " ✓ Notebook already stopped: $notebook" + fi + + # Delete the notebook + if delete_notebook "$region" "$notebook"; then + DELETED_NOTEBOOKS=$((DELETED_NOTEBOOKS + 1)) + else + 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 From f4640cd0e48cdbe756f0a51e8b8a56f818a0283b Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 21 Jan 2026 19:03:57 +0000 Subject: [PATCH 70/71] Update script to only scan enabled regions Modified the script to only scan AWS regions that are enabled for the account (opt-in-not-required or opted-in status), excluding regions that have not been opted-in. This prevents unnecessary API calls to disabled regions and focuses on regions actually in use. --- delete-sagemaker-notebooks.sh | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/delete-sagemaker-notebooks.sh b/delete-sagemaker-notebooks.sh index abd4d5c..b094524 100755 --- a/delete-sagemaker-notebooks.sh +++ b/delete-sagemaker-notebooks.sh @@ -1,6 +1,6 @@ #!/usr/bin/env bash -# Script to search and delete Amazon SageMaker Notebook instances across all opt-in regions +# Script to search and delete Amazon SageMaker Notebook instances across all enabled regions # Uses AWS CLI to manage SageMaker notebooks set -e @@ -41,9 +41,9 @@ echo "Running as: $USER_ARN" echo "Account ID: $ACCOUNT_ID" echo -# Function to get all regions (including opt-in regions) -get_all_regions() { - aws ec2 describe-regions --all-regions --query "Regions[].RegionName" --output text +# 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 @@ -103,8 +103,8 @@ delete_notebook() { } # Main execution -echo "Retrieving all AWS regions (including opt-in regions)..." -REGIONS=$(get_all_regions) +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." @@ -112,7 +112,7 @@ if [ -z "$REGIONS" ]; then fi REGION_COUNT=$(echo "$REGIONS" | wc -w) -echo "Found $REGION_COUNT regions to scan" +echo "Found $REGION_COUNT enabled region(s) to scan" echo # Track statistics From 771b18617fd1a2ab0201bc8843ff8a44080417cb Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 21 Jan 2026 19:21:03 +0000 Subject: [PATCH 71/71] Simplify script to assume notebooks are already stopped Removed the notebook stopping logic to simplify the script. The script now assumes all notebook instances are already stopped and proceeds directly to deletion. This makes the script faster and simpler for scenarios where instances are known to be stopped. Changes: - Removed stop_notebook() and get_notebook_status() functions - Simplified main processing loop to delete directly - Added note in script header about stopped instance assumption - Added helpful error message if deletion fails due to running instance --- delete-sagemaker-notebooks.sh | 69 ++--------------------------------- 1 file changed, 3 insertions(+), 66 deletions(-) diff --git a/delete-sagemaker-notebooks.sh b/delete-sagemaker-notebooks.sh index b094524..edbeeca 100755 --- a/delete-sagemaker-notebooks.sh +++ b/delete-sagemaker-notebooks.sh @@ -2,6 +2,7 @@ # 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 @@ -52,42 +53,6 @@ list_notebooks_in_region() { aws sagemaker list-notebook-instances --region "$region" --query "NotebookInstances[].NotebookInstanceName" --output text 2>/dev/null || echo "" } -# Function to get notebook instance status -get_notebook_status() { - local region=$1 - local notebook_name=$2 - aws sagemaker describe-notebook-instance --region "$region" --notebook-instance-name "$notebook_name" --query "NotebookInstanceStatus" --output text 2>/dev/null || echo "Unknown" -} - -# Function to stop a notebook instance -stop_notebook() { - local region=$1 - local notebook_name=$2 - echo " → Stopping notebook instance: $notebook_name" - aws sagemaker stop-notebook-instance --region "$region" --notebook-instance-name "$notebook_name" 2>/dev/null - - # Wait for the notebook to stop (max 5 minutes) - local max_wait=60 # 60 * 5 seconds = 5 minutes - local count=0 - while [ $count -lt $max_wait ]; do - local status=$(get_notebook_status "$region" "$notebook_name") - if [ "$status" = "Stopped" ]; then - echo " ✓ Notebook stopped: $notebook_name" - return 0 - elif [ "$status" = "Failed" ]; then - echo " ✗ Notebook failed to stop: $notebook_name" - return 1 - fi - sleep 5 - count=$((count + 1)) - if [ $((count % 6)) -eq 0 ]; then - echo " (Still waiting for $notebook_name to stop... status: $status)" - fi - done - echo " ⚠ Timeout waiting for notebook to stop: $notebook_name" - return 1 -} - # Function to delete a notebook instance delete_notebook() { local region=$1 @@ -144,39 +109,11 @@ for region in $REGIONS; do for notebook in $notebook_array; do echo " Processing: $notebook" - # Get current status - status=$(get_notebook_status "$region" "$notebook") - echo " Current status: $status" - - # Stop the notebook if it's not already stopped - if [ "$status" != "Stopped" ] && [ "$status" != "Stopping" ]; then - if ! stop_notebook "$region" "$notebook"; then - echo " ⚠ Skipping deletion due to stop failure: $notebook" - FAILED_DELETIONS=$((FAILED_DELETIONS + 1)) - continue - fi - elif [ "$status" = "Stopping" ]; then - echo " → Notebook is already stopping, waiting for it to stop..." - # Wait for it to finish stopping - local max_wait=60 - local count=0 - while [ $count -lt $max_wait ]; do - status=$(get_notebook_status "$region" "$notebook") - if [ "$status" = "Stopped" ]; then - echo " ✓ Notebook stopped: $notebook" - break - fi - sleep 5 - count=$((count + 1)) - done - else - echo " ✓ Notebook already stopped: $notebook" - fi - - # Delete the 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