---
cuppy_hub: aws
cuppy_path: guides/secrets-manager/rotate-secrets.md
cuppy_section: docs
cuppy_doc: NGHW-0XEA
cuppy_doc_uuid: 3d7f3c8e-75ae-4044-9921-5767a06ea0e1
cuppy_permalink: https://cuppy.io/aws/d/NGHW-0XEA
cuppy_version: 4
cuppy_updated: 2026-10-23T16:16:52.575Z
cuppy_canonical: https://cuppy.io/aws/docs/guides/secrets-manager/rotate-secrets
cuppy_mcp: https://cuppy.io/mcp
cuppy_body_sha256: a4ee942d878de0dc5c8aa8a28c73e7f4a3a942a31ee93d90b8e6af978ef81f7d
# cuppy_body_sha256 is over the document body ALONE, excluding both generated blocks.
---


# Rotate database secrets with Secrets Manager

**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-id` to 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-instances` shows `MasterUserSecret` with `SecretArn`, `SecretStatus` (`creating`, `active`, `rotating` or `impaired`) and `KmsKeyId`. The example secret ARNs contain `rds!`.
- Rotation is every 7 days by default. The schedule can be modified, and `--rotate-master-user-password` together with `--apply-immediately` rotates 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(…)` or `cron(…)`, UTC; as often as **every four hours**), *or* `AutomaticallyAfterDays`, not both. Hour-based schedules need `ScheduleExpression`.
- `Duration` sets the window, for example `3h`, 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:CreateRole` and `iam:AttachRolePolicy`, because it creates the function's role.

```ts
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

1. **createSecret:** stores a new version labelled **`AWSPENDING`**. The version ID is the request token, so retries reuse it.
2. **setSecret:** changes the password in the database to the pending value. It first checks that `AWSCURRENT` works and refers to the same user and host.
3. **testSecret:** logs in with the pending value.
4. **finishSecret:** moves **`AWSCURRENT`** to the new version. The old one becomes **`AWSPREVIOUS`**.

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 `GetSecretValue` returns `AWSCURRENT`.
- 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 |


---
<!-- cuppy -->
You are reading one file. The same content is free over MCP at https://cuppy.io/mcp with no signup: ranked search across all 131 documents in this hub, the merged view when you work in a layer, known workarounds matched to the error you hit, and depth control so you spend tokens on what you need. Start with the `_hello` tool. How to connect: https://cuppy.io/aws/ Something wrong in this document? Report it with the `session feedback` tool on the same endpoint.
<!-- /cuppy -->
