Every quarter, I end up in the same meeting. Someone on the platform team has noticed that the AWS bill has a line for Secrets Manager that keeps growing, and someone else has noticed that half the secrets in the account are actually in SSM Parameter Store. The question is always the same: why do we have both, and should we consolidate?
The honest answer is that AWS shipped Parameter Store first as part of Systems Manager, then shipped Secrets Manager as a dedicated product, and the feature surfaces overlap in ways that would not exist if either team had designed the other. Both can store secrets. Both encrypt with KMS. Both integrate with IAM. Both have SDKs and CLI support. But they are not interchangeable, and the places they diverge are exactly the places that matter when you are running production workloads.
The Cost Math Nobody Wants to Do
Parameter Store is free for standard parameters (up to 4 KB, with a 10,000 parameter limit per region). Advanced parameters cost $0.05 per parameter per month plus $0.05 per 10,000 API calls. Secrets Manager charges $0.40 per secret per month plus $0.05 per 10,000 API calls.
For 100 secrets accessed 100,000 times a month:
- Parameter Store standard: $0 for storage, $0.50 for API calls = $0.50/month
- Parameter Store advanced: $5 for storage, $0.50 for API calls = $5.50/month
- Secrets Manager: $40 for storage, $0.50 for API calls = $40.50/month
At 1,000 secrets, Secrets Manager is $400/month before API calls. Multiply that across staging, dev, and production accounts and you are at $15,000+ per year for the same logical secret set. That is real money for a feature set most organizations do not fully use.
The cost difference is only meaningful if Secrets Manager's extra features are actually being used. In my experience about half the time they are not.
What Each Service Actually Does Better
Parameter Store is better at:
- Configuration. If you have a feature flag, a database hostname, or a log level, Parameter Store is the right home. It has a hierarchical namespace (
/app/prod/db/host) andGetParametersByPathcan pull whole trees in one call. - Integration with CloudFormation, CDK, and Terraform, which all support Parameter Store natively without extra privileges.
- Cost when you have a lot of low-value secrets — API tokens, webhook URLs, per-tenant config — that do not need rotation.
- EC2 instance parameter refresh. An instance can pull parameters on boot and cache them, and the IAM policies are straightforward.
Secrets Manager is better at:
- Automatic rotation. If you need Lambda-driven rotation for an RDS password, a Redshift credential, or a DocumentDB user, Secrets Manager has the rotation hooks and the Lambda templates. Building the same in Parameter Store requires writing your own Lambda and scheduler.
- Cross-account secret sharing. Secrets Manager resource policies support cross-account access natively; Parameter Store requires going through KMS key policies and gets messy fast.
- Versioning with staging labels. Secrets Manager has
AWSCURRENT,AWSPREVIOUS,AWSPENDING— a built-in state machine for two-phase rotation. Parameter Store has version numbers but no semantics on top of them. - Secret replication across regions. A single API call replicates a Secrets Manager secret to N regions. Parameter Store requires a custom Lambda per region.
The dividing line in my head is simple: if the thing rotates or is cross-account, it goes in Secrets Manager. Otherwise, it goes in Parameter Store.
A Real IAM Pattern That Works
The most common mistake I see is overly permissive IAM for secrets. Both services support resource-level IAM, and the pattern is the same:
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": [
"secretsmanager:GetSecretValue",
"secretsmanager:DescribeSecret"
],
"Resource": "arn:aws:secretsmanager:us-east-1:111111111111:secret:prod/payments/*"
},
{
"Effect": "Allow",
"Action": ["kms:Decrypt"],
"Resource": "arn:aws:kms:us-east-1:111111111111:key/abcd-1234-...",
"Condition": {
"StringEquals": {
"kms:ViaService": "secretsmanager.us-east-1.amazonaws.com"
}
}
}
]
}
The wildcard on the secret ARN scopes the policy to a prefix, not a specific secret — useful when you have per-tenant or per-environment secrets. The kms:ViaService condition prevents a compromised IAM principal from using the KMS key to decrypt anything outside Secrets Manager.
For Parameter Store the analog is ssm:GetParameters and ssm:GetParametersByPath, scoped to a path prefix. Do not grant ssm:* to compute roles. I have cleaned up too many accounts where every EC2 instance profile could write any parameter in the account.
The Rotation Story
Secrets Manager rotation works well for services AWS has templates for: RDS (Postgres, MySQL, MariaDB, Oracle, SQL Server), Redshift, DocumentDB. The templates handle the two-phase commit properly — the new credential is created and tested before AWSCURRENT moves — and they log rotation events to CloudTrail.
What the marketing does not tell you: rotation for anything AWS does not template is custom Lambda code. If you have an API key for a third-party SaaS, you are writing a rotation Lambda that calls their API, updates the secret, and handles the version labels. I have written this Lambda maybe fifteen times and the pattern is always the same:
def lambda_handler(event, context):
step = event["Step"]
secret_arn = event["SecretId"]
token = event["ClientRequestToken"]
client = boto3.client("secretsmanager")
if step == "createSecret":
# Generate new credential against the third-party API
new_value = rotate_at_provider()
client.put_secret_value(
SecretId=secret_arn,
ClientRequestToken=token,
SecretString=json.dumps(new_value),
VersionStages=["AWSPENDING"],
)
elif step == "setSecret":
pass # Nothing to set on the target; the API is the target
elif step == "testSecret":
pending = get_version(client, secret_arn, "AWSPENDING")
assert test_credential(pending)
elif step == "finishSecret":
client.update_secret_version_stage(
SecretId=secret_arn, VersionStage="AWSCURRENT",
MoveToVersionId=token,
)
If you are rotating fewer than five third-party credentials, consider whether a scheduled GitHub Action calling the provider API and storing in Parameter Store is cheaper than the Lambda you will write, monitor, and patch.
Migration: From One to the Other
Consolidation usually goes one direction: Parameter Store to Secrets Manager. Do it per-secret, not wholesale. For each secret, ask:
- Does it rotate automatically? Move to Secrets Manager.
- Is it accessed cross-account? Move to Secrets Manager.
- Is it a database credential used by an RDS-native service? Move to Secrets Manager.
- Is it larger than 4 KB (e.g. a TLS keypair bundle)? Move to Secrets Manager (8 KB limit vs 4 KB standard, or Parameter Store advanced at 8 KB).
- Otherwise, leave it in Parameter Store.
Keep the SDK calls abstracted behind a thin wrapper in your application code so the storage backend is a config flag. If you ever switch back, or run multi-cloud, the wrapper pays for itself the first week.
How Safeguard Helps
Safeguard inventories every Secrets Manager and Parameter Store reference across your AWS accounts and maps them back to the workloads that consume them — so you can finally answer "which services read from this secret?" without grepping through a dozen repos. We surface unrotated credentials beyond your policy window, over-scoped IAM roles with blanket ssm:* or secretsmanager:* grants, and secrets that have moved between services without the corresponding IAM cleanup. When a secret is referenced in code but never found in either store, we flag it as a likely plaintext leak to investigate.