Skip to content

Instantly share code, notes, and snippets.

@RichardBronosky
Last active June 24, 2020 17:02
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save RichardBronosky/80306011bb5da80c765ffd6aa2ecf89b to your computer and use it in GitHub Desktop.
Save RichardBronosky/80306011bb5da80c765ffd6aa2ecf89b to your computer and use it in GitHub Desktop.
AWS Assume-Role requiring only aws-cli and jq

Assume-Role

AWS Assume-Role requiring only aws-cli and jq

Installation

Basic

git clone https://gist.github.com/80306011bb5da80c765ffd6aa2ecf89b.git arole
ln -s $PWD/arole/arole /usr/local/bin

Suggested: sourcing the script in ~/.bash_profile

After completing the 2 commands above...

cat >> ~/.bash_profile <<EOF 

[[ -f /usr/local/bin/arole ]] && source /usr/local/bin/arole
EOF

Usage

Sourcing the script in ~/.bash_profile and calling the function directly

Functions ran in your interactive shell can modify the environment of your shell. Because of this, I suggest using it in this way.

arole dev

Traditional Script Execution

Because child processes cannot modify the environment of thier parent, you must eval the output of the execcutable.

eval $(arole dev)

Testing

The script has its own mock function within it. It can be used for testing by setting the MOCK_AWS environment variable.

$ MOCK_AWS=1 ./assrole dev
export AWS_ASSUMED_ROLE_ACCOUNT_ID="485548554855"
export AWS_ASSUMED_ROLE_ACCOUNT="dev"
export AWS_ASSUMED_ROLE_ID="AROAIWL33TL33TL33TL33:brunobronosky"
export AWS_ASSUMED_ROLE_ARN="arn:aws:sts::485548554855:assumed-role/allow-full-access-from-other-accounts/brunobronosky"
export AWS_SECRET_ACCESS_KEY="L33TL33TL33TL33TL33TL33TL33TL33TL33TL33T"
export AWS_SESSION_TOKEN="L33TL33TL33TEI///////////L33TL33TL33TL33TL33TL33TL33TL33TL33TL33TL33TL33Tm9/TL33TL33TL33TL33T/TL33TL33TL33TL33TL33TL33TL33TL33TL33TL33Tz9/TL33TL33TL33TL33TL33TL33TL33TL33TL33TL33TL33TL33TS/TL33TL33TL33TL33TL33TL33TL33TL33TL33T/TL33TL33TL33TL33TL33TL33TL33TL33TL33TL33TL33TL33TL33TL33TL33TL33TL33TL33TL33TL33TL33TL33Tos/TL33TL33TL33TL33TL33TL33TL33T/A=="
export AWS_EXPIRATION="2019-11-01T00:00:00Z"
export AWS_ACCESS_KEY_ID="ASIAL33TL33TL33TL33T"
#!/bin/bash -eu
# TODO:
# Define: AWS_REGION based on `aws configure get profile.security.region || aws configure get profile.default.region`
if [[ -n ${DEBUG:-} ]]; then
set -o xtrace
fi
# TODO: Find a way to prevent leaking this variable. Currently it can't be defined 'local'
_arole_jq_functions='
def export:
"export " + .
;
def prefix:
"AWS" + . | sub("(?<a>ARN|ACCOUNT|PROFILE)"; "ASSUMED_ROLE_"+.a)
;
def snake_case:
.key | gsub("(?<a>[A-Z])"; "_"+.a) | ascii_upcase | prefix
;
def equals_value:
"=\"" + .value + "\""
;
def account_id:
{"AccountId": (.Arn | sub("([^:]*:){4}(?<a>[0-9]*):.*";.a))}
;
def aws_config_input:
{ "config":input, "credentials":input }
;
def aws_config_field_profiles:
.config | [ ( to_entries[] | select(.key | startswith("profile"))) | { (.key|sub("profile ";"")): .value } ] | add
;
def aws_config_field_plugins:
.config.plugins
;
def aws_config_field_config:
{"profiles": aws_config_field_profiles, "plugins": aws_config_field_plugins}
;
def aws_config_field_credentials:
.credentials
;
def aws_config_output:
{"config": aws_config_field_config, "credentials": aws_config_field_credentials}
;
def aws_config:
aws_config_input | aws_config_output
;
def role_profiles:
.config.profiles | to_entries | .[] | select(.value.role_arn)
;
def account_from_role_arn:
sub("([^:]*:){4}";"")|sub(":.*";"")
;
def role_name_from_role_arn:
sub("([^:]*:){5}[^/]*/";"")|sub("/.*";"")
;
def profile_flat:
[
.config.profiles | to_entries | .[] | select(.value.role_arn) |
{
"profile":.key,
"account":(.value.role_arn|account_from_role_arn),
"role":(.value.role_arn|role_name_from_role_arn)
}
]
;
def profile_nested:
[
.config.profiles | to_entries | .[] | select(.value.role_arn) |
{
(.key):{
"account":(.value.role_arn|account_from_role_arn),
"role":(.value.role_arn|role_name_from_role_arn)
}
}
]
;
'
function _arole_config_to_json(){
python3 -c 'from configparser import ConfigParser as CP; import json, sys; cp = CP(); cp.read_file(sys.stdin); print(json.dumps(cp._sections));'
}
function _arole_get_aws_config(){
{
_arole_config_to_json <~/.aws/config;
_arole_config_to_json <~/.aws/credentials;
} | _arole_export_aws_config_json
}
function _arole_export_aws_config_json(){
jq -n "
${_arole_jq_functions}
aws_config
"
}
function _arole_format_profiles(){
jq "
${_arole_jq_functions}
profile_nested
"
}
function _arole_export_json_assumed_role(){
jq -s -r "
${_arole_jq_functions}
[(add|add) | account_id,.] |
add | to_entries[] | snake_case + equals_value | export
"
}
function _arole_export_json_session_token(){
jq -s -r "
${_arole_jq_functions}
add |
add | to_entries[] | snake_case + equals_value | export
"
}
function _arole_mfa_session(){
HELPERS=aws,mfa _arole_define_helpers
echo "Creating mfa session" | _arole_err
aws sts get-session-token \
--serial-number ${MFA_SERIAL:-$(aws configure get --profile=security mfa_serial)} \
--token-code $(NOCOPY=1 MFA_WAIT=1 mfa)
}
function _arole_assume_role(){
HELPERS=aws,mfa _arole_define_helpers
local profile="${1:-}"
echo "Assuming role: $profile" | _arole_err
if [[ -n ${MOCK_AWS:-} ]]; then
function aws(){
<<<"H4sIAJcFxV0AA5WS0UvDMBDG3/0z8rzQ1jlwgT0E2UNRENaK4FvW3rAszcldypyj/7vpRnFuTmvI7+Uj33fHXXZCMzc1lAu08MRAQu2OpbQUSujFo06fH8bj/Ai1pMbhktAhr7diJDS58NaQU2bDij0rdXM7mfQoc0iVFGIjYy1u5KqxVpqiAGa5Iqwl+legTsHGeY6+l2hH4o6gBOcrY7lrNIOCwOt9wD1sQ/mTJi8S+s2CqUKX4xrciXOeRl9naGRPPY3OtHPlEh8/uP8iG2b5f/BQkH/PjvRsFiY+f3+ryPgw9DDv6ziZyiSRcZLHsdrfl+4X9cs8/Lws1Wera9urTy2VCbm3AgAA" base64 -d | gzip -d | jq .
}
fi
echo '{"merge1":{"Profile": "'$profile'"}}'
local mfa_serial="${MFA_SERIAL:-$( aws configure get --profile="$profile" mfa_serial )}"
if [[ -n $mfa_serial ]]; then
echo "Using MFA serial $mfa_serial" | _arole_err
mfa_serial="
--serial-number $mfa_serial
--token-code $(NOCOPY=1 MFA_WAIT=1 mfa)"
else
echo "WARN: No MFA specified for profile $profile" | _arole_err
fi
local role_arn="${ROLE_ARN:-$( aws configure get --profile="$profile" role_arn )}"
if [[ -n $role_arn ]]; then
role_arn="
--role-arn $role_arn"
fi
local duration_seconds="${DURATION_SECONDS:-$( aws configure get profile.$profile.max_session_duration )}"
if [[ -n $duration_seconds ]]; then
duration_seconds="
--duration-seconds $duration_seconds"
fi
aws sts assume-role $mfa_serial $role_arn $duration_seconds \
--role-session-name "$( ( id -un; date +-%Y-%m-%d+%H.%M.%S ) | tr -d '\n' )"
}
function _arole_update_config_with_max_session_duration(){
HELPERS=aws _arole_define_helpers
local profile="$1"
local arole_cache="$2"
eval $(cat $arole_cache)
local duration_seconds="${DURATION_SECONDS:-$( aws configure get profile.$profile.max_session_duration )}"
if [[ -z $duration_seconds ]]; then
local role_name="$(<<<"$AWS_ASSUMED_ROLE_ARN" jq -sRr "${_arole_jq_functions} role_name_from_role_arn")"
local max_session_duration="$( aws iam get-role --role-name "$role_name" | \
jq -r '.Role.MaxSessionDuration'
)"
aws configure set profile.$profile.max_session_duration "$max_session_duration"
fi
}
function _arole_reuse_cache(){
cache_file="$1"
if [[ -f $cache_file ]]; then
source $cache_file
if _arole_expiration_is_current; then
return 0
else
_arole_cleanup_aws_vars
return 8
fi
fi
echo "Did not find cache file: $cache_file" | _arole_err
return 9
}
function _arole_expiration_is_current(){
_arole_check_requirements
local date=$(which gdate date| head -n1)
if [[ -z ${AWS_EXPIRATION:-} ]]; then
echo "No expiration set" | _arole_err
return 8
fi
if [[ $($date +%s) -gt $($date --date="$AWS_EXPIRATION" +%s) ]]; then
echo "Expiration is in the past: $AWS_EXPIRATION" | _arole_err
return 8
else
echo "Expiration is in the future: $AWS_EXPIRATION" | _arole_err
return 0
fi
}
function _arole_entrypoint(){
local profile="${1:-}"
if [[ -z $profile ]]; then
_arole_usage
return 1
fi
local arole_cache="$_arole_tmp_dir/aws-role-$profile.sh"
if [[ $profile == "mfa" ]]; then
local cmd=_arole_mfa_session
local filter=_arole_export_json_session_token
else
local cmd=_arole_assume_role
local filter=_arole_export_json_assumed_role
HELPERS=arole _arole_define_helpers
arole -
fi
if [[ "${2:-}" == "-f" ]]; then shift; shift; fi
if _arole_reuse_cache $arole_cache; then
echo "Reused cached $arole_cache" | _arole_err
else
$cmd "$@" | tee $arole_cache.json | $filter > $arole_cache
_arole_update_config_with_max_session_duration $1 $arole_cache #&
fi
if [[ ${AROLE_EVAL:-1} = 1 && -z ${MOCK_AWS:-} ]]; then
eval $(cat $arole_cache)
else
cat $arole_cache
fi
}
function _arole_usage(){
_arole_err <<'EOF'
Usage: arole <profile> [-] [env]
Passing the "-" end causes this script to use your cached token rather than requesting a new one.
It's faster, but more importantly allows you to use for-loops without getting errors due to using
"one time passwords" more than once, as a consequence of the speed.
Passing literal "env" (with or without "-") causes output to be given in envFile format that is
appropriate for VSCode launch.json and `npm i dotenv`. This is done without sourcing the temp
file.
EOF
# Show all roles/profiles in ~/.aws/config
#awk -F '[:/ ]' '/^ *;/{next} /^.profile/{gsub("]",""); printf("\nprofile: %s\n", $2)} /role_arn/{printf(" account: %s\n role: %s\n", $5, $7)}' ~/.aws/config
_arole_get_aws_config | _arole_format_profiles | underscore --wrapwidth 240 pretty| column -t
echo | _arole_err
local brk=0
if [[ -n ${AWS_ASSUMED_ROLE_ACCOUNT:-} ]]; then
echo "Current profile: ${AWS_ASSUMED_ROLE_ACCOUNT}" | _arole_err
brk=1
fi
if [[ -n "${AWS_ASSUMED_ROLE_ACCOUNT_ID:-}" ]]; then
echo "Current account: ${AWS_ASSUMED_ROLE_ACCOUNT_ID}" | _arole_err
brk=1
fi
if [[ -n "${AWS_EXPIRATION:-}" ]]; then
echo "Credential Expiration: ${AWS_EXPIRATION}$(
date=$(which gdate date | head -n1)
$date && [[ $($date +%s) -gt $($date --date="$AWS_EXPIRATION" +%s) ]] && printf ' (expired)'
)" | _arole_err
brk=1
fi
[[ $brk -gt 0 ]] && echo
}
function _arole_copy_cat(){
if [[ -n ${NOCOPY:-} ]]; then
cat
else
pbcopy
pbpaste
fi
}
function _arole_err(){
if [[ ${NOERR:-} -eq 1 ]]; then
cat
elif [[ ${NOERR:-} -eq 2 ]]; then
cat >/dev/null
else
cat >/dev/stderr
fi
}
function _arole_get_totp(){
local service="$1"
local refresh_wait=$(date +%-S)
((refresh_wait=30-$refresh_wait))
[[ $refresh_wait -eq 0 ]] && ((refresh_wait=30))
[[ $refresh_wait -lt 0 ]] && ((refresh_wait=30+$refresh_wait))
echo "Getting an MFA code for $service" | _arole_err
echo "$(totp_generator -s $service) $refresh_wait"
}
function _arole_check_requirements(){
if ! ( $(which gdate date | head -n1) --version 2>/dev/null | grep -q GNU; ); then
echo "This requires GNU `date` command."
if [[ $(uname) == Darwin ]]; then
echo -e "On macOS, try:""\n"" brew install coreutils"
fi
exit 3
fi
}
function _arole_render_template(){
local template="$(cat)"
for varname in $(<<<"$template" grep -oE '\{\{([A-Za-z0-9_]+)\}\}' | sed -En 's/.*\{\{([A-Za-z0-9_]+)\}\}.*/\1/p' | sort | uniq); do
template="$(<<<"$template" sed -E "s/\{\{$varname\}\}/$(sed 's_\\_\\\\\\\\_g;s_/_\\/_g' <<<"${!varname}")/g")"
done
printf '%s' "$template"
}
function _arole_define_helpers(){
_arole_rename_function() {
test -n "$(declare -f "$1")" || return
local function_def="${_/$1/$2}"
eval "$(tee $_arole_tmp_dir/$2.template<<<"$function_def" | _arole_render_template | tee $_arole_tmp_dir/$2.src)"
unset -f "$1"
}
local function_name=arole
if [[ ,${HELPERS:-}, =~ ,($function_name|all), ]]; then
eval "$_arole_wrapper_function"
_arole_rename_function _arole_helper $function_name
fi
local function_name=aws
if [[ ,${HELPERS:-}, =~ ,($function_name|all), ]]; then
function _arole_helper(){
local hash="$(</dev/urandom LC_CTYPE=C tr -dc 'a-zA-Z0-9' | fold -w6 | head -n1)"
local curr=${AWS_CURR:-{{_arole_tmp_dir}}/aws-curr.$hash.json}
local last=${AWS_LAST:-{{_arole_tmp_dir}}/aws-last.json}
local log=${AWS_LOG:-{{_arole_tmp_dir}}/aws-cli.log}
printf '%q ' aws "$@" | sed -e 's/ $//'>>$log
local ishelp=0
local pager_or_cat="cat"
if [[ ${*: -1} == "--last" ]]; then
cat "$last"
else
[[ ${*: -1} == "help" ]] && ishelp=1 && pager_or_cat="less"
touch $curr
$(which aws) "$@" | tee $curr | $pager_or_cat
local retval=${PIPESTATUS[0]}
[[ $retval -eq 0 ]] && [[ $ishelp -eq 0 ]] && cp $curr $last || true
rm -f $curr
return $retval
fi
}
_arole_rename_function _arole_helper $function_name
fi
local function_name=mfa
if [[ ,${HELPERS:-}, =~ ,($function_name|all), ]]; then
function _arole_helper(){
local service=${1:-aws}
local arole_tmp="{{_arole_tmp_dir}}/mfa.$service"
[[ -n "$(cat $arole_tmp.curr 2>/dev/null)" ]] && mv $arole_tmp.curr $arole_tmp.last
local totp=($(arole -f _arole_get_totp $service | tee >(awk '{print $1}' >$arole_tmp.curr)))
if [[ ${MFA_WAIT:-0} != 0 ]] && diff $arole_tmp.{last,curr} >/dev/null 2>&1; then
local sleep_time=$((${totp[1]}+1))
echo "Waiting $sleep_time for MFA to refresh..." | arole -f _arole_err
sleep $sleep_time
totp=($(arole -f _arole_get_totp $service | tee >(awk '{print $1}' >$arole_tmp.curr)))
fi
echo "Expires in: ${totp[1]}" | arole -f _arole_err
arole -f _arole_copy_cat < $arole_tmp.curr
}
_arole_rename_function _arole_helper $function_name
fi
local function_name=awsconf
if [[ ,${HELPERS:-}, =~ ,($function_name|all), ]]; then
function _arole_helper(){ arole -f _arole_get_aws_config; }
_arole_rename_function _arole_helper $function_name
fi
# TODO: figure out if this --last flag is actually reaching the aws function (is it in scope?)
local function_name=awsres
if [[ ,${HELPERS:-}, =~ ,($function_name|all), ]]; then
function _arole_helper(){ [[ -p /dev/stdin ]] && cat /dev/stdin; aws --last; }
_arole_rename_function _arole_helper $function_name
fi
local function_name=awspres
if [[ ,${HELPERS:-}, =~ ,($function_name|all), ]]; then
function _arole_helper(){
[[ -p /dev/stdin ]] && cmd=cat || cmd=awsres
local red="$(tput setaf 001)"; local bred="$(tput setaf 009)";
local gre="$(tput setaf 002)"; local bgre="$(tput setaf 056)";
local yel="$(tput setaf 003)"; local byel="$(tput setaf 011)";
local blu="$(tput setaf 004)"; local bblu="$(tput setaf 012)";
local mag="$(tput setaf 005)"; local bmag="$(tput setaf 013)";
local cya="$(tput setaf 006)"; local bcya="$(tput setaf 014)";
local whi="$(tput setaf 007)"; local bwhi="$(tput setaf 015)";
local none="$(tput init)";
$cmd | \
sed -E "
s/(196342108771)/\1$red(rtg2)$none/g;
s/(224530485594)/\1$red(prod)$none/g;
s/(254860608894)/\1$red(security)$none/g;
s/(255672061625)/\1$red(rtg)$none/g;
s/(327762698288)/\1$red(stage)$none/g;
s/(361823887585)/\1$red(prdev)$none/g;
s/(485559245296)/\1$red(plan)$none/g;
s/(568386843263)/\1$red(shared)$none/g;
s/(734994113314)/\1$red(prprod)$none/g;
s/(742127223731)/\1$red(dev)$none/g;
s/(984407606261)/\1$red(root)$none/g;
"
}
_arole_rename_function _arole_helper $function_name
fi
}
function _arole_list_aws_vars(){
compgen -A export | grep '^AWS'
}
function _arole_list_arole_vars(){
compgen -A variable | grep '^_arole'
}
function _arole_list_arole_functions(){
compgen -A function | grep '^_arole'
}
function _arole_cleanup_aws_vars(){
unset $(arole -f _arole_list_aws_vars)
}
function _arole_cleanup_arole_vars(){
unset $(arole -f _arole_list_arole_vars)
}
function _arole_cleanup_functions(){
unset -f $(arole -f _arole_list_arole_functions)
}
function _arole_cleanup(){
_arole_cleanup_arole_vars
_arole_cleanup_functions
}
if [[ -d "${HOME:-}/.aws" ]]; then
_arole_tmp_dir="$HOME/.aws/temp"
[[ -d "$_arole_tmp_dir" ]] || mkdir -p "$_arole_tmp_dir"
else
_arole_tmp_dir="/tmp"
fi
_arole_script_name="$(basename "$BASH_SOURCE")"
_arole_script_path="$(cd "$(dirname "$BASH_SOURCE")"; pwd;)"
_arole_script="$_arole_script_path/$_arole_script_name"
_arole_wrapper_function="
function _arole_helper(){
if [[ -z \${1:-} ]]; then
${_arole_script}
else
if [[ \${TAIL_TEST:-0} -eq 1 ]]; then
date; tail $_arole_tmp_dir/mfa.aws.last $_arole_tmp_dir/mfa.aws.curr $_arole_tmp_dir/aws-cli.log
fi
if [[ \$1 == '-' ]]; then
unset \$(${_arole_script} -f _arole_list_aws_vars)
elif [[ \$1 =~ -. ]]; then
export DEBUG=\"\${DEBUG:-}\"; '${_arole_script}' \"\$@\"
else
eval \$(export DEBUG=\"\${DEBUG:-}\"; '${_arole_script}' \"\$@\")
fi
fi
}
"
if [[ $0 == $BASH_SOURCE ]]; then
export PS4="# (xtrace) \$(basename \"\${BASH_SOURCE:-}\") Line: \${LINENO:-}\$([[ -n \${FUNCNAME[0]:-} ]] && echo \" \${FUNCNAME[0]:-}()\")\n"
export AROLE_EVAL=0
entrypoint=_arole_entrypoint
if [[ "${1:-}" == "-f" ]]; then
shift
entrypoint=$1
shift
HELPERS=all _arole_define_helpers
fi
$entrypoint "$@"
else
_arole_define_helpers
_arole_check_requirements
_arole_cleanup
fi
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment