AWS

Configure AWS Secrets Manager: Rotation, IAM, and ECS

Hardcoded database passwords in a .env file committed to Git is still how a surprising number of teams ship software in 2026. It worked for the first release, nobody rotated anything, and now the credential lives in five repos, two CI systems, and a Slack thread from 2023. AWS Secrets Manager is the native fix. It stores secrets encrypted with KMS, rotates them on a schedule, audits every read in CloudTrail, and hands the value to your workload at runtime over an IAM-authenticated API call.

Original content from computingforgeeks.com - post 165606

This guide walks through the full workflow: creating secrets from the CLI, retrieving them safely in scripts, versioning and rollback, identity-based and resource-based IAM policies, customer-managed KMS keys, cross-region replication, ECS task injection, automatic rotation for RDS, auditing who read a secret with CloudTrail, real cost math against Parameter Store, and a tested Terraform module you can drop into a real project. Every command and output below came from a live eu-west-1 account. If you also run workloads on EKS, pair this with our IRSA guide because Pod Identity and IRSA are how most Kubernetes workloads end up calling Secrets Manager.

Verified April 2026 with AWS CLI v2.22, Terraform 1.5, and Secrets Manager in eu-west-1

Secrets Manager vs Parameter Store: decide before you build

Every Secrets Manager article that skips this section is dodging the first question every engineer asks. Both services store sensitive data. Only one of them charges you, and only one of them rotates credentials for you. Pick the wrong service at day zero and you will either rewrite your bootstrap code in six months or pay $4,000 a year for config you could have stored free.

FeatureSecrets ManagerParameter Store (Standard)Parameter Store (Advanced)
Storage cost$0.40 per secret per monthFree$0.05 per parameter per month
API cost$0.05 per 10,000 callsFree up to account limits$0.05 per 10,000 higher-throughput calls
Max value size64 KB4 KB8 KB
Max items per account per region500,00010,000100,000
Default throughput10,000 TPS40 TPS (scalable)Up to 10,000 TPS
EncryptionKMS, mandatoryOptional SecureStringOptional SecureString
Automatic rotationYes, built-inNot supportedNot supported
Managed RDS rotationYesNoNo
Cross-region replicationNativeManualManual
Cross-account sharingResource policiesNot supportedVia AWS RAM
Generate random passwordsYes (get-random-password)NoNo
Reference from Parameter StoreN/AYes, via /aws/reference/secretsmanager/Same

The rule of thumb is simple. Database credentials, OAuth tokens, third-party API keys that must rotate on a schedule, cross-account secrets, and anything under PCI or HIPAA audit go in Secrets Manager. Feature flags, app config, non-rotating bootstrap values, and anything read thousands of times per second at container start belong in Parameter Store SecureString. If a secret costs more than about $5 a month in API calls on Secrets Manager because your app re-reads it on every request, you are holding it wrong. Cache the value in process memory with a short TTL, or move it to Parameter Store.

How Secrets Manager actually works

Secrets Manager is a regional service. A secret created in eu-west-1 does not exist in us-east-1 until you explicitly replicate it. Every secret is encrypted at rest with a KMS key. By default that key is the AWS-managed aws/secretsmanager key (free). For production workloads with compliance requirements, bring your own customer-managed KMS key so the key policy becomes a second audit layer on top of the resource policy.

Every secret has a current value plus a short history of previous values, tagged with staging labels. The three built-in labels are AWSCURRENT (the active value your applications read), AWSPREVIOUS (the last value, kept for rollback), and AWSPENDING (a new value being tested during rotation). You can also create custom staging labels if you need more than three pointers. Applications should always read the default label, which is AWSCURRENT, and only reach for AWSPREVIOUS explicitly during a rollback scenario.

Rotation is a four-step Lambda lifecycle: createSecret generates the new credential and stores it as AWSPENDING, setSecret updates the target system, testSecret verifies the new credential works, and finishSecret promotes AWSPENDING to AWSCURRENT while demoting the old value to AWSPREVIOUS. For RDS, Aurora, DocumentDB, Redshift admin passwords, and ECS Service Connect private CA certificates, AWS runs managed rotation on your behalf and you never touch Lambda at all. For anything else, AWS ships a rotation template and you customize setSecret and testSecret for your target system. The minimum rotation interval is every 4 hours, and managed rotation typically finishes in under a minute.

One common trap: ElastiCache and MemoryDB are not managed rotation targets. They ship with rotation Lambda templates only, which is different from “managed” in AWS documentation terms. If you read another tutorial claiming managed rotation for Redis, that tutorial is wrong.

Prerequisites

  • An AWS account with an IAM admin user or role. Test account spend for this walkthrough stays under $1 if you destroy everything when done.
  • AWS CLI v2.22 or newer, configured with credentials for the target region. Run aws --version to verify.
  • Terraform 1.5 or newer for the IaC section.
  • A workstation with Python 3 and jq for JSON parsing in shell scripts.
  • Tested on: AWS CLI 2.22.0, Terraform 1.5.7, Rocky Linux 10.1 workstation, eu-west-1 region, April 2026.

Set your default region so you do not have to repeat it on every command.

export AWS_DEFAULT_REGION=eu-west-1
aws sts get-caller-identity

The identity check confirms which account you are about to modify. Never run Secrets Manager commands against an account you have not verified, especially in shared tooling environments.

Create your first secret

Secrets Manager accepts two flavors of secret payload: a plain string or a JSON document. Use JSON whenever you have more than one field (username, password, host, port, database name) because it keeps related credentials in one atomic version. Start with a plain-string API key so you see the basic shape of the API.

aws secretsmanager create-secret \
  --name "computingforgeeks/demo/api-key" \
  --description "Plain string API key for the computingforgeeks Secrets Manager demo" \
  --secret-string "sk-example-1234567890abcdefghijklmnop" \
  --tags Key=Environment,Value=demo Key=Application,Value=cfg-blog \
  --region eu-west-1

The API returns the full ARN, the friendly name, and a version ID for the value you just stored.

{
    "ARN": "arn:aws:secretsmanager:eu-west-1:123456789012:secret:computingforgeeks/demo/api-key-UFlApf",
    "Name": "computingforgeeks/demo/api-key",
    "VersionId": "d8aacfec-0da8-4d64-b25e-771e5eb64206"
}

The six-character suffix (UFlApf in this case) is appended automatically by Secrets Manager. It exists to prevent accidental IAM wildcards from matching a recreated secret with the same name. Always reference your secrets by the friendly name in code, never the full ARN, otherwise you hardcode this random suffix and any recreate-delete cycle will break your app.

Now create a realistic database credential secret with a structured JSON payload. This is the shape you will use 90% of the time in production.

aws secretsmanager create-secret \
  --name "computingforgeeks/demo/db-credentials" \
  --description "PostgreSQL credentials for the demo application" \
  --secret-string '{"username":"appuser","password":"S3cur3-D3mo-Pa55w0rd!","host":"db.prod.example.com","port":5432,"dbname":"appdb","engine":"postgres"}' \
  --tags Key=Environment,Value=production Key=Application,Value=api Key=Component,Value=database \
  --region eu-west-1

Tagging every secret with Environment, Application, and Component (or similar) is not optional. Tags drive cost allocation, IAM conditions, automated cleanup, and the answer to the question “which team owns this credential?” that will come up during the next audit. Set a convention and enforce it with SCPs or Terraform module defaults.

Inspect the secret metadata without revealing the value. The describe-secret call is safe to run from any IAM principal with secretsmanager:DescribeSecret and is the right way to build a secret inventory or validate tags.

aws secretsmanager describe-secret \
  --secret-id "computingforgeeks/demo/db-credentials" \
  --region eu-west-1

Describe never returns the secret value, only metadata. That is exactly what you want for monitoring scripts.

{
    "ARN": "arn:aws:secretsmanager:eu-west-1:123456789012:secret:computingforgeeks/demo/db-credentials-9NWLWY",
    "Name": "computingforgeeks/demo/db-credentials",
    "Description": "PostgreSQL credentials for the demo application",
    "LastChangedDate": 1775844659.55,
    "Tags": [
        {"Key": "Component", "Value": "database"},
        {"Key": "Environment", "Value": "production"},
        {"Key": "Application", "Value": "api"}
    ],
    "VersionIdsToStages": {
        "9213e49c-4499-4818-aec3-5101753ff66e": ["AWSCURRENT"]
    },
    "CreatedDate": 1775844659.521
}

Notice the VersionIdsToStages block. Right now the only version ID is labeled AWSCURRENT. After the first rotation that block will also include an AWSPREVIOUS entry. This map is how Secrets Manager tracks the rollback history and how your applications pin to a specific version if they ever need to.

Retrieve a secret

The GetSecretValue API is what applications hit at startup. It returns the version stages, the string payload, and the created date.

aws secretsmanager get-secret-value \
  --secret-id "computingforgeeks/demo/db-credentials" \
  --region eu-west-1

The default output wraps your value in a metadata envelope.

{
    "ARN": "arn:aws:secretsmanager:eu-west-1:123456789012:secret:computingforgeeks/demo/db-credentials-9NWLWY",
    "Name": "computingforgeeks/demo/db-credentials",
    "VersionId": "9213e49c-4499-4818-aec3-5101753ff66e",
    "SecretString": "{\"username\":\"appuser\",\"password\":\"S3cur3-D3mo-Pa55w0rd!\",\"host\":\"db.prod.example.com\",\"port\":5432,\"dbname\":\"appdb\",\"engine\":\"postgres\"}",
    "VersionStages": ["AWSCURRENT"],
    "CreatedDate": 1775844659.545
}

For shell scripts you only want the SecretString field. Use the --query and --output text combo so you get the raw JSON back without the envelope.

aws secretsmanager get-secret-value \
  --secret-id "computingforgeeks/demo/db-credentials" \
  --region eu-west-1 \
  --query SecretString \
  --output text

Now the output is exactly the payload you stored, ready for downstream parsing.

{"username":"appuser","password":"S3cur3-D3mo-Pa55w0rd!","host":"db.prod.example.com","port":5432,"dbname":"appdb","engine":"postgres"}

Most of the time you want a single key, not the whole JSON blob. Pipe through Python’s built-in json module for a dependency-free extractor.

aws secretsmanager get-secret-value \
  --secret-id "computingforgeeks/demo/db-credentials" \
  --region eu-west-1 \
  --query SecretString \
  --output text | python3 -c "import sys,json; print(json.loads(sys.stdin.read())['password'])"

That one-liner is the most common pattern in ops scripts. It works without jq, which matters on minimal container images and fresh EC2 instances. If you prefer jq, the equivalent is jq -r .password and is one fewer character.

Press ESC to close