Choosing Between Parameter Store and Secrets Manager Across a Mixed Inventory

October 11, 2028 · 18 min read

Security Specialty · SCS-C03 · part of The Exam Room

The situation

A SaaS platform team operates roughly 150 secrets across about thirty microservices in a single AWS Organization:

  • ~20 database credentials, master and application users for several Amazon RDS Postgres instances, an Aurora MySQL cluster, and an Amazon Redshift warehouse.
  • ~50 third-party API keys, payment, analytics, observability, marketing, support. Some vendors publish rotation procedures, most don’t.
  • ~30 signing keys. HMAC secrets for webhook verification, JWT signing keys, internal service-to-service tokens. Manual annual rotation.
  • ~40 OAuth client secrets and app config, per-environment client IDs, encryption salts, feature-flag tokens.
  • ~10 stragglers, including the ones currently in s3://platform-config/dev/.env, which the security review unearthed last month.

Current storage is mixed: Parameter Store SecureString, Secrets Manager, and those committed env files. No naming convention crosses teams. Security wants one canonical home, automatic rotation for the databases, hierarchical organisation, cross-region replication for the production-critical secrets, and a reasonable cost envelope.

What actually matters

Before mapping services to the ask, worth naming the deeper properties of an answer.

The first thing to notice is that “secrets” is not one kind of thing. A database credential, a third-party API key, a JWT signing key, and an OAuth client secret are all strings the application needs at runtime, but they behave completely differently. Database credentials rotate on a schedule we control, we can push a new password to Postgres whenever we like, because Postgres is ours. API keys rotate on the vendor’s schedule, which might be never, might be annually, and the rotation ceremony involves logging into the vendor’s dashboard. Signing keys rotate by redeploying a different value. OAuth client secrets are generally rotated the same way, issuer-side change plus consumer redeploy. The storage layer’s job is different for each. For the first we want lifecycle automation built into the store; for the others we want cheap, hierarchical retrieval with an access-control story, because the store will not be the thing that rotates them.

Cost shape at 150 secrets matters more than at 15. A per-secret monthly charge that’s negligible for a handful turns into a visible line item across 150, especially once replicas double the production half. A free-storage store, by contrast, lets the no-rotation half of the inventory live cheaply forever. The question isn’t whether to be cheap, it’s whether to pay for rotation where we need it and not where we don’t.

Hierarchical naming is a property worth having even before the first IAM policy is written. A single convention that runs across the whole inventory – /team/service/environment/category/name, lets the access-control story be a prefix match rather than a hand-maintained allow list. Service roles get the slice of the tree their team owns; reviews become “list everything under this prefix, ask who owns each branch.” That convention has to span both stores, or the convenience evaporates.

Cross-region replication is a property that distinguishes “we have a disaster recovery plan” from “we have a disaster recovery document.” If the primary region goes dark and the secondary region can’t fetch the database password, the failover doesn’t happen. The storage layer has to propagate the value across regions at least as fast as the database itself can fail over, native replication is the property that matters, and not every secret store offers it. That shapes which secrets we put where.

Finally, there’s the rotation lifecycle itself. For a database credential the workflow is: generate a new password, apply it to the database, verify we can authenticate with it, atomically flip applications over, retain the old password briefly in case of rollback. That’s four distinct steps with rollback points between them, and it’s the kind of lifecycle we want the storage layer to own. The alternative, a home-grown rotation cron that mutates the database and overwrites a Parameter Store value, is a project of its own that nobody wants to maintain.

What we’ll filter on

  1. Automatic credential rotation with a documented lifecycle for the databases.
  2. Hierarchical organisation/team/service/env/... with IAM scoping by prefix.
  3. Cross-region replication for production-critical secrets.
  4. KMS encryption at rest, with a choice of AWS-managed (free) or customer-managed.
  5. Cost efficiency at 150 secrets. Linear and predictable.

The secrets storage landscape

Six places a secret could live.

1. Environment variables committed to source control or S3. The current state for ten of the secrets. Plain text on disk, version-controlled, read-accessible to anyone with read on the bucket. No rotation, no hierarchy beyond directory layout. The first action is aws s3 rm plus rotating every credential that ever touched the bucket.

2. Application-managed (CI-injected at deploy time). Secrets sit in the CI/CD platform’s store and get written into runtime config at deploy time. Better than committed env files, but the CI platform becomes the secret authority, AWS IAM doesn’t gate access, and rotation means redeploying. Useful for bootstrap credentials; not a long-term home.

3. AWS Systems Manager Parameter Store. Standard tier. Hierarchical paths separated by /, three parameter types (String, StringList, SecureString), and SecureString values encrypted with KMS (AWS-managed aws/ssm free or customer-managed). Standard limits: 4 KB maximum value, 10,000 parameters per Region per account, no parameter policies, default throughput of 40 TPS. Storage is free. No native cross-region replication, no native rotation. Wins on cost and hierarchy; loses on rotation and cross-region.

4. AWS Systems Manager Parameter Store. Advanced tier. $0.05 per parameter per month, value size to 8 KB, per-Region capacity to 100,000 parameters, plus parameter policies: Expiration, ExpirationNotification, NoChangeNotification. API calls $0.05 per 10,000. Still no native rotation or cross-region replication.

5. AWS Secrets Manager. The purpose-built secrets store. $0.40 per secret per month plus $0.05 per 10,000 API calls. KMS encryption by default. JSON-structured values up to 65,536 bytes. Two features Parameter Store doesn’t have: automatic rotation with managed Lambda templates for the major databases, and cross-region replication, a primary replicates to N others, with new values from rotation propagating automatically. Replica secrets bill as separate secrets.

6. HashiCorp Vault on AWS. The third-party reference for dynamic secrets, short-lived database credentials issued on demand, transit secrets engine, response wrapping, but you operate it. Excellent fit when dynamic credentials dominate and you have the platform capacity. Wrong fit for 150 mostly-static secrets and a six-person platform team.

Side by side

Option Rotation Hierarchy Cross-region Cost at 150 Ops overhead
Env vars in S3 ,
CI-injected , , ,
Parameter Store Standard
Parameter Store Advanced ,
Secrets Manager ,
Vault on AWS ,

No single row wins everything at 150 secrets. Rotation rules out Parameter Store for the database credentials. Cost rules out a single-store Secrets Manager answer ($0.40 × 150 = $60/month before API calls and replicas). Operational overhead rules out Vault. The cheapest correct answer is hybrid: Secrets Manager for credentials that need rotation, Parameter Store SecureString for the rest.

The hybrid split

Secrets Manager rotates + replicates ~20 database credentials /platform/payments-api/prod/db/master /platform/payments-api/prod/db/app /platform/analytics/prod/redshift/admin managed rotation: RDS, Aurora, Redshift Cross-region replication primary in eu-west-1 -> replica eu-west-2 rotation propagates automatically ~$20 / month $0.40 x (20 primary + 20 replicas) Parameter Store SecureString free storage; bring your own rotation ~50 third-party API keys /platform/payments-api/prod/stripe/api-key vendor-driven rotation ~30 signing keys rotate-by-redeploy, annual ~40 OAuth + app config per-env client IDs, encryption salts feature-flag tokens ~$0 / month free tier; GetParametersByPath at 40 TPS Application bootstraps from both stores IAM policy (by prefix) secretsmanager:GetSecretValue arn:...secret:/platform/payments-api/* ssm:GetParametersByPath arn:...parameter/platform/payments-api/* same tree, two ARN namespaces shared /team/service/env convention is what keeps the policy small connection refresh on rotation EventBridge: RotationSucceeded -> close + reopen DB connections or refetch on first auth-failed error RotationSucceeded event fires -> app refreshes its DB pool Rotation: 4-step Lambda contract 1. createSecret mint new password write AWSPENDING AWSCURRENT unchanged idempotent 2. setSecret ALTER USER ... WITH PASSWORD (NEW) DB accepts new password single-user: old breaks 3. testSecret fresh conn as app_user with NEW password SELECT 1, verify fail -> no flip 4. finishSecret atomic move: AWSCURRENT -> v2 AWSPREVIOUS -> v1 rollback handle
Two stores, one naming convention; Secrets Manager rotates the databases while Parameter Store holds the rest at no cost.

Allocating the inventory:

  • Secrets Manager (rotation, replication required): the ~20 database credentials. Roughly $8/month at $0.40 each, plus replicas in the DR Region (~$8 for the production half). Rotation Lambda invocations are pence per month per secret.
  • Parameter Store SecureString, Standard tier (free): the ~50 third-party API keys, the ~30 signing keys, the ~40 OAuth client secrets and app config. About 120 parameters at $0.00 each.
  • Parameter Store Advanced: any parameter exceeding 4 KB, or where a NoChangeNotification is worth $0.05/month.

Total steady-state storage cost: roughly $20 for Secrets Manager (primary plus production replicas) plus pennies in API calls. A single-store Secrets Manager answer would be five times that and climbing with replication.

The naming convention crosses both stores: Secrets Manager holds /platform/payments-api/prod/db/master; Parameter Store holds /platform/payments-api/prod/stripe/api-key. IAM policies scope by prefix on each ARN namespace (secretsmanager:secret:/platform/payments-api/*, ssm:parameter/platform/payments-api/*). Same logical namespace; different ARN prefix.

Secrets Manager rotation, in depth

The point of putting the database credentials in Secrets Manager is the rotation lifecycle. The mechanism is a rotation Lambda that Secrets Manager invokes on a schedule, with a four-step contract and a versioning model built around three staging labels.

Staging labels. Every version of a secret carries zero or more staging labels. Three are reserved: AWSCURRENT (returned by default from GetSecretValue), AWSPENDING (the version being rotated into AWSCURRENT, created by the Lambda, promoted at the end of a successful rotation), and AWSPREVIOUS (the prior AWSCURRENT, kept so rollback can move AWSCURRENT back to it). A given label sits on at most one version at a time, and moves are atomic, that’s what makes the rotation flip instantaneous.

Managed rotation versus Lambda rotation. For RDS, Aurora, Redshift, and DocumentDB, Secrets Manager ships managed rotation, no customer-supplied Lambda. For other resources, you supply a Lambda implementing the four-step contract.

The four-step contract. Secrets Manager invokes the Lambda four times per rotation, passing Step. Each invocation is idempotent:

  • createSecret: mint a new value (typically via GetRandomPassword), write it as a new version labelled AWSPENDING. Old version keeps AWSCURRENT. If called again with a version already labelled AWSPENDING, return without creating a duplicate.
  • setSecret: apply the new credential to the underlying resource. For RDS Postgres that’s ALTER USER ... WITH PASSWORD '...'.
  • testSecret: read the AWSPENDING version and authenticate against the resource, a SELECT 1, a db.runCommand({ping:1}). If this fails, rotation aborts before the staging-label flip; AWSCURRENT still points at the working version.
  • finishSecret: UpdateSecretVersionStage atomically moves AWSCURRENT to the AWSPENDING version; AWSPREVIOUS lands on the demoted version; AWSPENDING is removed.

Single user versus alternating users. Single user: one account, rotation calls ALTER USER in place, simple, used for masters, briefly disruptive between setSecret and finishSecret. Alternating users: two accounts (app_user_a, app_user_b) take turns; each rotation updates the inactive account, tests it, and flips, zero-downtime. Alternating-users is the strategy for application credentials.

Rotation schedule. RotateSecret takes RotationRules with AutomaticallyAfterDays (1-365) or a ScheduleExpression. Minimum interval every 4 hours. Each rotation emits RotationStarted, RotationSucceeded, or RotationFailed to EventBridge.

A worked rotation trace

/platform/payments-api/prod/db/app, the application user for an RDS Postgres database, 30-day cadence, 29 days 23 hours since last rotation. The schedule fires.

Step 1 – createSecret. The Lambda calls GetRandomPassword to mint a 32-character password, then PutSecretValue with VersionStages = ["AWSPENDING"]. Two versions now: v1 with AWSCURRENT, v2 with AWSPENDING. GetSecretValue without a VersionStage still returns v1. The database is untouched.

Step 2 – setSecret. The Lambda fetches the master credential (a separate secret, configured at rotation setup), connects to the RDS Postgres endpoint, and runs ALTER USER app_user WITH PASSWORD '<NEW>'. The database accepts the new password. The application is still reading v1 and using the OLD one, which has just stopped working. Connections re-authenticating with the OLD password fail until finishSecret. Exactly why alternating-user is the strategy for high-traffic application credentials.

Step 3 – testSecret. The Lambda reads v2, opens a fresh connection as app_user with the NEW password, runs SELECT 1, and confirms. If this fails, Secrets Manager marks the rotation RotationFailed and AWSCURRENT is never moved. testSecret gates the flip.

Step 4 – finishSecret. UpdateSecretVersionStage moves AWSCURRENT from v1 to v2; AWSPREVIOUS lands on v1; AWSPENDING is removed. The next GetSecretValue returns the NEW password. EventBridge receives RotationSucceeded.

Rollback. A single UpdateSecretVersionStage call moves AWSCURRENT back from v2 to v1. The OLD password is still in v1; the database needs it reinstated via ALTER USER, or the alternating-user strategy keeps it valid automatically.

Cross-region replication. The primary in eu-west-1 has a replica in eu-west-2. When the primary rotates, the new version replicates within seconds. Applications in either Region see the same value after finishSecret. Rotation runs only against the primary; replicas are read-only until promoted.

Where Parameter Store wins

Parameter Store earns its place for everything that doesn’t need rotation. Cost is the obvious driver; two places it’s also actively better.

Hierarchical reads with GetParametersByPath. A single API call returns every parameter under a prefix, recursively. A service starting up fetches its entire config tree – /platform/payments-api/prod/*, in one request and decrypts the SecureString values inline. Secrets Manager’s nearest equivalent is BatchGetSecretValue, which takes explicit lists or filters, not a path prefix. For app-config patterns, the path semantics fit cleaner.

Advanced-tier parameter policies. NoChangeNotification is useful for signing keys that rotate annually-by-redeploy: a 350-day NoChangeNotification fires an EventBridge event 15 days before the unwritten policy expects rotation, surfacing in the team’s alerting channel. Not rotation, but a calendar reminder built into the storage layer.

The pattern that doesn’t work: NoChangeNotification as a substitute for rotation on database credentials. The notification is a poke; nothing changes. For credentials that must rotate, the Secrets Manager Lambda is the only first-class answer.

Hybrid patterns

Three patterns recur. First, bootstrap reads from both stores, a service calls GetParametersByPath /platform/payments-api/prod/ for its config and non-rotating secrets, then GetSecretValue for each rotating database credential; the IAM policy allows both actions under the matching prefix. Second, rotation triggers application refresh. Secrets Manager publishes RotationSucceeded to EventBridge after finishSecret; services with long-lived database connections either close-and-reopen on that event or re-fetch the secret on the first authentication failed error. Without the event, applications cache indefinitely and break silently on the next rotation. Third, cross-region reads, instances in eu-west-2 read Secrets Manager via the local regional endpoint, replicas make the read local; Parameter Store has no native equivalent, so write the parameter into each Region at deploy time.

What’s worth remembering

  1. Two stores, not one. Secrets Manager for credentials that rotate, Parameter Store SecureString for everything else. Cheaper and more architecturally honest than a single-store answer.
  2. Secrets Manager: $0.40 per secret per month plus $0.05 per 10,000 API calls; replicas bill separately at the same rate; KMS with aws/secretsmanager is free.
  3. Parameter Store Standard is free for SecureString, hierarchical paths, 4 KB values; default 40 TPS shared across GetParameter, GetParameters, GetParametersByPath.
  4. Parameter Store Advanced is $0.05 per parameter per month, 8 KB values, policies Expiration, ExpirationNotification, NoChangeNotification; no native rotation.
  5. Secrets Manager rotation is a four-step Lambda contract: createSecret / setSecret / testSecret / finishSecret; each step must be idempotent.
  6. Three reserved staging labels: AWSCURRENT (default read), AWSPENDING (rotating in), AWSPREVIOUS (rollback handle). One version per label; moves are atomic.
  7. Managed rotation covers RDS, Aurora, Redshift, and DocumentDB, no custom Lambda; everything else uses the four-step contract.
  8. Single-user for masters (briefly disruptive); alternating-user for application credentials (zero-downtime).
  9. Cross-region replication is native to Secrets Manager; rotation propagates from primary to replicas. Parameter Store has no equivalent.
  10. Minimum rotation interval 4 hours; events go to EventBridge; secret value max 65,536 bytes.

These posts are LLM-aided. Backbone, original writing, and structure by Craig. Research and editing by Craig + LLM. Proof-reading by Craig.