aws-secrets-rotation
Automate AWS secrets rotation for RDS, API keys, and credentials
- category
- security
- risk
- safe
- source
- community
- tags
- [aws, secrets-manager, security, automation, kiro-cli, credentials]
- date added
- 2026-02-27
AWS Secrets Rotation
Automate rotation of secrets, credentials, and API keys using AWS Secrets Manager and Lambda.
When to Use
Use this skill when you need to implement automated secrets rotation, manage credentials securely, or comply with security policies requiring regular key rotation.
Supported Secret Types
AWS Services
- RDS database credentials
- DocumentDB credentials
- Redshift credentials
- ElastiCache credentials
Third-Party Services
- API keys
- OAuth tokens
- SSH keys
- Custom credentials
Secrets Manager Setup
Create a Secret
# Create RDS secret aws secretsmanager create-secret \ --name prod/db/mysql \ --description "Production MySQL credentials" \ --secret-string '{ "username": "admin", "password": "CHANGE_ME", "engine": "mysql", "host": "mydb.cluster-abc.us-east-1.rds.amazonaws.com", "port": 3306, "dbname": "myapp" }' # Create API key secret aws secretsmanager create-secret \ --name prod/api/stripe \ --secret-string '{ "api_key": "sk_live_xxxxx", "webhook_secret": "whsec_xxxxx" }' # Create secret from file aws secretsmanager create-secret \ --name prod/ssh/private-key \ --secret-binary fileb://~/.ssh/id_rsa
Retrieve Secrets
# Get secret value aws secretsmanager get-secret-value \ --secret-id prod/db/mysql \ --query 'SecretString' --output text # Get specific field aws secretsmanager get-secret-value \ --secret-id prod/db/mysql \ --query 'SecretString' --output text | \ jq -r '.password' # Get binary secret aws secretsmanager get-secret-value \ --secret-id prod/ssh/private-key \ --query 'SecretBinary' --output text | \ base64 -d > private-key.pem
Automatic Rotation Setup
Enable RDS Rotation
# Enable automatic rotation (30 days) aws secretsmanager rotate-secret \ --secret-id prod/db/mysql \ --rotation-lambda-arn arn:aws:lambda:us-east-1:123456789012:function:SecretsManagerRDSMySQLRotation \ --rotation-rules AutomaticallyAfterDays=30 # Rotate immediately aws secretsmanager rotate-secret \ --secret-id prod/db/mysql # Check rotation status aws secretsmanager describe-secret \ --secret-id prod/db/mysql \ --query 'RotationEnabled'
Lambda Rotation Function
# lambda_rotation.py import boto3 import json import os secrets_client = boto3.client('secretsmanager') rds_client = boto3.client('rds') def lambda_handler(event, context): """Rotate RDS MySQL password""" secret_arn = event['SecretId'] token = event['ClientRequestToken'] step = event['Step'] # Get current secret current = secrets_client.get_secret_value(SecretId=secret_arn) secret = json.loads(current['SecretString']) if step == "createSecret": # Generate new password new_password = generate_password() secret['password'] = new_password # Store as pending secrets_client.put_secret_value( SecretId=secret_arn, ClientRequestToken=token, SecretString=json.dumps(secret), VersionStages=['AWSPENDING'] ) elif step == "setSecret": # Update RDS password rds_client.modify_db_instance( DBInstanceIdentifier=secret['dbInstanceIdentifier'], MasterUserPassword=secret['password'], ApplyImmediately=True ) elif step == "testSecret": # Test new credentials import pymysql conn = pymysql.connect( host=secret['host'], user=secret['username'], password=secret['password'], database=secret['dbname'] ) conn.close() elif step == "finishSecret": # Mark as current secrets_client.update_secret_version_stage( SecretId=secret_arn, VersionStage='AWSCURRENT', MoveToVersionId=token, RemoveFromVersionId=current['VersionId'] ) return {'statusCode': 200} def generate_password(length=32): import secrets import string alphabet = string.ascii_letters + string.digits + "!@#$%^&*()" return ''.join(secrets.choice(alphabet) for _ in range(length))
Custom Rotation for API Keys
# api_key_rotation.py import boto3 import requests import json secrets_client = boto3.client('secretsmanager') def rotate_stripe_key(secret_arn, token, step): """Rotate Stripe API key""" current = secrets_client.get_secret_value(SecretId=secret_arn) secret = json.loads(current['SecretString']) if step == "createSecret": # Create new Stripe key via API response = requests.post( 'https://api.stripe.com/v1/api_keys', auth=(secret['api_key'], ''), data={'name': f'rotated-{token[:8]}'} ) new_key = response.json()['secret'] secret['api_key'] = new_key secrets_client.put_secret_value( SecretId=secret_arn, ClientRequestToken=token, SecretString=json.dumps(secret), VersionStages=['AWSPENDING'] ) elif step == "testSecret": # Test new key response = requests.get( 'https://api.stripe.com/v1/balance', auth=(secret['api_key'], '') ) if response.status_code != 200: raise Exception("New key failed validation") elif step == "finishSecret": # Revoke old key old_key = json.loads(current['SecretString'])['api_key'] requests.delete( f'https://api.stripe.com/v1/api_keys/{old_key}', auth=(secret['api_key'], '') ) # Promote to current secrets_client.update_secret_version_stage( SecretId=secret_arn, VersionStage='AWSCURRENT', MoveToVersionId=token )
Rotation Monitoring
CloudWatch Alarms
# Create alarm for rotation failures aws cloudwatch put-metric-alarm \ --alarm-name secrets-rotation-failures \ --alarm-description "Alert on secrets rotation failures" \ --metric-name RotationFailed \ --namespace AWS/SecretsManager \ --statistic Sum \ --period 300 \ --evaluation-periods 1 \ --threshold 1 \ --comparison-operator GreaterThanThreshold \ --alarm-actions arn:aws:sns:us-east-1:123456789012:alerts
Rotation Audit Script
#!/bin/bash # audit-rotations.sh echo "Secrets Rotation Audit" echo "=====================" aws secretsmanager list-secrets --query 'SecretList[*].[Name,RotationEnabled,LastRotatedDate]' \ --output text | \ while read name enabled last_rotated; do echo "" echo "Secret: $name" echo " Rotation Enabled: $enabled" echo " Last Rotated: $last_rotated" if [ "$enabled" = "True" ]; then # Check rotation schedule rules=$(aws secretsmanager describe-secret --secret-id "$name" \ --query 'RotationRules.AutomaticallyAfterDays' --output text) echo " Rotation Schedule: Every $rules days" # Calculate days since last rotation if [ "$last_rotated" != "None" ]; then days_ago=$(( ($(date +%s) - $(date -d "$last_rotated" +%s)) / 86400 )) echo " Days Since Rotation: $days_ago" if [ $days_ago -gt $rules ]; then echo " ⚠️ OVERDUE for rotation!" fi fi fi done
Application Integration
Python SDK
import boto3 import json def get_secret(secret_name): """Retrieve secret from Secrets Manager""" client = boto3.client('secretsmanager') try: response = client.get_secret_value(SecretId=secret_name) return json.loads(response['SecretString']) except Exception as e: print(f"Error retrieving secret: {e}") raise # Usage db_creds = get_secret('prod/db/mysql') connection = pymysql.connect( host=db_creds['host'], user=db_creds['username'], password=db_creds['password'], database=db_creds['dbname'] )
Node.js SDK
const AWS = require('aws-sdk'); const secretsManager = new AWS.SecretsManager(); async function getSecret(secretName) { try { const data = await secretsManager.getSecretValue({ SecretId: secretName }).promise(); return JSON.parse(data.SecretString); } catch (err) { console.error('Error retrieving secret:', err); throw err; } } // Usage const dbCreds = await getSecret('prod/db/mysql'); const connection = mysql.createConnection({ host: dbCreds.host, user: dbCreds.username, password: dbCreds.password, database: dbCreds.dbname });
Rotation Best Practices
Planning
- Identify all secrets requiring rotation
- Define rotation schedules (30, 60, 90 days)
- Test rotation in non-production first
- Document rotation procedures
- Plan for emergency rotation
Implementation
- Use AWS managed rotation when possible
- Implement proper error handling
- Add CloudWatch monitoring
- Test application compatibility
- Implement gradual rollout
Operations
- Monitor rotation success/failure
- Set up alerts for failures
- Regular rotation audits
- Document troubleshooting steps
- Maintain rotation runbooks
Emergency Rotation
# Immediate rotation (compromise detected) aws secretsmanager rotate-secret \ --secret-id prod/db/mysql \ --rotate-immediately # Force rotation even if recently rotated aws secretsmanager rotate-secret \ --secret-id prod/api/stripe \ --rotation-lambda-arn arn:aws:lambda:us-east-1:123456789012:function:RotateStripeKey \ --rotate-immediately # Verify rotation completed aws secretsmanager describe-secret \ --secret-id prod/db/mysql \ --query 'LastRotatedDate'
Compliance Tracking
#!/usr/bin/env python3 # compliance-report.py import boto3 from datetime import datetime, timedelta client = boto3.client('secretsmanager') def generate_compliance_report(): secrets = client.list_secrets()['SecretList'] compliant = [] non_compliant = [] for secret in secrets: name = secret['Name'] rotation_enabled = secret.get('RotationEnabled', False) last_rotated = secret.get('LastRotatedDate') if not rotation_enabled: non_compliant.append({ 'name': name, 'issue': 'Rotation not enabled' }) continue if last_rotated: days_ago = (datetime.now(last_rotated.tzinfo) - last_rotated).days if days_ago > 90: non_compliant.append({ 'name': name, 'issue': f'Not rotated in {days_ago} days' }) else: compliant.append(name) else: non_compliant.append({ 'name': name, 'issue': 'Never rotated' }) print(f"Compliant Secrets: {len(compliant)}") print(f"Non-Compliant Secrets: {len(non_compliant)}") print("\nNon-Compliant Details:") for item in non_compliant: print(f" - {item['name']}: {item['issue']}") if __name__ == "__main__": generate_compliance_report()
Example Prompts
- "Set up automatic rotation for my RDS credentials"
- "Create a Lambda function to rotate API keys"
- "Audit all secrets for rotation compliance"
- "Implement emergency rotation for compromised credentials"
- "Generate a secrets rotation report"
Kiro CLI Integration
kiro-cli chat "Use aws-secrets-rotation to set up RDS credential rotation" kiro-cli chat "Create a rotation audit report with aws-secrets-rotation"