Skip to content

Instantly share code, notes, and snippets.

@philfreo
Last active June 9, 2025 20:14
Show Gist options
  • Save philfreo/0fdb5c027b97f9fa078cc7256bc91e52 to your computer and use it in GitHub Desktop.
Save philfreo/0fdb5c027b97f9fa078cc7256bc91e52 to your computer and use it in GitHub Desktop.
Automatic AWS credential rotation script

Rotating AWS credentials is important for SOC 2 (and enforced by Vanta and similar tools).

You can run the ./rotate_aws_credentials.sh manually, or call it automatically from your dev environment boot-up script like this:

# Rotate AWS credentials if needed
"$(dirname "$0")/rotate_aws_credentials.sh" --min-age 60

Example script output:

Authenticated as AWS user: phil
Current access key AKIXXXXXXXXXXXXX is 6 days old

Creating backup of current credentials at: ~/.aws/credentials.backup.20250609_161230

Creating new access key...
✅ New access key created: AKIYYYYYYYYYYYYY

Updating local AWS credentials...
Waiting for AWS to propagate new credentials...

Testing new credentials...
✅ New credentials are working correctly!

Deleting old access key: AKIXXXXXXXXXXXXX
✅ Old access key deleted successfully!

🎉 Credential rotation completed successfully!

Summary:
  Old key (deleted): AKIXXXXXXXXXXXXX
  New key (active):  AKIYYYYYYYYYYYYY

Next rotation due: 2025-09-07

Remove credentials backup file (~/.aws/credentials.backup.20250609_161230)? (Y/n): 

Backup file removed.
#!/bin/bash
# AWS Credential Rotation Script
# Run this script manually (e.g. quarterly) to rotate your AWS access keys for you.
# Or call it directly from your dev env bootup script (with a --max-age) to automatically
# rotate your credentials when they get too old.
set -e
PROFILE="default" # CHANGE IF NEEDED
REGION="us-west-2" # CHANGE IF NEEDED
MIN_AGE_DAYS=""
# Parse command line arguments
while [[ $# -gt 0 ]]; do
case $1 in
--min-age)
MIN_AGE_DAYS="$2"
shift 2
;;
--help|-h)
echo "Usage: $0 [--min-age DAYS]"
echo ""
echo "Options:"
echo " --min-age DAYS Only rotate if current key is at least DAYS old"
echo " --help, -h Show this help message"
exit 0
;;
*)
echo "Unknown option: $1"
echo "Use --help for usage information"
exit 1
;;
esac
done
# Check if AWS CLI is installed
if ! command -v aws &> /dev/null; then
echo "Error: AWS CLI is not installed. Please install it with: brew install awscli"
exit 1
fi
# Check if profile exists
if ! aws configure list-profiles | grep -q "^${PROFILE}$"; then
echo "Error: AWS profile '${PROFILE}' not found."
echo "Please run: aws configure --profile ${PROFILE}"
exit 1
fi
# Test current AWS credentials
CURRENT_USER=$(aws --profile ${PROFILE} sts get-caller-identity --query 'Arn' --output text) || {
echo "Error: Unable to authenticate with current credentials."
echo "Please check your existing AWS configuration."
exit 1
}
USERNAME=$(echo $CURRENT_USER | cut -d'/' -f2)
echo "Authenticated as AWS user: ${USERNAME}"
# Get current access keys
CURRENT_KEYS=$(aws --profile ${PROFILE} iam list-access-keys --user-name ${USERNAME} --query 'AccessKeyMetadata[].AccessKeyId' --output text)
CURRENT_KEY_ID=$(aws --profile ${PROFILE} configure get aws_access_key_id)
# Calculate key age
KEY_AGE_STR=$(aws --profile ${PROFILE} iam list-access-keys --user-name ${USERNAME} --query "AccessKeyMetadata[?AccessKeyId=='${CURRENT_KEY_ID}'].CreateDate" --output text)
if [[ -z "$KEY_AGE_STR" ]]; then
echo "Error: Could not retrieve creation date for current access key"
exit 1
fi
# Calculate key age in days
if [[ "$OSTYPE" == "darwin"* ]]; then
# macOS: Convert +00:00 to +0000 for date command
KEY_AGE_STR_FIXED=$(echo "$KEY_AGE_STR" | sed 's/+00:00/+0000/')
KEY_AGE_DAYS=$(( ($(date +%s) - $(date -j -f "%Y-%m-%dT%H:%M:%S%z" "$KEY_AGE_STR_FIXED" +%s)) / 86400 ))
else
# Linux
KEY_AGE_DAYS=$(( ($(date +%s) - $(date -d "$KEY_AGE_STR" +%s)) / 86400 ))
fi
echo "Current access key ${CURRENT_KEY_ID} is ${KEY_AGE_DAYS} days old"
# Check key age if min-age is specified
if [[ -n "$MIN_AGE_DAYS" ]]; then
if [[ $KEY_AGE_DAYS -lt $MIN_AGE_DAYS ]]; then
echo "Skipping AWS key rotation"
exit 0
fi
echo "Key is ${KEY_AGE_DAYS} days old so it will now be rotated..."
fi
# Check if user already has 2 keys (AWS limit)
KEY_COUNT=$(echo $CURRENT_KEYS | wc -w)
if [ $KEY_COUNT -ge 2 ]; then
echo ""
echo "Warning: You already have ${KEY_COUNT} access keys (AWS limit is 2)."
echo "Current keys: $CURRENT_KEYS"
echo ""
# Find the key that's NOT currently configured
for key in $CURRENT_KEYS; do
if [ "$key" != "$CURRENT_KEY_ID" ]; then
OLD_KEY_TO_DELETE="$key"
break
fi
done
read -p "Do you want to delete the old key ${OLD_KEY_TO_DELETE} first and then create a new one? (y/n): " -n 1 -r
echo
if [[ ! $REPLY =~ ^[Yy]$ ]]; then
echo "Aborted. Please manually delete an old key first or choose to proceed."
exit 1
fi
if [ -n "$OLD_KEY_TO_DELETE" ]; then
echo "Deleting old access key: ${OLD_KEY_TO_DELETE}"
aws --profile ${PROFILE} iam delete-access-key --user-name ${USERNAME} --access-key-id ${OLD_KEY_TO_DELETE}
echo "✅ Old key deleted successfully"
fi
fi
# Create backup of current credentials
BACKUP_FILE="$HOME/.aws/credentials.backup.$(date +%Y%m%d_%H%M%S)"
echo ""
echo "Creating backup of current credentials at: ${BACKUP_FILE}"
cp "$HOME/.aws/credentials" "${BACKUP_FILE}"
# Create new access key
echo ""
echo "Creating new access key..."
NEW_KEY_JSON=$(aws --profile ${PROFILE} iam create-access-key --user-name ${USERNAME}) || {
echo "Error: Failed to create new access key. Check your IAM permissions."
echo "You need the following permissions: iam:CreateAccessKey, iam:DeleteAccessKey, iam:ListAccessKeys"
exit 1
}
NEW_ACCESS_KEY_ID=$(echo "$NEW_KEY_JSON" | grep -o '"AccessKeyId": "[^"]*"' | cut -d'"' -f4)
NEW_SECRET_ACCESS_KEY=$(echo "$NEW_KEY_JSON" | grep -o '"SecretAccessKey": "[^"]*"' | cut -d'"' -f4)
if [[ -z "$NEW_ACCESS_KEY_ID" || -z "$NEW_SECRET_ACCESS_KEY" ]]; then
echo "Error: Failed to extract new access key details"
echo "JSON Output: $NEW_KEY_JSON"
exit 1
fi
echo "✅ New access key created: ${NEW_ACCESS_KEY_ID}"
# Update local AWS configuration
echo ""
echo "Updating local AWS credentials..."
aws configure set aws_access_key_id "${NEW_ACCESS_KEY_ID}" --profile ${PROFILE}
aws configure set aws_secret_access_key "${NEW_SECRET_ACCESS_KEY}" --profile ${PROFILE}
aws configure set region "${REGION}" --profile ${PROFILE}
# Wait a moment for AWS to propagate the new key
echo "Waiting for AWS to propagate new credentials..."
sleep 5
# Test new credentials
echo ""
echo "Testing new credentials..."
if aws --profile ${PROFILE} sts get-caller-identity >/dev/null 2>&1; then
echo "✅ New credentials are working correctly!"
# Delete the old access key
echo ""
echo "Deleting old access key: ${CURRENT_KEY_ID}"
aws --profile ${PROFILE} iam delete-access-key --user-name ${USERNAME} --access-key-id ${CURRENT_KEY_ID}
echo "✅ Old access key deleted successfully!"
echo ""
echo "🎉 Credential rotation completed successfully!"
echo ""
echo "Summary:"
echo " Old key (deleted): ${CURRENT_KEY_ID}"
echo " New key (active): ${NEW_ACCESS_KEY_ID}"
echo ""
echo "Next rotation due: $(date -v +90d '+%Y-%m-%d')"
# Clean up backup
echo ""
read -p "Remove credentials backup file (${BACKUP_FILE})? (Y/n): " -n 1 -r
echo
if [[ ! $REPLY =~ ^[Nn]$ ]]; then
rm "${BACKUP_FILE}"
echo "Backup file removed."
else
echo "Backup file preserved at: ${BACKUP_FILE}"
fi
else
echo ""
echo "❌ New credentials failed to authenticate or timed out."
echo "Restoring previous credentials from backup..."
cp "${BACKUP_FILE}" "$HOME/.aws/credentials"
# Try to delete the failed new key
echo "Cleaning up failed new access key..."
aws --profile ${PROFILE} iam delete-access-key --user-name ${USERNAME} --access-key-id ${NEW_ACCESS_KEY_ID} 2>/dev/null || true
echo "Previous credentials restored."
exit 1
fi
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment