Skip to main content

Secrets Manager vs Parameter Store vs Vault — Secure Your Secrets on AWS

· 6 min read
Goel Academy
DevOps & Cloud Learning Hub

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.

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:

FeatureStandard TierAdvanced Tier
Max parameters10,000100,000
Max value size4 KB8 KB
Parameter policiesNoYes (expiration, notification)
CostFree$0.05/parameter/month
Throughput40 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

FeatureSecrets ManagerParameter StoreHashiCorp Vault
Auto rotationBuilt-in (RDS, Redshift, DocumentDB)Manual (Lambda required)Built-in (many backends)
Cost$0.40/secret/month + $0.05/10K API callsFree (Standard), $0.05/param (Advanced)Self-hosted or HCP ($$$)
Max size64 KB4 KB (Standard), 8 KB (Advanced)No limit
VersioningYes (staging labels)Yes (version numbers)Yes (versions + metadata)
Cross-accountYes (resource policy)Yes (via IAM + KMS)Yes (namespaces)
EncryptionMandatory (KMS)Optional (SecureString type)Mandatory (Shamir/auto-unseal)
AuditCloudTrailCloudTrailBuilt-in audit log
Dynamic secretsNoNoYes (generates on-demand)
Multi-cloudAWS onlyAWS onlyAny cloud + on-prem
ComplexityLowVery LowHigh

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.