Skip to content

Secrets Manager and Parameter Store

First PublishedByAtif Alam

Applications need configuration values (feature flags, endpoint URLs) and secrets (database passwords, API keys, certificates). AWS provides two services for this: Secrets Manager for secrets with automatic rotation, and Parameter Store for general configuration.

FeatureSecrets ManagerParameter Store
PurposeSecrets (passwords, API keys, tokens)Configuration values and secrets
Automatic rotationYes (built-in, Lambda-based)No
Cost$0.40/secret/month + $0.05/10K API callsFree for standard (up to 10K parameters); advanced tier: $0.05/parameter/month
Max size64 KB4 KB (standard) / 8 KB (advanced)
EncryptionAlways encrypted (KMS)Optional (SecureString uses KMS)
Cross-accountYes (resource policies)No
VersioningYes (automatic)Yes (labels)

Rule of thumb: Use Secrets Manager for credentials that need rotation (DB passwords, API keys). Use Parameter Store for everything else (config values, feature flags, non-sensitive settings).

Terminal window
# Create a secret
aws secretsmanager create-secret \
--name prod/database/credentials \
--description "Production database credentials" \
--secret-string '{"username":"admin","password":"S3cure!Pass","host":"mydb.cluster-xyz.rds.amazonaws.com","port":"5432","dbname":"myapp"}'
Terminal window
# CLI
aws secretsmanager get-secret-value --secret-id prod/database/credentials
# Python (boto3)
import boto3, json
client = boto3.client('secretsmanager')
response = client.get_secret_value(SecretId='prod/database/credentials')
secret = json.loads(response['SecretString'])
db_host = secret['host']
db_pass = secret['password']
import boto3, json, os
secrets_client = boto3.client('secretsmanager')
def get_db_credentials():
response = secrets_client.get_secret_value(
SecretId=os.environ['DB_SECRET_ARN']
)
return json.loads(response['SecretString'])
def handler(event, context):
creds = get_db_credentials()
# connect to database using creds['host'], creds['username'], creds['password']

Tip: Cache the secret value in a module-level variable so you don’t call Secrets Manager on every Lambda invocation. The AWS SDK provides a caching layer for this.

ECS can inject secrets directly as environment variables:

{
"containerDefinitions": [{
"name": "app",
"secrets": [
{
"name": "DB_PASSWORD",
"valueFrom": "arn:aws:secretsmanager:us-east-1:123456789012:secret:prod/database/credentials:password::"
},
{
"name": "API_KEY",
"valueFrom": "arn:aws:secretsmanager:us-east-1:123456789012:secret:prod/api-key"
}
]
}]
}

The valueFrom ARN can reference a specific JSON key within the secret (:password:: above).

buildspec.yml
env:
secrets-manager:
DB_PASSWORD: prod/database/credentials:password
API_KEY: prod/api-key

Secrets Manager can rotate secrets automatically using a Lambda function:

┌──────────────┐ trigger ┌──────────────┐ update ┌──────────────┐
│ Secrets │─────────────────►│ Rotation │──────────────────►│ Database │
│ Manager │ │ Lambda │ │ (new pass) │
│ (schedule) │◄─────────────────│ │ └──────────────┘
│ │ store new secret│ │
└──────────────┘ └──────────────┘
Terminal window
# Enable rotation for an RDS secret (AWS provides built-in rotation Lambdas)
aws secretsmanager rotate-secret \
--secret-id prod/database/credentials \
--rotation-lambda-arn arn:aws:lambda:us-east-1:123456789012:function:SecretsManagerRDSRotation \
--rotation-rules AutomaticallyAfterDays=30

How rotation works (4 steps):

  1. createSecret — Generate a new password and store it as AWSPENDING.
  2. setSecret — Update the database with the new password.
  3. testSecret — Verify the new password works.
  4. finishSecret — Mark the new version as AWSCURRENT.

If any step fails, the old password remains active — no downtime.

Use a hierarchical naming pattern:

{environment}/{service}/{secret-type}
prod/database/credentials
prod/api/stripe-key
staging/database/credentials
dev/api/sendgrid-key

This makes it easy to control access with IAM policies:

{
"Effect": "Allow",
"Action": "secretsmanager:GetSecretValue",
"Resource": "arn:aws:secretsmanager:us-east-1:123456789012:secret:prod/*"
}

Parameter Store is a simpler, cheaper option for configuration values and secrets that don’t need rotation.

TypeEncryptionUse Case
StringNoFeature flags, URLs, non-sensitive config
StringListNoComma-separated lists
SecureStringYes (KMS)Passwords, API keys (without rotation)
Terminal window
# Plain string
aws ssm put-parameter \
--name /myapp/prod/db_host \
--value "mydb.cluster-xyz.rds.amazonaws.com" \
--type String
# Secure string (encrypted)
aws ssm put-parameter \
--name /myapp/prod/db_password \
--value "S3cure!Pass" \
--type SecureString
# With a specific KMS key
aws ssm put-parameter \
--name /myapp/prod/api_key \
--value "sk_live_abc123" \
--type SecureString \
--key-id alias/myapp-key
Terminal window
# Single parameter
aws ssm get-parameter --name /myapp/prod/db_host
# Decrypt SecureString
aws ssm get-parameter --name /myapp/prod/db_password --with-decryption
# Get all parameters under a path
aws ssm get-parameters-by-path --path /myapp/prod/ --with-decryption --recursive
import boto3
ssm = boto3.client('ssm')
# Single parameter
response = ssm.get_parameter(Name='/myapp/prod/db_host')
db_host = response['Parameter']['Value']
# Multiple parameters by path
response = ssm.get_parameters_by_path(
Path='/myapp/prod/',
WithDecryption=True,
Recursive=True
)
config = {p['Name'].split('/')[-1]: p['Value'] for p in response['Parameters']}
# {'db_host': '...', 'db_password': '...', 'api_key': '...'}
{
"containerDefinitions": [{
"name": "app",
"secrets": [
{
"name": "DB_HOST",
"valueFrom": "arn:aws:ssm:us-east-1:123456789012:parameter/myapp/prod/db_host"
}
]
}]
}

Reference Parameter Store values in CloudFormation/SAM/CDK:

# SAM template
Environment:
Variables:
DB_HOST: !Sub "{{resolve:ssm:/myapp/prod/db_host}}"
DB_PASSWORD: !Sub "{{resolve:ssm-secure:/myapp/prod/db_password}}"
data "aws_ssm_parameter" "db_host" {
name = "/myapp/prod/db_host"
}
data "aws_ssm_parameter" "db_password" {
name = "/myapp/prod/db_password"
with_decryption = true
}
resource "aws_ecs_task_definition" "app" {
container_definitions = jsonencode([{
environment = [
{ name = "DB_HOST", value = data.aws_ssm_parameter.db_host.value }
]
secrets = [
{ name = "DB_PASSWORD", valueFrom = data.aws_ssm_parameter.db_password.arn }
]
}])
}
FeatureStandardAdvanced
Max parameters10,000100,000
Max size4 KB8 KB
Parameter policiesNoYes (expiration, notification)
CostFree$0.05/parameter/month
PracticeWhy
Never hardcode secrets in codeGit history, container images, and logs expose them
Use hierarchical naming (/env/service/key)Easy IAM scoping and bulk retrieval
Encrypt with KMS (SecureString)At-rest encryption for all secrets
Enable rotation for database credentialsReduces risk of credential compromise
Use IAM roles for access (not access keys)Temporary credentials, no key management
Cache secrets in application memoryReduce API calls and latency
Audit access with CloudTrailKnow who accessed which secrets and when
Use separate secrets per environmentprod/* and dev/* with different IAM policies
  • Secrets Manager is for secrets that need automatic rotation — DB passwords, API keys, certificates. Costs $0.40/secret/month.
  • Parameter Store is for general configuration values and simpler secrets. Standard tier is free (up to 10K parameters).
  • Both integrate with Lambda, ECS, CodeBuild, and Terraform for seamless secret injection.
  • Use hierarchical naming (/env/service/key) and scope IAM policies by path prefix.
  • Never hardcode secrets — use one of these services and inject at runtime.