A platform team standardizing on AWS in 2026 is sorting through 300 secrets across staging and production — database passwords, third-party API keys, JWT signing keys, internal service-to-service tokens, OAuth client secrets, webhook signing secrets. The current state is a mix of .env files, Kubernetes secrets, and a half-broken HashiCorp Vault that someone set up two years ago and nobody maintains. AWS has two services that solve the problem — Secrets Manager and SSM Parameter Store — and the team keeps having the same argument: Parameter Store is free, so why pay for Secrets Manager. The answer is that the two services are not interchangeable. The decision is per-secret, and the wrong call is wasting money on Secrets Manager for things that do not rotate or losing rotation on things that should.
What’s happening
Secrets Manager and Parameter Store both let an application fetch a secret at runtime via an IAM-authenticated API call, both encrypt at rest with KMS, both log access via CloudTrail. The differences that actually matter:
| Dimension | Secrets Manager | Parameter Store (SecureString) |
|---|---|---|
| Cost model | Per secret per month + per API call | Free for standard tier; advanced tier (>4KB or per-second high-throughput) charged |
| Native rotation | Yes — Lambda-based for RDS, Aurora, Redshift, DocumentDB, generic | No native rotation |
| Hierarchy | Flat naming with / allowed (no real path semantics) | True hierarchical paths with GetParametersByPath |
| Cross-account share | Yes — resource policies on the secret | Yes for advanced tier; via parameter ARN reference |
| Max value size | 64 KB | 4 KB (standard) / 8 KB (advanced) |
| Replication | Cross-region replication built in | No native replication |
| Versioning | Yes — version IDs + staging labels (AWSCURRENT/AWSPENDING) | Yes — sequential version numbers |
| Caching SDK | AWS provides a client-side cache library | No first-party cache — implement yours |
The argument-ending rule: if the secret needs to rotate (database credentials, third-party API keys with rotation policies, anything regulated), Secrets Manager. If it does not (feature flags, internal configuration, OAuth client secrets that change once a year on manual rotation), Parameter Store SecureString. The per-secret monthly cost on Secrets Manager is modest, but it accumulates fast across hundreds of values that never needed rotation. Conversely, building Lambda rotation on top of Parameter Store is a project; Secrets Manager has it as a checkbox for RDS.
A subtle one: cross-region replication. If the workload is multi-region active-active, replicating Secrets Manager secrets to the second region is built in. With Parameter Store, the secret has to be duplicated by your own tooling (Terraform, CDK, a CI step) and the two copies can drift. For multi-region workloads, that nudges database credentials onto Secrets Manager regardless of rotation cadence.
How to verify
The classification pass — for the 300-secret inventory:
# everything in Parameter Store under /prod
aws ssm get-parameters-by-path \
--path /prod/ \
--recursive \
--with-decryption \
--query 'Parameters[].[Name,Type,LastModifiedDate]' \
--output table
# every Secrets Manager secret, with rotation flag
aws secretsmanager list-secrets \
--query 'SecretList[].[Name,RotationEnabled,LastRotatedDate,LastChangedDate]' \
--output table
Two patterns to spot:
- Parameter Store entries where
LastModifiedDateis more than 365 days ago and the parameter is a database password. That secret has not rotated in a year. It belongs in Secrets Manager with rotation enabled. - Secrets Manager secrets where
RotationEnabledisfalseandLastChangedDateis years old. You are paying the per-secret monthly charge for a value that could have lived in Parameter Store SecureString for free.
For the IAM policy split, list every role that touches a secret:
aws cloudtrail lookup-events \
--lookup-attributes AttributeKey=EventName,AttributeValue=GetSecretValue \
--start-time $(date -u -v-30d +%Y-%m-%dT%H:%M:%S) \
--query 'Events[].Username' \
--output text | sort -u
If a role is reading secrets that belong to a different team (the web app role reading the analytics database password), the IAM policy is too broad. Each role should only read the secrets its workload owns.
The fix
The classification rule we apply across every secrets-cleanup engagement:
Use Secrets Manager when:
- The secret rotates (or should). RDS, Aurora, Redshift, DocumentDB passwords are the canonical case — native rotation Lambda, no code to write.
- The secret is replicated cross-region. Multi-region failover with a single managed copy of the credential.
- The secret is shared across accounts via resource policies. Centralized secret in the security account, IAM grant to specific roles in workload accounts.
- Size matters — values above 4 KB (e.g. multi-line private keys, large certificate chains).
Use Parameter Store when:
- The value does not rotate — internal configuration, OAuth client IDs (the ID, not the secret), feature flags, public URLs.
- Hierarchical organization matters. Parameter Store has real path semantics —
aws ssm get-parameters-by-path --path /prod/app/ --recursivereturns the whole tree in one call. Secrets Manager does not. - The value is referenced from CloudFormation/CDK at deploy time as a SSM parameter — first-class integration with CFN.
The “rotation that breaks the app” gotcha is the one that bites teams new to Secrets Manager. When rotation runs, Secrets Manager creates a new password version under the AWSPENDING staging label, calls the rotation Lambda to set the new password on the database, and only then promotes AWSPENDING to AWSCURRENT. An application that fetches the secret once at startup and caches it forever does not see the rotated password until it restarts. The connection pool gets Authentication failed errors hours after rotation, and the on-call is confused because the secret in the console looks fine — AWSCURRENT is the new password, but the app is still using the value it cached before rotation.
The fix is the cache TTL. The AWS-provided cache libraries (aws-secretsmanager-caching-java, aws-secretsmanager-caching-python) refresh on configurable TTL — 15 minutes is a reasonable default. The application gets the new password within 15 minutes of rotation without restarting. Without a cache, every database connection costs a GetSecretValue API call. Without a TTL on the cache, rotation breaks the app.
The IAM policy split is the second piece. The principle of least privilege gets violated by default if a role has secretsmanager:GetSecretValue on Resource: "*". The right policy reads:
{
"Version": "2012-10-17",
"Statement": [{
"Effect": "Allow",
"Action": ["secretsmanager:GetSecretValue"],
"Resource": [
"arn:aws:secretsmanager:us-east-1:123456789012:secret:prod/app-x/db-*",
"arn:aws:secretsmanager:us-east-1:123456789012:secret:prod/app-x/api-keys-*"
]
}]
}
The trailing * on the resource ARN matches the suffix that Secrets Manager appends to every secret name. The path prefix (prod/app-x/) means the role can only read its own application’s secrets. Cross-team secrets require their own IAM grant.
For Parameter Store the same pattern applies, with ssm:GetParameter and ssm:GetParametersByPath. KMS decrypt permission is separate — if SecureString uses a customer-managed CMK, the role also needs kms:Decrypt on the key ARN, scoped via the kms:EncryptionContext:PARAMETER_ARN condition.
Three rules across every secrets-cleanup engagement:
Classify before you migrate. A spreadsheet with every secret, its current location, its rotation cadence, and its target service. The migration is mechanical once the matrix is filled.
Cache with a TTL, never cache forever. The rotation gotcha is the single biggest cause of post-migration incidents.
IAM policy resource ARN, not wildcard. A role’s permission to read secrets should list the exact path prefixes it owns. The blast radius of a compromised role drops dramatically when the IAM policy is specific.
The migration on a 300-secret inventory is a focused week — most of it is the classification spreadsheet, not the API calls. Done right, the team has rotation on the credentials that need it and zero recurring cost on the ones that do not.
Stack Harbor runs IAM cleanup, CloudWatch baseline, AWS Backup, and the cost-control rhythm as part of Managed AWS and ongoing Managed Cloud Ops. If your secret hygiene is a folder of .env files and a Vault nobody maintains, start with an infrastructure consult.