Rotate database secrets with Secrets Manager
Turn on rotation for RDS and Aurora credentials — RDS-managed master passwords (ManageMasterUserPassword, 7-day default) or a Lambda rotation function (single or alternating users) — set the schedule and window, understand the AWSCURRENT/AWSPENDING/AWSPREVIOUS labels and the four rotation steps, make clients survive a rotation, and fix the common failures by their log text.
markdownguidev4NGHW-0XEAstage: operatetrust: officialservice: secretsmanagerservice: rdsinterface: cliinterface: sdkSecrets Manager rotationrotate RDS passwordManageMasterUserPasswordmanage-master-user-passwordMasterUserSecretrotate-master-user-passwordrotation LambdacreateSecret setSecret testSecret finishSecretAWSCURRENTAWSPENDINGAWSPREVIOUSsingle user rotationalternating usersRDS Proxy alternating usersrotation schedule ScheduleExpressionrotation window Durationrotation failedAccess to KMS is not allowedsetSecret Unable to log into databasePending secret version was not created by LambdaRotationTokenFound credentials in environment variablesSecrets Manager VPC endpoint
For an RDS or Aurora master password, let RDS manage it (--manage-master-user-password): no Lambda function, and rotation every 7 days by default. For application users, or anything RDS doesn't manage, use a Lambda rotation function. Either way, clients must re-read the secret when a login fails, rather than caching it forever.
Option A: RDS-managed master password
aws rds modify-db-instance --db-instance-identifier mydb \
--manage-master-user-password \
--master-user-secret-kms-key-id alias/my-key \
--apply-immediately
- Omit
--master-user-secret-kms-key-idto use Secrets Manager's default key. A key in another account must be named by ARN. The key can't be changed later. describe-db-instancesshowsMasterUserSecretwithSecretArn,SecretStatus(creating,active,rotatingorimpaired) andKmsKeyId. The example secret ARNs containrds!.- Rotation is every 7 days by default. The schedule can be modified, and
--rotate-master-user-passwordtogether with--apply-immediatelyrotates now. Generated passwords are 28 characters. - Not supported with:
- creating read replicas from such a source (except RDS for SQL Server and Db2);
- Blue/Green Deployments;
- RDS Custom;
- Oracle Data Guard switchover.
Plan around these before you switch.
Option B: a Lambda rotation function
| Strategy | How it works | Use when |
|---|---|---|
| Single user | Changes the one user's password in place | Simple apps; how existing connections behave during the switch isn't stated on the fetched pages (UNVERIFIED) |
| Alternating users | Keeps two users and clones one with a superuser secret | Two users take turns, so one stays untouched while the other changes (the hub's reading; uninterrupted logins aren't stated on the fetched pages). RDS Proxy doesn't support this strategy |
Schedule (SDK RotationRules):
ScheduleExpression(rate(…)orcron(…), UTC; as often as every four hours), orAutomaticallyAfterDays, not both. Hour-based schedules needScheduleExpression.Durationsets the window, for example3h, and must not reach into the next window. Without it, hourly schedules close after one hour and daily ones at the end of the day. The window starts at midnight unless you set a start time.- Failed rotations are retried several times.
- Turning rotation on in the console needs
iam:CreateRoleandiam:AttachRolePolicy, because it creates the function's role.
import { SecretsManagerClient, RotateSecretCommand } from "@aws-sdk/client-secrets-manager";
const sm = new SecretsManagerClient({});
await sm.send(new RotateSecretCommand({
SecretId: "prod/app/db",
RotationLambdaARN: "arn:aws:lambda:<region>:<account-id>:function:<rotation-fn>",
RotationRules: { ScheduleExpression: "rate(30 days)", Duration: "3h" },
}));
What a rotation does
- createSecret: stores a new version labelled
AWSPENDING. The version ID is the request token, so retries reuse it. - setSecret: changes the password in the database to the pending value. It first checks that
AWSCURRENTworks and refers to the same user and host. - testSecret: logs in with the pending value.
- finishSecret: moves
AWSCURRENTto the new version. The old one becomesAWSPREVIOUS.
If AWSPENDING sits on a version other than AWSCURRENT, later rotations assume a rotation is still running and fail. A failed run can leave AWSPENDING on an empty version. The fix is to clear AWSPENDING and retry.
Network
The function runs in the database's VPC and security group, and it must also reach the Secrets Manager endpoint: through an interface VPC endpoint (preferred) or NAT. For alternating users with an RDS-managed superuser secret, it also needs the RDS API endpoint. The endpoint's security group must admit the VPC's CIDR on 443, and its endpoint policy must allow the function (patterns/iam-to-vpc-endpoints.md).
Make clients survive rotation
- Read the secret by name. The default
GetSecretValuereturnsAWSCURRENT. - Cache it, but on an authentication failure re-read it once and reconnect before failing (
guides/rds/connect-securely.md).
When rotation fails (CloudWatch logs of the rotation function)
| Log or error text | Cause → fix |
|---|---|
Nothing after Found credentials in environment variables | The function can't reach Secrets Manager (long duration, then timeout). Add a Secrets Manager interface endpoint or NAT |
Nothing after createSecret | HTTPS blocked by a NACL, the endpoint security group or the endpoint policy; Lambda timeout too short; or no RDS API endpoint (alternating users with an RDS-managed superuser secret) |
An error occurred (AccessDeniedException) when calling the GetSecretValue operation: Access to KMS is not allowed | The function's role can't use the secret's customer managed key. The key policy may restrict by encryption context (reference/kms/key-policies-and-errors.md) |
Key is missing from secret JSON | The secret's JSON lacks fields the function expects (username, password, host and so on). Match the documented structure |
setSecret: Unable to log into database | Slow (over about 5000 ms): network, so put the function in the DB's VPC and open its SG to the DB SG. Fast: bad credentials, so test AWSCURRENT and AWSPREVIOUS. Aurora PostgreSQL 13+ with scram-sha-256, or TLS-required databases, with a function created before late December 2021: recreate the function |
Unable to import module 'lambda_function' | An old Python 3.7 function was moved to a newer runtime. Recreate it on Python 3.12 (CDK users: aws-cdk-lib v2.258.0 or later) |
Pending secret version <id> for Secret <arn> was not created by Lambda <arn>. Remove the AWSPENDING staging label and restart rotation | Cross-account or assumed-role rotation didn't pass RotationToken. Read event['RotationToken'] and pass it to put_secret_value |
Error when executing lambda <arn> during <step> step (looping) | Concurrency too low. Set reserved concurrency to at least 10 |