Last active
February 4, 2026 20:08
-
-
Save andrew-templeton/35448042c64fe9b148f7b682bd4df399 to your computer and use it in GitHub Desktop.
AWS SSO to read-only session for agent use
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #!/bin/bash | |
| # aws-readonly | |
| # Creates a 12-hour read-only AWS session for agent use | |
| # Auto-creates the ReadOnlyAccess role via CloudFormation if it doesn't exist | |
| # | |
| # Usage: aws-readonly <sso-profile-name> [output-profile-name] | |
| # | |
| # Example: | |
| # aws-readonly csc-prod-admin # Creates csc-prod-admin-readonly | |
| # aws-readonly csc-prod-admin agent-ro # Creates agent-ro | |
| set -e | |
| # Configuration | |
| READONLY_ROLE="${AWS_READONLY_ROLE:-AgentReadOnlyAccess}" | |
| STACK_NAME="agent-readonly-role" | |
| # Parse arguments | |
| SSO_PROFILE="${1:?Usage: aws-readonly <sso-profile-name> [output-profile-name]}" | |
| OUTPUT_PROFILE="${2:-${SSO_PROFILE}-readonly}" | |
| # Colors for output | |
| RED='\033[0;31m' | |
| GREEN='\033[0;32m' | |
| YELLOW='\033[1;33m' | |
| NC='\033[0m' | |
| log() { echo -e "${GREEN}[+]${NC} $1"; } | |
| warn() { echo -e "${YELLOW}[!]${NC} $1"; } | |
| error() { echo -e "${RED}[x]${NC} $1" >&2; } | |
| # Step 1: Extract account ID from profile config | |
| log "Reading profile config for: ${SSO_PROFILE}" | |
| ACCOUNT_ID=$(aws configure get sso_account_id --profile "${SSO_PROFILE}" 2>/dev/null || true) | |
| if [ -z "$ACCOUNT_ID" ]; then | |
| error "Could not find sso_account_id for profile '${SSO_PROFILE}'" | |
| error "Make sure the profile exists in ~/.aws/config with SSO settings" | |
| echo "" | |
| echo "Expected format in ~/.aws/config:" | |
| echo " [profile ${SSO_PROFILE}]" | |
| echo " sso_start_url = https://your-sso.awsapps.com/start" | |
| echo " sso_region = us-east-1" | |
| echo " sso_account_id = 123456789012" | |
| echo " sso_role_name = AdministratorAccess" | |
| exit 1 | |
| fi | |
| log "Account ID: ${ACCOUNT_ID}" | |
| # Step 2: Initiate SSO login | |
| log "Initiating SSO login (browser will open)..." | |
| aws sso login --profile "${SSO_PROFILE}" | |
| # Step 3: Verify SSO session works and get caller identity | |
| log "Verifying SSO session..." | |
| CALLER_IDENTITY=$(aws sts get-caller-identity --profile "${SSO_PROFILE}" --output json) | |
| CALLER_ARN=$(echo "$CALLER_IDENTITY" | jq -r '.Arn') | |
| log "Authenticated as: ${CALLER_ARN}" | |
| # Extract the role name and get full ARN (SSO roles have paths like /aws-reserved/sso.amazonaws.com/) | |
| SSO_ROLE_NAME=$(echo "$CALLER_ARN" | sed -n 's|.*assumed-role/\([^/]*\)/.*|\1|p') | |
| SSO_ROLE_ARN=$(aws iam get-role --profile "${SSO_PROFILE}" --role-name "${SSO_ROLE_NAME}" --query "Role.Arn" --output text) | |
| log "SSO Role: ${SSO_ROLE_ARN}" | |
| # Step 4: Check if role exists, create via CloudFormation if not | |
| ROLE_ARN="arn:aws:iam::${ACCOUNT_ID}:role/${READONLY_ROLE}" | |
| log "Checking if ${READONLY_ROLE} role exists..." | |
| if ! aws iam get-role --role-name "${READONLY_ROLE}" --profile "${SSO_PROFILE}" >/dev/null 2>&1; then | |
| warn "Role ${READONLY_ROLE} does not exist. Creating via CloudFormation..." | |
| # Create CloudFormation template | |
| CFN_TEMPLATE=$(cat <<EOF | |
| AWSTemplateFormatVersion: '2010-09-09' | |
| Description: 'Read-only role for agent access - created by aws-readonly script' | |
| Parameters: | |
| TrustedRoleArn: | |
| Type: String | |
| Description: ARN of the role allowed to assume this read-only role | |
| Resources: | |
| AgentReadOnlyRole: | |
| Type: AWS::IAM::Role | |
| Properties: | |
| RoleName: ${READONLY_ROLE} | |
| MaxSessionDuration: 43200 | |
| AssumeRolePolicyDocument: | |
| Version: '2012-10-17' | |
| Statement: | |
| - Effect: Allow | |
| Principal: | |
| AWS: !Ref TrustedRoleArn | |
| Action: sts:AssumeRole | |
| ManagedPolicyArns: | |
| - arn:aws:iam::aws:policy/ReadOnlyAccess | |
| Tags: | |
| - Key: CreatedBy | |
| Value: aws-readonly-script | |
| - Key: Purpose | |
| Value: Agent read-only access | |
| Outputs: | |
| RoleArn: | |
| Description: ARN of the created read-only role | |
| Value: !GetAtt AgentReadOnlyRole.Arn | |
| EOF | |
| ) | |
| # Deploy CloudFormation stack | |
| log "Deploying CloudFormation stack: ${STACK_NAME}..." | |
| CFN_TEMP_FILE=$(mktemp -t cfn-readonly).yaml | |
| echo "$CFN_TEMPLATE" > "$CFN_TEMP_FILE" | |
| aws cloudformation deploy \ | |
| --profile "${SSO_PROFILE}" \ | |
| --stack-name "${STACK_NAME}" \ | |
| --template-file "$CFN_TEMP_FILE" \ | |
| --parameter-overrides "TrustedRoleArn=${SSO_ROLE_ARN}" \ | |
| --capabilities CAPABILITY_NAMED_IAM \ | |
| --no-fail-on-empty-changeset | |
| rm -f "$CFN_TEMP_FILE" | |
| log "Waiting for stack to complete..." | |
| aws cloudformation wait stack-create-complete \ | |
| --profile "${SSO_PROFILE}" \ | |
| --stack-name "${STACK_NAME}" 2>/dev/null || \ | |
| aws cloudformation wait stack-update-complete \ | |
| --profile "${SSO_PROFILE}" \ | |
| --stack-name "${STACK_NAME}" 2>/dev/null || true | |
| # Verify role was created | |
| STACK_STATUS=$(aws cloudformation describe-stacks \ | |
| --profile "${SSO_PROFILE}" \ | |
| --stack-name "${STACK_NAME}" \ | |
| --query "Stacks[0].StackStatus" \ | |
| --output text) | |
| if [[ "$STACK_STATUS" != *"COMPLETE"* ]] || [[ "$STACK_STATUS" == *"ROLLBACK"* ]]; then | |
| error "CloudFormation stack failed with status: ${STACK_STATUS}" | |
| error "Check the AWS Console for details" | |
| exit 1 | |
| fi | |
| log "CloudFormation stack deployed successfully (${STACK_STATUS})" | |
| # Brief pause for IAM propagation | |
| log "Waiting for IAM propagation..." | |
| sleep 5 | |
| else | |
| log "Role ${READONLY_ROLE} already exists" | |
| fi | |
| # Step 5: Assume the read-only role | |
| # Note: Role chaining (assuming from an assumed role like SSO) limits to 1 hour max | |
| EFFECTIVE_DURATION=3600 # 1 hour - role chaining limit | |
| log "Assuming read-only role (${READONLY_ROLE}) for account ${ACCOUNT_ID}..." | |
| log "Session duration: ${EFFECTIVE_DURATION}s (1 hour - role chaining limit)" | |
| SESSION_NAME="agent-session-$(date +%Y%m%d)" | |
| CREDS=$(aws sts assume-role \ | |
| --profile "${SSO_PROFILE}" \ | |
| --role-arn "${ROLE_ARN}" \ | |
| --role-session-name "${SESSION_NAME}" \ | |
| --duration-seconds "${EFFECTIVE_DURATION}" \ | |
| --output json 2>&1) || { | |
| error "Failed to assume role ${ROLE_ARN}" | |
| echo "$CREDS" | |
| exit 1 | |
| } | |
| # Extract credentials | |
| ACCESS_KEY=$(echo "$CREDS" | jq -r '.Credentials.AccessKeyId') | |
| SECRET_KEY=$(echo "$CREDS" | jq -r '.Credentials.SecretAccessKey') | |
| SESSION_TOKEN=$(echo "$CREDS" | jq -r '.Credentials.SessionToken') | |
| EXPIRATION=$(echo "$CREDS" | jq -r '.Credentials.Expiration') | |
| # Step 6: Write to credentials file | |
| CREDS_FILE="$HOME/.aws/credentials" | |
| touch "$CREDS_FILE" | |
| # Remove existing profile section if present | |
| if grep -q "^\[${OUTPUT_PROFILE}\]" "$CREDS_FILE" 2>/dev/null; then | |
| log "Removing existing ${OUTPUT_PROFILE} profile..." | |
| awk -v profile="[${OUTPUT_PROFILE}]" ' | |
| $0 == profile { skip=1; next } | |
| /^\[/ { skip=0 } | |
| !skip { print } | |
| ' "$CREDS_FILE" > "$CREDS_FILE.tmp" | |
| mv "$CREDS_FILE.tmp" "$CREDS_FILE" | |
| fi | |
| # Append new credentials | |
| cat >> "$CREDS_FILE" << EOF | |
| [${OUTPUT_PROFILE}] | |
| aws_access_key_id = ${ACCESS_KEY} | |
| aws_secret_access_key = ${SECRET_KEY} | |
| aws_session_token = ${SESSION_TOKEN} | |
| # Expires: ${EXPIRATION} | |
| # Source: ${SSO_PROFILE} | |
| # Account: ${ACCOUNT_ID} | |
| # Role: ${READONLY_ROLE} | |
| EOF | |
| log "Credentials written to profile: ${OUTPUT_PROFILE}" | |
| # Step 7: Verify the session works | |
| log "Verifying readonly session..." | |
| IDENTITY=$(aws sts get-caller-identity --profile "${OUTPUT_PROFILE}" --output json) | |
| ASSUMED_ARN=$(echo "$IDENTITY" | jq -r '.Arn') | |
| echo "" | |
| echo "==========================================" | |
| echo -e "${GREEN}Session created successfully!${NC}" | |
| echo "==========================================" | |
| echo "Profile: ${OUTPUT_PROFILE}" | |
| echo "Source: ${SSO_PROFILE}" | |
| echo "Account: ${ACCOUNT_ID}" | |
| echo "Role: ${READONLY_ROLE}" | |
| echo "Expires: ${EXPIRATION}" | |
| echo "ARN: ${ASSUMED_ARN}" | |
| echo "" | |
| echo "Usage:" | |
| echo " aws s3 ls --profile ${OUTPUT_PROFILE}" | |
| echo " AWS_PROFILE=${OUTPUT_PROFILE} aws ec2 describe-instances" | |
| echo "" | |
| echo "For agents:" | |
| echo " 'Use AWS_PROFILE=${OUTPUT_PROFILE} for all AWS CLI commands'" | |
| echo "==========================================" |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment