Published: January 2, 2025
•
Updated: January 2, 2025
Every application needs secrets: database passwords, API keys, TLS certificates, encryption keys. The worst place to store them? Hardcoded in code, committed to Git, or in plain-text environment variables.
The two most popular solutions for proper secrets management are:
This guide helps you choose the right one for your organization, provides a detailed comparison, and includes migration plans for moving from one to the other.
┌─────────────────────────────┬───────────────────┬──────────────────┬──────────────────────┐ │ Feature │ HashiCorp Vault │ SSM Parameter │ AWS Secrets Manager │ │ │ │ Store │ │ ├─────────────────────────────┼───────────────────┼──────────────────┼──────────────────────┤ │ **Platform Support** │ Any cloud, on-prem│ AWS only │ AWS only │ │ Multi-cloud/hybrid │ ✅ Excellent │ ❌ AWS locked-in │ ❌ AWS locked-in │ │ Self-hosted option │ ✅ Open source │ ❌ Managed only │ ❌ Managed only │ │ │ │ │ │ │ **Secret Storage** │ │ │ │ │ Max secret size │ 1 MB │ 4 KB (standard) │ 64 KB │ │ │ │ 8 KB (advanced) │ │ │ Versioning │ ✅ Full history │ ✅ Up to 100 │ ✅ Full history │ │ Secret rotation │ ✅ Built-in │ ❌ Manual │ ✅ Automated (RDS, │ │ │ │ │ RedShift, etc.) │ │ │ │ │ │ │ **Dynamic Secrets** │ │ │ │ │ Database credentials │ ✅ Ephemeral │ ❌ Static only │ ⚠️ Rotation only │ │ AWS IAM credentials │ ✅ Temporary │ ❌ No │ ❌ No │ │ PKI/TLS certificates │ ✅ CA + issuance │ ❌ No │ ⚠️ Import only │ │ │ │ │ │ │ **Access Control** │ │ │ │ │ Fine-grained policies │ ✅ Path-based ACL │ ✅ IAM policies │ ✅ IAM policies │ │ Identity federation │ ✅ OIDC, LDAP, │ ⚠️ IAM only │ ⚠️ IAM only │ │ │ K8s, etc. │ │ │ │ Leasing & TTL │ ✅ Yes │ ❌ No │ ❌ No │ │ │ │ │ │ │ **Auditability** │ │ │ │ │ Audit logging │ ✅ Detailed │ ✅ CloudTrail │ ✅ CloudTrail │ │ Who accessed what/when │ ✅ Full trail │ ✅ Yes │ ✅ Yes │ │ │ │ │ │ │ **Operations** │ │ │ │ │ High Availability │ ⚠️ Self-manage │ ✅ AWS managed │ ✅ AWS managed │ │ Disaster Recovery │ ⚠️ Manual backup │ ✅ Multi-region │ ✅ Multi-region │ │ Operational complexity │ 🔴 High (if self- │ 🟢 Low │ 🟢 Low │ │ │ hosted) │ │ │ │ │ │ │ │ │ **Cost (approx)** │ │ │ │ │ 1,000 secrets, 1M reads/mo │ $0-$50 (self) │ $1-$5 │ $400-$500 │ │ │ or $300 (HCP) │ │ │ │ 10,000 secrets, 10M reads │ $0-$200 (self) │ $10-$50 │ $4,000-$5,000 │ │ │ or $1,500 (HCP) │ │ │ └─────────────────────────────┴───────────────────┴──────────────────┴──────────────────────┘
Choose Vault if you need:
# Install Vault on Kubernetes (production-ready with HA) helm repo add hashicorp https://helm.releases.hashicorp.com helm install vault hashicorp/vault \ --namespace vault --create-namespace \ --set server.ha.enabled=true \ --set server.ha.replicas=3 \ --set ui.enabled=true \ --set server.dataStorage.size=10Gi # Initialize Vault (do this ONCE) kubectl exec -n vault vault-0 -- vault operator init -key-shares=5 -key-threshold=3 # Output: 5 unseal keys + 1 root token # STORE THESE SECURELY (split across team members or use PGP encryption) # Unseal Vault on each pod (requires 3 of 5 keys) kubectl exec -n vault vault-0 -- vault operator unseal <KEY1> kubectl exec -n vault vault-0 -- vault operator unseal <KEY2> kubectl exec -n vault vault-0 -- vault operator unseal <KEY3> # Repeat for vault-1 and vault-2 # Verify status kubectl exec -n vault vault-0 -- vault status
# Enable database secrets engine vault secrets enable database # Configure PostgreSQL connection vault write database/config/prod-postgres \ plugin_name=postgresql-database-plugin \ allowed_roles="readonly","readwrite" \ connection_url="postgresql://{{username}}:{{password}}@postgres.prod.svc:5432/app?sslmode=require" \ username="vault-admin" \ password="vault-admin-password" # Create a role that generates 1-hour credentials vault write database/roles/readonly \ db_name=prod-postgres \ creation_statements="CREATE ROLE \"{{name}}\" WITH LOGIN PASSWORD '{{password}}' VALID UNTIL '{{expiration}}'; \ GRANT SELECT ON ALL TABLES IN SCHEMA public TO \"{{name}}\";" \ default_ttl="1h" \ max_ttl="24h" # Test: Generate ephemeral credentials vault read database/creds/readonly # Output: # Key Value # lease_id database/creds/readonly/abc123 # lease_duration 1h # username v-root-readonly-xyz456 # password A1b2C3d4E5f6... # After 1 hour, these credentials are automatically revoked!
Key benefit: Applications never store long-lived database passwords. They fetch new credentials on startup, use them, and they expire automatically.
Choose SSM if you need:
# Store a secret (encrypted with KMS) aws ssm put-parameter \ --name "/production/database/password" \ --value "super-secret-password" \ --type SecureString \ --kms-key-id alias/production-secrets \ --description "Production database password" \ --tags Key=Environment,Value=production Key=Owner,Value=platform-team # Retrieve a secret aws ssm get-parameter \ --name "/production/database/password" \ --with-decryption # Output (JSON): { "Parameter": { "Name": "/production/database/password", "Type": "SecureString", "Value": "super-secret-password", "Version": 1, "LastModifiedDate": "2025-01-02T10:00:00.000Z", "ARN": "arn:aws:ssm:us-east-1:123456789012:parameter/production/database/password" } } # Get multiple secrets by path aws ssm get-parameters-by-path \ --path "/production/database" \ --with-decryption \ --recursive # Update a secret (creates new version) aws ssm put-parameter \ --name "/production/database/password" \ --value "new-super-secret-password" \ --type SecureString \ --overwrite
AWS has TWO secret storage services. Here's when to use each:
Use for: Configuration values, feature flags, non-sensitive data, infrequently-changing secrets
Cost: $0 for up to 10,000 parameters + $0.05 per 10,000 API calls
Use for: Secrets requiring automatic rotation (RDS, Redshift, DocumentDB passwords), secrets with built-in integrations
Cost: $0.40/secret/month + $0.05 per 10,000 API calls
Both Vault and SSM can inject secrets into Kubernetes using the External Secrets Operator. This is the modern approach (replaces older methods like Vault Agent Injector or aws-secret-operator).
# Install External Secrets Operator helm repo add external-secrets https://charts.external-secrets.io helm install external-secrets external-secrets/external-secrets \ --namespace external-secrets --create-namespace # Verify installation kubectl get pods -n external-secrets
# Create SecretStore (points to Vault) apiVersion: external-secrets.io/v1beta1 kind: SecretStore metadata: name: vault-backend namespace: production spec: provider: vault: server: "https://vault.vault.svc:8200" path: "secret" version: "v2" auth: kubernetes: mountPath: "kubernetes" role: "production-app" --- # Create ExternalSecret (syncs specific secrets) apiVersion: external-secrets.io/v1beta1 kind: ExternalSecret metadata: name: app-database-secret namespace: production spec: refreshInterval: 1h secretStoreRef: name: vault-backend kind: SecretStore target: name: app-database-secret # Name of resulting K8s Secret creationPolicy: Owner data: - secretKey: DB_PASSWORD # Key in K8s Secret remoteRef: key: production/database # Path in Vault property: password # Field in Vault secret
# Create SecretStore (points to SSM) apiVersion: external-secrets.io/v1beta1 kind: SecretStore metadata: name: aws-parameter-store namespace: production spec: provider: aws: service: ParameterStore region: us-east-1 auth: jwt: # Uses IRSA (IAM Roles for Service Accounts) serviceAccountRef: name: external-secrets-sa --- # Create ExternalSecret apiVersion: external-secrets.io/v1beta1 kind: ExternalSecret metadata: name: app-config namespace: production spec: refreshInterval: 5m secretStoreRef: name: aws-parameter-store kind: SecretStore target: name: app-config dataFrom: - extract: key: /production/app/ # Syncs ALL parameters under this path
Result: Secrets in Vault/SSM are automatically synced to Kubernetes Secrets. Update the source, and External Secrets Operator refreshes the K8s Secret within `refreshInterval`.
Use this flowchart to decide:
Question 1: Are you multi-cloud or on-premises?
✅ Yes: → Use Vault (SSM is AWS-only)
❌ No (AWS-only): → Continue to Question 2
Question 2: Do you need dynamic secrets (ephemeral credentials)?
✅ Yes: → Use Vault (SSM doesn't support dynamic secrets)
❌ No: → Continue to Question 3
Question 3: Do you need automatic secret rotation?
✅ Yes, for RDS/Redshift/etc: → Use AWS Secrets Manager (built-in rotation)
❌ No / Custom rotation: → Continue to Question 4
Question 4: How many secrets do you have?
🔹 <1,000 secrets: → Use SSM Parameter Store (simple, free tier)
🔹 1,000-10,000 secrets: → Use SSM or Vault (cost similar, choose based on team expertise)
🔹 >10,000 secrets: → Use Vault (AWS Secrets Manager becomes expensive)
┌────────────────────────┬──────────────┬──────────────┬──────────────────┐ │ Scenario │ Vault │ SSM Param │ Secrets Manager │ │ │ (self-hosted)│ Store │ │ ├────────────────────────┼──────────────┼──────────────┼──────────────────┤ │ 100 secrets │ $0-$10 │ $0 │ $40/mo │ │ 100K reads/mo │ │ │ │ │ │ │ │ │ │ 1,000 secrets │ $0-$50 │ $1-$5 │ $400/mo │ │ 1M reads/mo │ │ │ │ │ │ │ │ │ │ 5,000 secrets │ $0-$100 │ $5-$25 │ $2,000/mo │ │ 5M reads/mo │ │ │ │ │ │ │ │ │ │ 10,000 secrets │ $0-$200 │ $10-$50 │ $4,000/mo │ │ 10M reads/mo │ │ │ │ │ │ │ │ │ │ 50,000 secrets │ $0-$500 │ $50-$250 │ $20,000/mo │ │ 50M reads/mo │ │ │ │ └────────────────────────┴──────────────┴──────────────┴──────────────────┘ Note: Vault costs assume self-hosted on existing infrastructure. Add $300-$1,500/mo for HCP Vault (managed) depending on scale.
Key takeaway: AWS Secrets Manager is expensive at scale ($0.40/secret/month adds up fast). SSM Parameter Store is nearly free for storage, only charging for API calls. Vault is free if self-hosted, but adds operational complexity.
If you're outgrowing SSM and need Vault's advanced features, here's a phased migration plan:
#!/bin/bash # migrate-ssm-to-vault.sh set -e VAULT_ADDR="https://vault.example.com" VAULT_TOKEN="your-vault-token" SSM_PATH="/production/" echo "Fetching all SSM parameters under $SSM_PATH..." PARAMETERS=$(aws ssm get-parameters-by-path \ --path "$SSM_PATH" \ --with-decryption \ --recursive \ --output json) echo "$PARAMETERS" | jq -r '.Parameters[] | @json' | while read -r param; do NAME=$(echo "$param" | jq -r '.Name') VALUE=$(echo "$param" | jq -r '.Value') # Convert SSM path to Vault path # /production/database/password -> secret/data/production/database VAULT_PATH=$(echo "$NAME" | sed 's|^/||' | sed 's|/[^/]*$||') KEY=$(basename "$NAME") echo "Migrating $NAME -> secret/data/$VAULT_PATH (key: $KEY)" # Write to Vault (KV v2) vault kv put "secret/$VAULT_PATH" "$KEY=$VALUE" done echo "Migration complete!"
Moving from Vault to SSM (simplification/cost reduction scenario):
#!/bin/bash # migrate-vault-to-ssm.sh set -e VAULT_ADDR="https://vault.example.com" VAULT_TOKEN="your-vault-token" VAULT_PATH="secret/production" echo "Fetching all secrets from Vault at $VAULT_PATH..." SECRETS=$(vault kv list -format=json "$VAULT_PATH") echo "$SECRETS" | jq -r '.[]' | while read -r secret; do echo "Processing $VAULT_PATH/$secret..." # Get secret data SECRET_DATA=$(vault kv get -format=json "$VAULT_PATH/$secret") # Extract key-value pairs echo "$SECRET_DATA" | jq -r '.data.data | to_entries[] | @json' | while read -r kv; do KEY=$(echo "$kv" | jq -r '.key') VALUE=$(echo "$kv" | jq -r '.value') # Convert to SSM path SSM_NAME="/production/$secret/$KEY" echo "Writing to SSM: $SSM_NAME" aws ssm put-parameter \ --name "$SSM_NAME" \ --value "$VALUE" \ --type SecureString \ --overwrite done done echo "Migration complete!"
Use git-secrets or gitleaks to scan commits. Configure pre-commit hooks to prevent accidental commits.
High-risk secrets (prod DB): Every 30-90 days
Medium-risk (API keys): Every 6 months
Low-risk (feature flags): As needed
Vault: Path-based policies (apps can only read /production/their-app/)
SSM: IAM policies with parameter name prefixes (Allow: GetParameter on /production/app-name/*)
Vault: Configure audit backend (file, syslog, or AWS CloudWatch)
SSM: Enable CloudTrail for all GetParameter API calls
There's no universal winner between Vault and SSM—it depends on your requirements:
Pro tip: You can use both! Store config in SSM, dynamic secrets in Vault. Use External Secrets Operator to abstract the backend from your apps. This gives you flexibility to migrate later without changing application code.
We design and implement production-grade secrets management: Vault setup, SSM architecture, rotation automation, Kubernetes integration, and migration support.
HostingX IL
Scalable automation & integration platform accelerating modern B2B product teams.
Services
Subscribe to our newsletter
Get monthly email updates about improvements.
Copyright © 2025 HostingX IL. All Rights Reserved.