Secrets Manager vs Parameter Store vs Vault — Secure Your Secrets on AWS
A developer pushes a commit. Buried on line 47 of a config file is a database password in plaintext. The repo is public. Within 6 hours, a bot has scraped the credential, connected to the RDS instance, and exfiltrated the user table. This isn't hypothetical — GitHub reports revoking millions of leaked secrets every year. The fix isn't discipline; it's architecture.
Why Hardcoded Secrets Fail
Every time a secret lives in source code, environment files checked into git, or Docker images, you have the same problems: anyone with repo access sees it, rotating it means redeploying everywhere, and you have no audit trail of who accessed it or when. The solution is a centralized secrets store that your applications query at runtime.
AWS gives you two native options (Secrets Manager and Parameter Store), and HashiCorp Vault is the popular third-party alternative. They solve the same problem differently.
AWS Secrets Manager — Full-Featured Secret Management
Secrets Manager is purpose-built for secrets. Its headline feature is automatic rotation — it can rotate RDS, Redshift, and DocumentDB credentials on a schedule without any application changes:
# Create a secret for an RDS database
aws secretsmanager create-secret \
--name prod/myapp/database \
--description "Production RDS credentials" \
--secret-string '{
"username": "admin",
"password": "initial-password-change-me",
"engine": "mysql",
"host": "mydb.cluster-abc123.us-east-1.rds.amazonaws.com",
"port": 3306,
"dbname": "myapp"
}'
# Enable automatic rotation (every 30 days)
aws secretsmanager rotate-secret \
--secret-id prod/myapp/database \
--rotation-lambda-arn arn:aws:lambda:us-east-1:123456789012:function:SecretsManagerRDSMySQLRotation \
--rotation-rules '{"AutomaticallyAfterDays": 30}'
Secrets Manager handles the rotation lifecycle: it creates a new password, updates the database, tests the connection, and marks the new version as current. Your app just fetches the latest version every time.
# Retrieve the current secret value
aws secretsmanager get-secret-value \
--secret-id prod/myapp/database \
--query 'SecretString' --output text | jq .
# Retrieve a specific version (during rotation, both old and new exist)
aws secretsmanager get-secret-value \
--secret-id prod/myapp/database \
--version-stage AWSPREVIOUS
Parameter Store — The Lightweight Alternative
Systems Manager Parameter Store stores configuration and secrets in a key-value hierarchy. It has two tiers:
| Feature | Standard Tier | Advanced Tier |
|---|---|---|
| Max parameters | 10,000 | 100,000 |
| Max value size | 4 KB | 8 KB |
| Parameter policies | No | Yes (expiration, notification) |
| Cost | Free | $0.05/parameter/month |
| Throughput | 40 TPS (default) | 1,000 TPS (higher limit) |
# Store a plain configuration value
aws ssm put-parameter \
--name "/myapp/config/api-url" \
--value "https://api.example.com" \
--type String
# Store an encrypted secret
aws ssm put-parameter \
--name "/myapp/prod/db-password" \
--value "s3cur3-p@ssw0rd" \
--type SecureString \
--key-id alias/myapp-key
# Retrieve parameters by path (great for loading all config at once)
aws ssm get-parameters-by-path \
--path "/myapp/prod/" \
--with-decryption \
--recursive
Parameter Store is free for standard parameters with SecureString encryption using your KMS key. The only cost is KMS API calls ($0.03 per 10,000).
Head-to-Head Comparison
| Feature | Secrets Manager | Parameter Store | HashiCorp Vault |
|---|---|---|---|
| Auto rotation | Built-in (RDS, Redshift, DocumentDB) | Manual (Lambda required) | Built-in (many backends) |
| Cost | $0.40/secret/month + $0.05/10K API calls | Free (Standard), $0.05/param (Advanced) | Self-hosted or HCP ($$$) |
| Max size | 64 KB | 4 KB (Standard), 8 KB (Advanced) | No limit |
| Versioning | Yes (staging labels) | Yes (version numbers) | Yes (versions + metadata) |
| Cross-account | Yes (resource policy) | Yes (via IAM + KMS) | Yes (namespaces) |
| Encryption | Mandatory (KMS) | Optional (SecureString type) | Mandatory (Shamir/auto-unseal) |
| Audit | CloudTrail | CloudTrail | Built-in audit log |
| Dynamic secrets | No | No | Yes (generates on-demand) |
| Multi-cloud | AWS only | AWS only | Any cloud + on-prem |
| Complexity | Low | Very Low | High |
Secret Rotation Lambda
If you need to rotate a non-RDS secret (like an API key), you write a Lambda function that Secrets Manager invokes:
# rotation_lambda.py — Custom secret rotation
import boto3
import json
import os
secrets_client = boto3.client('secretsmanager')
def lambda_handler(event, context):
secret_id = event['SecretId']
step = event['Step']
token = event['ClientRequestToken']
if step == "createSecret":
# Generate a new secret value
current = secrets_client.get_secret_value(SecretId=secret_id)
current_dict = json.loads(current['SecretString'])
# Generate new API key (your logic here)
new_key = generate_new_api_key()
current_dict['api_key'] = new_key
secrets_client.put_secret_value(
SecretId=secret_id,
ClientRequestToken=token,
SecretString=json.dumps(current_dict),
VersionStages=['AWSPENDING']
)
elif step == "setSecret":
# Update the external service with the new key
pending = secrets_client.get_secret_value(
SecretId=secret_id, VersionStage='AWSPENDING')
new_secret = json.loads(pending['SecretString'])
update_external_service(new_secret['api_key'])
elif step == "testSecret":
# Verify the new secret works
pending = secrets_client.get_secret_value(
SecretId=secret_id, VersionStage='AWSPENDING')
new_secret = json.loads(pending['SecretString'])
test_connection(new_secret['api_key'])
elif step == "finishSecret":
# Mark the new version as current
secrets_client.update_secret_version_stage(
SecretId=secret_id,
VersionStage='AWSCURRENT',
MoveToVersionId=token,
RemoveFromVersionId=get_current_version(secret_id)
)
The four-step rotation ensures zero downtime — the old secret remains active until the new one is verified.
Accessing Secrets From EC2, Lambda, and ECS
From Lambda (Python SDK)
import boto3
import json
def lambda_handler(event, context):
client = boto3.client('secretsmanager')
response = client.get_secret_value(SecretId='prod/myapp/database')
secret = json.loads(response['SecretString'])
# Use the secret
db_host = secret['host']
db_user = secret['username']
db_pass = secret['password']
From ECS Task Definition
{
"containerDefinitions": [{
"name": "myapp",
"image": "myapp:latest",
"secrets": [
{
"name": "DB_PASSWORD",
"valueFrom": "arn:aws:secretsmanager:us-east-1:123456789012:secret:prod/myapp/database:password::"
},
{
"name": "API_KEY",
"valueFrom": "arn:aws:ssm:us-east-1:123456789012:parameter/myapp/prod/api-key"
}
]
}]
}
ECS natively supports both Secrets Manager and Parameter Store — secrets are injected as environment variables without your code knowing where they came from.
IAM Policy for Access
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": [
"secretsmanager:GetSecretValue"
],
"Resource": "arn:aws:secretsmanager:us-east-1:123456789012:secret:prod/myapp/*"
},
{
"Effect": "Allow",
"Action": [
"kms:Decrypt"
],
"Resource": "arn:aws:kms:us-east-1:123456789012:key/your-kms-key-id"
}
]
}
Always scope the resource ARN to the specific secrets the application needs. Never use * for the resource.
HashiCorp Vault on AWS
Vault adds features that native AWS services don't have, most notably dynamic secrets — Vault generates a unique database credential for each requesting application and automatically revokes it after a TTL:
# Enable the database secrets engine
vault secrets enable database
# Configure Vault to manage your RDS MySQL
vault write database/config/mydb \
plugin_name=mysql-database-plugin \
connection_url="{{username}}:{{password}}@tcp(mydb.abc123.us-east-1.rds.amazonaws.com:3306)/" \
allowed_roles="readonly" \
username="vault_admin" \
password="vault_admin_password"
# Create a role that generates read-only credentials
vault write database/roles/readonly \
db_name=mydb \
creation_statements="CREATE USER '{{name}}'@'%' IDENTIFIED BY '{{password}}'; \
GRANT SELECT ON myapp.* TO '{{name}}'@'%';" \
default_ttl="1h" \
max_ttl="24h"
# Get a dynamic credential (unique username/password, auto-expires)
vault read database/creds/readonly
# username: v-token-readonly-abc123
# password: random-generated-password
# lease_duration: 1h
Every credential is unique, short-lived, and audited. If one leaks, it expires on its own. The tradeoff is operational complexity — you're running and maintaining Vault infrastructure.
Which Should You Pick?
- Few secrets, AWS-only, want auto-rotation for RDS: Secrets Manager
- Lots of config + some secrets, want free tier: Parameter Store (SecureString)
- Multi-cloud, dynamic secrets, advanced policies: HashiCorp Vault
- Practical starting point: Use Parameter Store for config and Secrets Manager for database credentials. Migrate to Vault only if you outgrow them.
What's Next
Now that your secrets are managed properly, what happens when the entire region goes down? In the next post, we'll cover AWS Disaster Recovery — RTO, RPO, and the four DR strategies from backup-and-restore to multi-site active/active.
