Lambda secret rotation on RDS passwords is surprisingly complex because the AWS Secrets Manager system itself is designed to be a secure vault, not a dynamic credential provider for other AWS services.
Let’s watch this happen. Imagine you’ve configured a Lambda function to rotate your RDS password. You’ve set up a scheduled event for this Lambda, and it’s supposed to go off every 30 days. The Lambda function, armed with the necessary IAM permissions, first retrieves the current secret from Secrets Manager. It then uses this retrieved secret to connect to your RDS instance. Once connected, it executes an ALTER USER command to change the password for the database user associated with the secret. After successfully changing the password, it updates the secret in Secrets Manager with the new password. Finally, it performs a "finish" rotation step in Secrets Manager, marking the new password as the primary one.
The core problem this solves is the operational overhead and security risk of manually rotating database credentials. Storing credentials directly in code or configuration files is a major security anti-pattern. Manual rotation is tedious, prone to human error, and often neglected, leading to stale credentials that are more likely to be compromised. By automating this process, you ensure that your database passwords are changed regularly, significantly reducing the attack surface.
Internally, AWS Secrets Manager uses a "staging label" system to manage secret versions. When you initiate a rotation, Secrets Manager creates a new, unpublished version of the secret. The Lambda function then updates this new version with the rotated credential. Once the Lambda confirms the rotation was successful, it tells Secrets Manager to "promote" this new version to the primary, or "AWSCURRENT" label. The old password remains available under an "AWSPREVIOUS" label for a grace period, allowing for quick rollback if something goes wrong.
Here’s a snippet of what the Lambda function might look like, focusing on the core logic.
import boto3
import json
import rds_config # A hypothetical module for RDS connection details
def lambda_handler(event, context):
# Retrieve secret ARN and other rotation details from the event
secret_arn = event['SecretId']
# Get the current secret information
secrets_client = boto3.client('secretsmanager')
get_secret_value_response = secrets_client.get_secret_value(
SecretId=secret_arn
)
current_secret = json.loads(get_secret_value_response['SecretString'])
# Extract database credentials
username = current_secret['username']
# The password here is the OLD password, used to connect and change it
old_password = current_secret['password']
host = rds_config.DB_HOST
port = rds_config.DB_PORT
db_name = rds_config.DB_NAME
# Generate a new password (e.g., using a library or AWS KMS)
new_password = generate_new_password() # Placeholder for actual password generation
# Connect to RDS and change the password
try:
# Use the old password to connect to the DB
conn = connect_to_rds(host, port, db_name, username, old_password)
cursor = conn.cursor()
# The actual SQL command to change the password varies by database engine
cursor.execute(f"ALTER USER {username} PASSWORD '{new_password}';")
conn.commit()
cursor.close()
conn.close()
except Exception as e:
print(f"Failed to update password in RDS: {e}")
raise e
# Update the secret in Secrets Manager with the new password
try:
secrets_client.update_secret(
SecretId=secret_arn,
SecretString=json.dumps({'username': username, 'password': new_password, 'host': host, 'port': port, 'dbname': db_name})
)
except Exception as e:
print(f"Failed to update secret in Secrets Manager: {e}")
raise e
# Finish the rotation process in Secrets Manager
secrets_client.finish_secret_rotation(
SecretId=secret_arn,
# VersionStage can be AWSCURRENT or a custom stage if needed
VersionStage='AWSCURRENT'
)
return {
'status': 'success',
'message': 'Secret rotated successfully'
}
# Placeholder functions for actual implementation
def generate_new_password():
# In a real scenario, use a secure method, e.g., from AWS KMS or a secrets library
return "a_new_super_secure_password_123!"
def connect_to_rds(host, port, db_name, username, password):
# This is where you'd import your specific database connector (e.g., psycopg2, mysql.connector)
# and establish the connection.
print(f"Connecting to {db_name}@{host}:{port} with user {username}...")
# Simulate connection success
class MockCursor:
def execute(self, query): print(f"Executing: {query}")
def close(self): pass
class MockConnection:
def cursor(self): return MockCursor()
def commit(self): print("Committing transaction.")
def close(self): print("Closing connection.")
return MockConnection()
A common point of confusion is how the Lambda function gets the old password to connect to RDS and perform the ALTER USER command. Secrets Manager provides this through the get_secret_value API call. The Lambda function then uses this retrieved old password to establish its initial database connection. Once the password is changed within the database, the Lambda updates the secret in Secrets Manager with the new password.
The most surprising aspect for many is that Secrets Manager doesn’t automatically handle the database-specific commands for password rotation. It provides the framework and the API for updating secrets, but the logic to connect to your specific database (PostgreSQL, MySQL, SQL Server, etc.) and execute the correct ALTER USER statement (which varies slightly between engines) must be implemented entirely within your Lambda function.
The next hurdle you’ll likely encounter is handling different database engines and their specific password change syntaxes, especially if you have multiple RDS instances with varying configurations.