Skip to main content

Enable Automatic Rotation for Secrets Manager Secrets

Overview

This check verifies that your AWS Secrets Manager secrets have automatic rotation enabled. Automatic rotation periodically replaces your secret values without manual intervention, reducing the risk of credential compromise.

Risk

Severity: High

Without automatic rotation:

  • Leaked credentials remain valid longer - If a secret is exposed in code, logs, or container images, attackers can use it until someone manually rotates it
  • Compliance violations - Many security frameworks (SOC 2, PCI-DSS, HIPAA) require regular credential rotation
  • Increased blast radius - Long-lived secrets give attackers more time to move laterally through your systems
  • Harder incident response - Without rotation, you cannot be certain old credentials are no longer in use

Remediation Steps

Prerequisites

  • AWS Console access with permissions to modify Secrets Manager secrets
  • For custom rotation: ability to create Lambda functions
Required IAM permissions

To enable rotation, you need these permissions:

{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": [
"secretsmanager:RotateSecret",
"secretsmanager:DescribeSecret",
"secretsmanager:UpdateSecret"
],
"Resource": "arn:aws:secretsmanager:us-east-1:*:secret:*"
},
{
"Effect": "Allow",
"Action": [
"lambda:CreateFunction",
"lambda:InvokeFunction",
"lambda:AddPermission"
],
"Resource": "*"
}
]
}

AWS Console Method

  1. Open the AWS Console and navigate to Secrets Manager
  2. Select the secret you want to configure
  3. Scroll down to the Rotation configuration section
  4. Click Edit rotation
  5. Toggle Automatic rotation to On
  6. Choose your rotation schedule:
    • For database credentials: Select Managed rotation (AWS handles the Lambda function)
    • For other secrets: Choose an existing Lambda function or create a new one
  7. Set the Rotation schedule (recommended: 30 days for sensitive credentials)
  8. Click Save

Tip: For RDS, Redshift, and DocumentDB credentials, AWS provides managed rotation that automatically creates and manages the rotation Lambda function for you.

AWS CLI (optional)

Enable rotation for an existing secret

For managed rotation (RDS, Redshift, DocumentDB):

aws secretsmanager rotate-secret \
--secret-id my-database-secret \
--rotation-rules '{"AutomaticallyAfterDays": 30}' \
--region us-east-1

For custom rotation with a Lambda function:

aws secretsmanager rotate-secret \
--secret-id my-secret \
--rotation-lambda-arn arn:aws:lambda:us-east-1:123456789012:function:my-rotation-function \
--rotation-rules '{"AutomaticallyAfterDays": 30}' \
--region us-east-1

Using a cron schedule for more control

aws secretsmanager rotate-secret \
--secret-id my-secret \
--rotation-lambda-arn arn:aws:lambda:us-east-1:123456789012:function:my-rotation-function \
--rotation-rules '{"ScheduleExpression": "cron(0 12 1 * ? *)"}' \
--region us-east-1

This rotates on the 1st of every month at 12:00 PM UTC.

List secrets without rotation enabled

aws secretsmanager list-secrets \
--region us-east-1 \
--query 'SecretList[?RotationEnabled==`false`].{Name:Name,ARN:ARN}' \
--output table
CloudFormation (optional)

This template creates a secret with automatic rotation using a custom Lambda function:

AWSTemplateFormatVersion: '2010-09-09'
Description: 'AWS Secrets Manager secret with automatic rotation enabled'

Parameters:
SecretName:
Type: String
Description: Name for the secret
Default: my-rotated-secret

RotationScheduleDays:
Type: Number
Description: Number of days between automatic rotations
Default: 30
MinValue: 1
MaxValue: 365

Resources:
# IAM Role for the Lambda rotation function
RotationLambdaRole:
Type: AWS::IAM::Role
Properties:
RoleName: !Sub '${SecretName}-rotation-role'
AssumeRolePolicyDocument:
Version: '2012-10-17'
Statement:
- Effect: Allow
Principal:
Service: lambda.amazonaws.com
Action: sts:AssumeRole
ManagedPolicyArns:
- arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole
Policies:
- PolicyName: SecretsManagerRotationPolicy
PolicyDocument:
Version: '2012-10-17'
Statement:
- Effect: Allow
Action:
- secretsmanager:DescribeSecret
- secretsmanager:GetSecretValue
- secretsmanager:PutSecretValue
- secretsmanager:UpdateSecretVersionStage
Resource: !Sub 'arn:aws:secretsmanager:${AWS::Region}:${AWS::AccountId}:secret:${SecretName}*'
- Effect: Allow
Action:
- secretsmanager:GetRandomPassword
Resource: '*'

# Lambda function for rotation
RotationLambdaFunction:
Type: AWS::Lambda::Function
Properties:
FunctionName: !Sub '${SecretName}-rotation-function'
Runtime: python3.11
Handler: index.lambda_handler
Role: !GetAtt RotationLambdaRole.Arn
Timeout: 30
Code:
ZipFile: |
import boto3
import json
import string
import secrets

def lambda_handler(event, context):
arn = event['SecretId']
token = event['ClientRequestToken']
step = event['Step']

service_client = boto3.client('secretsmanager')

if step == "createSecret":
# Generate a new secret value
chars = string.ascii_letters + string.digits + "!@#$%^&*"
password = ''.join(secrets.choice(chars) for _ in range(32))
service_client.put_secret_value(
SecretId=arn,
ClientRequestToken=token,
SecretString=json.dumps({"password": password}),
VersionStages=['AWSPENDING']
)
elif step == "setSecret":
# Apply the pending secret (e.g., update database credentials)
pass
elif step == "testSecret":
# Test that the pending secret works
pass
elif step == "finishSecret":
# Finalize the rotation
metadata = service_client.describe_secret(SecretId=arn)
current_version = None
for version, stages in metadata['VersionIdsToStages'].items():
if 'AWSCURRENT' in stages:
current_version = version
break

service_client.update_secret_version_stage(
SecretId=arn,
VersionStage='AWSCURRENT',
MoveToVersionId=token,
RemoveFromVersionId=current_version
)

return {"statusCode": 200}

# Permission for Secrets Manager to invoke the Lambda function
LambdaInvokePermission:
Type: AWS::Lambda::Permission
Properties:
FunctionName: !Ref RotationLambdaFunction
Action: lambda:InvokeFunction
Principal: secretsmanager.amazonaws.com
SourceArn: !Ref MySecret

# The secret with automatic rotation enabled
MySecret:
Type: AWS::SecretsManager::Secret
Properties:
Name: !Ref SecretName
Description: Secret with automatic rotation enabled
GenerateSecretString:
SecretStringTemplate: '{"username": "admin"}'
GenerateStringKey: password
PasswordLength: 32
ExcludeCharacters: '"@/\'

# Rotation schedule for the secret
MySecretRotationSchedule:
Type: AWS::SecretsManager::RotationSchedule
DependsOn: LambdaInvokePermission
Properties:
SecretId: !Ref MySecret
RotationLambdaARN: !GetAtt RotationLambdaFunction.Arn
RotationRules:
AutomaticallyAfterDays: !Ref RotationScheduleDays

Outputs:
SecretArn:
Description: ARN of the created secret
Value: !Ref MySecret

RotationLambdaArn:
Description: ARN of the rotation Lambda function
Value: !GetAtt RotationLambdaFunction.Arn

Deploy with:

aws cloudformation deploy \
--template-file template.yaml \
--stack-name secrets-rotation-stack \
--parameter-overrides SecretName=my-app-secret RotationScheduleDays=30 \
--capabilities CAPABILITY_NAMED_IAM \
--region us-east-1
Terraform (optional)
terraform {
required_version = ">= 1.0"
required_providers {
aws = {
source = "hashicorp/aws"
version = ">= 5.0"
}
}
}

provider "aws" {
region = "us-east-1"
}

variable "secret_name" {
description = "Name for the secret"
type = string
default = "my-rotated-secret"
}

variable "rotation_days" {
description = "Number of days between automatic rotations"
type = number
default = 30
}

# IAM Role for the Lambda rotation function
resource "aws_iam_role" "rotation_lambda_role" {
name = "${var.secret_name}-rotation-role"

assume_role_policy = jsonencode({
Version = "2012-10-17"
Statement = [
{
Action = "sts:AssumeRole"
Effect = "Allow"
Principal = {
Service = "lambda.amazonaws.com"
}
}
]
})
}

resource "aws_iam_role_policy_attachment" "lambda_basic_execution" {
role = aws_iam_role.rotation_lambda_role.name
policy_arn = "arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole"
}

resource "aws_iam_role_policy" "secrets_manager_rotation" {
name = "SecretsManagerRotationPolicy"
role = aws_iam_role.rotation_lambda_role.id

policy = jsonencode({
Version = "2012-10-17"
Statement = [
{
Effect = "Allow"
Action = [
"secretsmanager:DescribeSecret",
"secretsmanager:GetSecretValue",
"secretsmanager:PutSecretValue",
"secretsmanager:UpdateSecretVersionStage"
]
Resource = aws_secretsmanager_secret.main.arn
},
{
Effect = "Allow"
Action = ["secretsmanager:GetRandomPassword"]
Resource = "*"
}
]
})
}

# Lambda function for rotation
resource "aws_lambda_function" "rotation" {
function_name = "${var.secret_name}-rotation-function"
role = aws_iam_role.rotation_lambda_role.arn
handler = "index.lambda_handler"
runtime = "python3.11"
timeout = 30

filename = data.archive_file.lambda_zip.output_path
source_code_hash = data.archive_file.lambda_zip.output_base64sha256
}

data "archive_file" "lambda_zip" {
type = "zip"
output_path = "${path.module}/lambda_function.zip"

source {
content = <<-PYTHON
import boto3
import json
import string
import secrets

def lambda_handler(event, context):
arn = event['SecretId']
token = event['ClientRequestToken']
step = event['Step']

service_client = boto3.client('secretsmanager')

if step == "createSecret":
chars = string.ascii_letters + string.digits + "!@#$%^&*"
password = ''.join(secrets.choice(chars) for _ in range(32))
service_client.put_secret_value(
SecretId=arn,
ClientRequestToken=token,
SecretString=json.dumps({"password": password}),
VersionStages=['AWSPENDING']
)
elif step == "setSecret":
pass
elif step == "testSecret":
pass
elif step == "finishSecret":
metadata = service_client.describe_secret(SecretId=arn)
current_version = None
for version, stages in metadata['VersionIdsToStages'].items():
if 'AWSCURRENT' in stages:
current_version = version
break

service_client.update_secret_version_stage(
SecretId=arn,
VersionStage='AWSCURRENT',
MoveToVersionId=token,
RemoveFromVersionId=current_version
)

return {"statusCode": 200}
PYTHON
filename = "index.py"
}
}

# Permission for Secrets Manager to invoke the Lambda function
resource "aws_lambda_permission" "secretsmanager" {
statement_id = "AllowSecretsManagerInvoke"
action = "lambda:InvokeFunction"
function_name = aws_lambda_function.rotation.function_name
principal = "secretsmanager.amazonaws.com"
source_arn = aws_secretsmanager_secret.main.arn
}

# The secret
resource "aws_secretsmanager_secret" "main" {
name = var.secret_name
description = "Secret with automatic rotation enabled"
}

# Initial secret value
resource "aws_secretsmanager_secret_version" "initial" {
secret_id = aws_secretsmanager_secret.main.id
secret_string = jsonencode({
username = "admin"
password = "initial-password-to-be-rotated"
})

lifecycle {
ignore_changes = [secret_string]
}
}

# Rotation schedule
resource "aws_secretsmanager_secret_rotation" "main" {
secret_id = aws_secretsmanager_secret.main.id
rotation_lambda_arn = aws_lambda_function.rotation.arn

rotation_rules {
automatically_after_days = var.rotation_days
}

depends_on = [aws_lambda_permission.secretsmanager]
}

output "secret_arn" {
description = "ARN of the created secret"
value = aws_secretsmanager_secret.main.arn
}

output "rotation_lambda_arn" {
description = "ARN of the rotation Lambda function"
value = aws_lambda_function.rotation.arn
}

Deploy with:

terraform init
terraform plan
terraform apply

Verification

After enabling rotation, verify it is configured correctly:

  1. In the AWS Console, go to Secrets Manager
  2. Select your secret
  3. Look for Rotation configuration - it should show:
    • Automatic rotation: Enabled
    • Rotation schedule: Your configured interval
    • Next rotation date: A future date
CLI verification commands
# Check rotation status for a specific secret
aws secretsmanager describe-secret \
--secret-id my-secret \
--region us-east-1 \
--query '{RotationEnabled: RotationEnabled, RotationLambdaARN: RotationLambdaARN, RotationRules: RotationRules}'

# Expected output for a properly configured secret:
# {
# "RotationEnabled": true,
# "RotationLambdaARN": "arn:aws:lambda:us-east-1:123456789012:function:...",
# "RotationRules": {
# "AutomaticallyAfterDays": 30
# }
# }

# Trigger an immediate rotation to test
aws secretsmanager rotate-secret \
--secret-id my-secret \
--region us-east-1

# Re-run Prowler to confirm the check passes
prowler aws --check secretsmanager_automatic_rotation_enabled

Additional Resources

Notes

  • Managed rotation: For RDS, Redshift, and DocumentDB, use AWS managed rotation. It automatically creates and manages the Lambda function.

  • Custom rotation: For other secrets (API keys, custom credentials), you must create a Lambda function that implements the four rotation steps: createSecret, setSecret, testSecret, and finishSecret.

  • Application updates: Ensure your applications retrieve secrets at runtime (not at deployment) so they automatically use the latest rotated values.

  • VPC considerations: If your rotation Lambda needs to access resources in a VPC (like a database), configure VPC settings on the Lambda function and ensure proper security group rules.

  • Rotation windows: Use cron expressions instead of AutomaticallyAfterDays if you need precise control over when rotation occurs (e.g., during maintenance windows).

  • Cost considerations: Each rotation invokes your Lambda function. For secrets rotated frequently, factor in Lambda execution costs.