mirror of
https://github.com/ghndrx/devops-scripts.git
synced 2026-02-09 22:35:06 +00:00
feat(aws): add assume-role.sh with MFA support and session caching
- MFA token prompting with auto-detection - Session caching to ~/.aws/cli/cache (5-min buffer) - Cross-account support with external-id - Eval-friendly output for shell sourcing - AWS CLI profile-aware - Configurable session duration (up to 12h) - Comprehensive README with usage examples
This commit is contained in:
78
aws/README.md
Normal file
78
aws/README.md
Normal file
@@ -0,0 +1,78 @@
|
||||
# AWS Scripts
|
||||
|
||||
AWS CLI helper scripts for common operations.
|
||||
|
||||
## assume-role.sh
|
||||
|
||||
Production-ready role assumption with MFA support and session caching.
|
||||
|
||||
### Features
|
||||
|
||||
- **MFA Support**: Prompts for TOTP code, auto-detects MFA device
|
||||
- **Session Caching**: Avoids re-authentication within session duration
|
||||
- **Cross-Account**: Supports external-id for cross-account roles
|
||||
- **Profile-Aware**: Works with AWS CLI named profiles
|
||||
- **Eval-Friendly**: Output designed for `eval` or `source`
|
||||
|
||||
### Usage
|
||||
|
||||
```bash
|
||||
# Basic - source to set env vars in current shell
|
||||
source assume-role.sh arn:aws:iam::123456789012:role/AdminRole
|
||||
|
||||
# Eval alternative
|
||||
eval "$(./assume-role.sh arn:aws:iam::123456789012:role/AdminRole)"
|
||||
|
||||
# With MFA (will prompt for code)
|
||||
source assume-role.sh arn:aws:iam::123456789012:role/AdminRole \
|
||||
--mfa-serial arn:aws:iam::123456789012:mfa/myuser
|
||||
|
||||
# Cross-account with external-id
|
||||
source assume-role.sh arn:aws:iam::987654321098:role/CrossAccountRole \
|
||||
--external-id MyExternalId123
|
||||
|
||||
# Extended session (up to 12 hours)
|
||||
source assume-role.sh arn:aws:iam::123456789012:role/AdminRole \
|
||||
--duration 43200
|
||||
|
||||
# With specific profile and region
|
||||
source assume-role.sh arn:aws:iam::123456789012:role/AdminRole \
|
||||
--profile production \
|
||||
--region us-west-2
|
||||
```
|
||||
|
||||
### Options
|
||||
|
||||
| Option | Description |
|
||||
|--------|-------------|
|
||||
| `-m, --mfa-serial` | MFA device ARN (auto-detected if not specified) |
|
||||
| `-e, --external-id` | External ID for cross-account trust |
|
||||
| `-d, --duration` | Session duration in seconds (default: 3600) |
|
||||
| `-s, --session-name` | Session name identifier |
|
||||
| `-p, --profile` | AWS CLI profile for source credentials |
|
||||
| `-r, --region` | AWS region |
|
||||
| `-c, --no-cache` | Disable session caching |
|
||||
| `-v, --verbose` | Verbose output |
|
||||
|
||||
### Session Caching
|
||||
|
||||
Credentials are cached in `~/.aws/cli/cache/` and reused if more than 5 minutes remain before expiration. Use `--no-cache` to force fresh credentials.
|
||||
|
||||
### Requirements
|
||||
|
||||
- AWS CLI v2
|
||||
- jq
|
||||
- Valid AWS credentials (profile or environment)
|
||||
|
||||
### Environment Variables Set
|
||||
|
||||
After sourcing, these variables are exported:
|
||||
|
||||
```bash
|
||||
AWS_ACCESS_KEY_ID
|
||||
AWS_SECRET_ACCESS_KEY
|
||||
AWS_SESSION_TOKEN
|
||||
AWS_CREDENTIAL_EXPIRATION
|
||||
```
|
||||
|
||||
`AWS_PROFILE` is unset to prevent conflicts with temporary credentials.
|
||||
253
aws/assume-role.sh
Executable file
253
aws/assume-role.sh
Executable file
@@ -0,0 +1,253 @@
|
||||
#!/usr/bin/env bash
|
||||
#
|
||||
# assume-role.sh - AWS role assumption with MFA support and session caching
|
||||
#
|
||||
# Features:
|
||||
# - MFA token prompting
|
||||
# - Session caching (avoids re-auth within session duration)
|
||||
# - Cross-account support with external-id
|
||||
# - Environment variable export or eval-friendly output
|
||||
# - AWS CLI profile-aware
|
||||
#
|
||||
# Usage:
|
||||
# source assume-role.sh <role-arn> [options]
|
||||
# eval "$(assume-role.sh <role-arn> [options])"
|
||||
#
|
||||
# Options:
|
||||
# -m, --mfa-serial <arn> MFA device ARN (auto-detected if not specified)
|
||||
# -e, --external-id <id> External ID for cross-account roles
|
||||
# -d, --duration <seconds> Session duration (default: 3600, max: 43200)
|
||||
# -s, --session-name <name> Session name (default: assumed-role-session)
|
||||
# -p, --profile <profile> AWS CLI profile for source credentials
|
||||
# -r, --region <region> AWS region
|
||||
# -c, --no-cache Disable session caching
|
||||
# -v, --verbose Verbose output
|
||||
# -h, --help Show this help
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
# Colors for output
|
||||
RED='\033[0;31m'
|
||||
GREEN='\033[0;32m'
|
||||
YELLOW='\033[1;33m'
|
||||
BLUE='\033[0;34m'
|
||||
NC='\033[0m' # No Color
|
||||
|
||||
# Defaults
|
||||
DURATION=3600
|
||||
SESSION_NAME="assumed-role-session-$$"
|
||||
CACHE_DIR="${HOME}/.aws/cli/cache"
|
||||
USE_CACHE=true
|
||||
VERBOSE=false
|
||||
|
||||
# Log functions
|
||||
log_info() { [[ "$VERBOSE" == "true" ]] && echo -e "${BLUE}[INFO]${NC} $*" >&2 || true; }
|
||||
log_success() { echo -e "${GREEN}[OK]${NC} $*" >&2; }
|
||||
log_warn() { echo -e "${YELLOW}[WARN]${NC} $*" >&2; }
|
||||
log_error() { echo -e "${RED}[ERROR]${NC} $*" >&2; }
|
||||
|
||||
usage() {
|
||||
grep '^#' "$0" | grep -v '#!/' | cut -c 3-
|
||||
exit 0
|
||||
}
|
||||
|
||||
# Parse arguments
|
||||
parse_args() {
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case $1 in
|
||||
-m|--mfa-serial) MFA_SERIAL="$2"; shift 2 ;;
|
||||
-e|--external-id) EXTERNAL_ID="$2"; shift 2 ;;
|
||||
-d|--duration) DURATION="$2"; shift 2 ;;
|
||||
-s|--session-name) SESSION_NAME="$2"; shift 2 ;;
|
||||
-p|--profile) AWS_PROFILE="$2"; shift 2 ;;
|
||||
-r|--region) AWS_REGION="$2"; shift 2 ;;
|
||||
-c|--no-cache) USE_CACHE=false; shift ;;
|
||||
-v|--verbose) VERBOSE=true; shift ;;
|
||||
-h|--help) usage ;;
|
||||
-*) log_error "Unknown option: $1"; exit 1 ;;
|
||||
*)
|
||||
if [[ -z "${ROLE_ARN:-}" ]]; then
|
||||
ROLE_ARN="$1"
|
||||
else
|
||||
log_error "Unexpected argument: $1"
|
||||
exit 1
|
||||
fi
|
||||
shift
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
if [[ -z "${ROLE_ARN:-}" ]]; then
|
||||
log_error "Role ARN is required"
|
||||
echo "Usage: assume-role.sh <role-arn> [options]" >&2
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
# Generate cache key based on role and MFA
|
||||
get_cache_key() {
|
||||
local key="${ROLE_ARN}:${MFA_SERIAL:-none}:${EXTERNAL_ID:-none}"
|
||||
echo "$key" | sha256sum | cut -c1-16
|
||||
}
|
||||
|
||||
# Check for valid cached credentials
|
||||
get_cached_credentials() {
|
||||
[[ "$USE_CACHE" != "true" ]] && return 1
|
||||
|
||||
local cache_key
|
||||
cache_key=$(get_cache_key)
|
||||
local cache_file="${CACHE_DIR}/assume-role-${cache_key}.json"
|
||||
|
||||
if [[ -f "$cache_file" ]]; then
|
||||
local expiration
|
||||
expiration=$(jq -r '.Credentials.Expiration' "$cache_file" 2>/dev/null || echo "")
|
||||
|
||||
if [[ -n "$expiration" ]]; then
|
||||
local exp_epoch now_epoch
|
||||
exp_epoch=$(date -d "$expiration" +%s 2>/dev/null || date -j -f "%Y-%m-%dT%H:%M:%S" "${expiration%+*}" +%s 2>/dev/null || echo 0)
|
||||
now_epoch=$(date +%s)
|
||||
|
||||
# Use cached creds if more than 5 minutes remain
|
||||
if (( exp_epoch - now_epoch > 300 )); then
|
||||
log_info "Using cached credentials (expires: $expiration)"
|
||||
cat "$cache_file"
|
||||
return 0
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
|
||||
return 1
|
||||
}
|
||||
|
||||
# Save credentials to cache
|
||||
save_cached_credentials() {
|
||||
[[ "$USE_CACHE" != "true" ]] && return 0
|
||||
|
||||
local credentials="$1"
|
||||
local cache_key
|
||||
cache_key=$(get_cache_key)
|
||||
|
||||
mkdir -p "$CACHE_DIR"
|
||||
chmod 700 "$CACHE_DIR"
|
||||
|
||||
local cache_file="${CACHE_DIR}/assume-role-${cache_key}.json"
|
||||
echo "$credentials" > "$cache_file"
|
||||
chmod 600 "$cache_file"
|
||||
|
||||
log_info "Credentials cached to $cache_file"
|
||||
}
|
||||
|
||||
# Auto-detect MFA device
|
||||
detect_mfa_device() {
|
||||
if [[ -n "${MFA_SERIAL:-}" ]]; then
|
||||
return 0
|
||||
fi
|
||||
|
||||
log_info "Detecting MFA device..."
|
||||
|
||||
local profile_args=()
|
||||
[[ -n "${AWS_PROFILE:-}" ]] && profile_args+=(--profile "$AWS_PROFILE")
|
||||
|
||||
local mfa_devices
|
||||
mfa_devices=$(aws iam list-mfa-devices "${profile_args[@]}" --query 'MFADevices[0].SerialNumber' --output text 2>/dev/null || echo "None")
|
||||
|
||||
if [[ "$mfa_devices" != "None" && -n "$mfa_devices" ]]; then
|
||||
MFA_SERIAL="$mfa_devices"
|
||||
log_info "Detected MFA device: $MFA_SERIAL"
|
||||
fi
|
||||
}
|
||||
|
||||
# Prompt for MFA token
|
||||
get_mfa_token() {
|
||||
local token
|
||||
echo -n "Enter MFA code for ${MFA_SERIAL}: " >&2
|
||||
read -r token
|
||||
echo "$token"
|
||||
}
|
||||
|
||||
# Assume the role
|
||||
assume_role() {
|
||||
local profile_args=()
|
||||
local assume_args=()
|
||||
|
||||
[[ -n "${AWS_PROFILE:-}" ]] && profile_args+=(--profile "$AWS_PROFILE")
|
||||
[[ -n "${AWS_REGION:-}" ]] && profile_args+=(--region "$AWS_REGION")
|
||||
|
||||
assume_args+=(--role-arn "$ROLE_ARN")
|
||||
assume_args+=(--role-session-name "$SESSION_NAME")
|
||||
assume_args+=(--duration-seconds "$DURATION")
|
||||
|
||||
if [[ -n "${EXTERNAL_ID:-}" ]]; then
|
||||
assume_args+=(--external-id "$EXTERNAL_ID")
|
||||
fi
|
||||
|
||||
if [[ -n "${MFA_SERIAL:-}" ]]; then
|
||||
local mfa_token
|
||||
mfa_token=$(get_mfa_token)
|
||||
assume_args+=(--serial-number "$MFA_SERIAL")
|
||||
assume_args+=(--token-code "$mfa_token")
|
||||
fi
|
||||
|
||||
log_info "Assuming role: $ROLE_ARN"
|
||||
|
||||
local result
|
||||
if ! result=$(aws sts assume-role "${profile_args[@]}" "${assume_args[@]}" --output json 2>&1); then
|
||||
log_error "Failed to assume role: $result"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "$result"
|
||||
}
|
||||
|
||||
# Export credentials as environment variables
|
||||
export_credentials() {
|
||||
local credentials="$1"
|
||||
|
||||
local access_key secret_key session_token expiration
|
||||
access_key=$(echo "$credentials" | jq -r '.Credentials.AccessKeyId')
|
||||
secret_key=$(echo "$credentials" | jq -r '.Credentials.SecretAccessKey')
|
||||
session_token=$(echo "$credentials" | jq -r '.Credentials.SessionToken')
|
||||
expiration=$(echo "$credentials" | jq -r '.Credentials.Expiration')
|
||||
|
||||
# Output for eval or sourcing
|
||||
cat <<EOF
|
||||
export AWS_ACCESS_KEY_ID='${access_key}'
|
||||
export AWS_SECRET_ACCESS_KEY='${secret_key}'
|
||||
export AWS_SESSION_TOKEN='${session_token}'
|
||||
export AWS_CREDENTIAL_EXPIRATION='${expiration}'
|
||||
unset AWS_PROFILE
|
||||
EOF
|
||||
|
||||
log_success "Role assumed: $ROLE_ARN"
|
||||
log_info "Session expires: $expiration"
|
||||
}
|
||||
|
||||
# Main
|
||||
main() {
|
||||
parse_args "$@"
|
||||
|
||||
# Check for required tools
|
||||
command -v aws >/dev/null 2>&1 || { log_error "AWS CLI not found"; exit 1; }
|
||||
command -v jq >/dev/null 2>&1 || { log_error "jq not found"; exit 1; }
|
||||
|
||||
# Try cache first
|
||||
local credentials
|
||||
if credentials=$(get_cached_credentials); then
|
||||
export_credentials "$credentials"
|
||||
return 0
|
||||
fi
|
||||
|
||||
# Detect MFA if needed
|
||||
detect_mfa_device
|
||||
|
||||
# Assume role
|
||||
credentials=$(assume_role)
|
||||
|
||||
# Cache for future use
|
||||
save_cached_credentials "$credentials"
|
||||
|
||||
# Export
|
||||
export_credentials "$credentials"
|
||||
}
|
||||
|
||||
main "$@"
|
||||
Reference in New Issue
Block a user