vault kv get secret/myapp/db (HashiCorp Vault) or aws secretsmanager get-secret-value --secret-id myapp/db (AWS Secrets Manager)| Tool | Type | Best For | Rotation | Cost | Complexity | Cloud Lock-in |
|---|---|---|---|---|---|---|
| HashiCorp Vault | Self-hosted / HCP | Multi-cloud, enterprise, dynamic secrets | Built-in (dynamic) | Free OSS / HCP from $0.03/hr | High | None |
| AWS Secrets Manager | Managed service | AWS-native workloads | Built-in (Lambda) | $0.40/secret/mo + $0.05/10K API | Low | AWS |
| GCP Secret Manager | Managed service | GCP-native workloads | Custom (Cloud Functions) | $0.06/active version/mo | Low | GCP |
| Azure Key Vault | Managed service | Azure-native workloads | Built-in (certificates) | $0.03/10K ops (Standard) | Low | Azure |
| SOPS (CNCF) | File encryption | GitOps, encrypted config in repos | Manual | Free OSS | Medium | None |
| git-crypt | File encryption | Simple repo encryption | Manual | Free OSS | Low | None |
| dotenv (.env) | Local file | Local development only | Manual | Free | Low | None |
| Secret Type | Recommended Storage | Rotation Frequency | Notes |
|---|---|---|---|
| Database credentials | Vault dynamic secrets / cloud SM | Per-connection or monthly | Prefer dynamic (ephemeral) credentials |
| API keys (third-party) | Cloud secrets manager | Quarterly or on compromise | Store with metadata (expiry, owner, scope) |
| TLS certificates | Vault PKI / cloud certificate manager | 90 days or annually | Automate with cert-manager in K8s |
| SSH keys | Vault SSH secrets engine | Per-session or daily | Signed certificates preferred over static keys |
| Encryption keys | Cloud KMS (not secrets manager) | Annually or per policy | Envelope encryption -- never store KEK with DEK |
| CI/CD tokens | CI/CD built-in secrets + short TTL | Per-pipeline run | Avoid long-lived tokens; use OIDC federation |
| .env files | Local only, never committed | N/A | Add to .gitignore; use secrets manager for production |
START: What is your deployment environment?
├── Single cloud (AWS/GCP/Azure)?
│ ├── YES → Use cloud-native secrets manager (AWS SM, GCP SM, Azure KV)
│ └── NO ↓
├── Multi-cloud or hybrid?
│ ├── YES → Use HashiCorp Vault (self-hosted or HCP)
│ └── NO ↓
├── Kubernetes-only?
│ ├── YES → External Secrets Operator + cloud SM or Vault
│ └── NO ↓
├── Small team, few secrets, GitOps workflow?
│ ├── YES → SOPS + cloud KMS for encryption keys
│ └── NO ↓
├── Local development only?
│ ├── YES → dotenv (.env files in .gitignore) + git-secrets pre-commit hook
│ └── NO ↓
└── DEFAULT → HashiCorp Vault (most flexible, no lock-in)
Prevent secrets from ever reaching version control. Install git-secrets as a pre-commit hook. [src6]
# Install git-secrets
brew install git-secrets # macOS
# Initialize hooks in your repo
cd /path/to/your/repo
git secrets --install
git secrets --register-aws
# Add custom patterns
git secrets --add 'PRIVATE KEY'
git secrets --add 'password\s*=\s*["\047][^"\047]+'
Verify: git secrets --scan → should report any existing secrets in the repo.
Choose based on your infrastructure. Example: HashiCorp Vault dev server for evaluation. [src2]
# Start Vault dev server (evaluation only)
vault server -dev -dev-root-token-id="dev-only-token"
# Store a secret
export VAULT_ADDR='http://127.0.0.1:8200'
export VAULT_TOKEN='dev-only-token'
vault kv put secret/myapp/db username="dbadmin" password="s3cur3-p@ssw0rd"
Verify: vault kv get secret/myapp/db → should display stored key-value pairs.
Replace hardcoded credentials with secrets manager SDK calls. [src1]
import os, hvac # pip install hvac>=2.4.0
client = hvac.Client(
url=os.environ['VAULT_ADDR'],
token=os.environ['VAULT_TOKEN']
)
secret = client.secrets.kv.v2.read_secret_version(
path='myapp/db', mount_point='secret'
)
db_password = secret['data']['data']['password']
Verify: Application starts and connects to the database without credentials in source code.
Configure automatic rotation to limit blast radius of compromised credentials. [src3]
# AWS: Enable rotation with Lambda
aws secretsmanager rotate-secret \
--secret-id myapp/db-credentials \
--rotation-lambda-arn arn:aws:lambda:us-east-1:123456789:function:SecretsRotation \
--rotation-rules '{"AutomaticallyAfterDays": 30}'
Verify: aws secretsmanager describe-secret --secret-id myapp/db-credentials → RotationEnabled: true.
For GitOps workflows, encrypt secrets in-repo with SOPS. [src7]
# Encrypt a secrets file (values only)
sops --encrypt secrets/production.yaml > secrets/production.enc.yaml
# Decrypt at deploy time
sops --decrypt secrets/production.enc.yaml > /tmp/production.yaml
Verify: cat secrets/production.enc.yaml → keys readable, values encrypted.
# Input: VAULT_ADDR and VAULT_TOKEN environment variables
# Output: Database credentials fetched from Vault KV v2
import os
import hvac # pip install hvac>=2.4.0
def get_vault_client():
client = hvac.Client(
url=os.environ['VAULT_ADDR'],
token=os.environ['VAULT_TOKEN']
)
if not client.is_authenticated():
raise RuntimeError("Vault authentication failed")
return client
def get_db_credentials(client, path='myapp/db'):
resp = client.secrets.kv.v2.read_secret_version(
path=path, mount_point='secret'
)
return resp['data']['data']
// Input: AWS credentials (IAM role or env vars) + secret name
// Output: Parsed secret object with database credentials
const { SecretsManagerClient, GetSecretValueCommand }
= require("@aws-sdk/client-secrets-manager"); // ^3.500.0
async function getSecret(secretId, region = "us-east-1") {
const client = new SecretsManagerClient({ region });
const resp = await client.send(
new GetSecretValueCommand({ SecretId: secretId })
);
return JSON.parse(resp.SecretString);
}
# Input: AWS credentials (IAM role or env vars) + secret name
# Output: Parsed secret dict with credentials
import json, boto3 # pip install boto3>=1.34.0
def get_secret(secret_name, region="us-east-1"):
client = boto3.client("secretsmanager", region_name=region)
resp = client.get_secret_value(SecretId=secret_name)
return json.loads(resp["SecretString"])
# Input: A git repository
# Output: Pre-commit hook that blocks secret commits
git secrets --install
git secrets --register-aws
git secrets --add 'PRIVATE KEY'
git secrets --add 'password\s*=\s*["\047][^"\047]+'
# Verify: attempt to commit a test secret
echo "AWS_SECRET=AKIAIOSFODNN7EXAMPLE" > test.txt
git add test.txt && git commit -m "test" # blocked
# BAD -- CWE-798: hardcoded credentials
import psycopg2
conn = psycopg2.connect(
host="prod-db.example.com",
user="admin",
password="SuperSecret123!" # hardcoded
)
# GOOD -- credentials fetched at runtime from Vault
import os, hvac, psycopg2
client = hvac.Client(url=os.environ['VAULT_ADDR'], token=os.environ['VAULT_TOKEN'])
creds = client.secrets.kv.v2.read_secret_version(path='myapp/db')['data']['data']
conn = psycopg2.connect(
host=creds['host'], user=creds['username'], password=creds['password']
)
# BAD -- git history retains the secret forever
echo "API_KEY=sk-live-abc123xyz" > config.env
git add config.env && git commit -m "add config"
# Even git rm config.env doesn't remove from history
# GOOD -- prevent secrets from ever entering the repo
echo "*.env" >> .gitignore
git secrets --install
git secrets --register-aws
# BAD -- secret visible via docker history
FROM node:20-alpine
ENV DATABASE_URL="postgresql://admin:secret@db:5432/myapp"
COPY . /app
CMD ["node", "server.js"]
# GOOD -- no secrets in the image
FROM node:20-alpine
COPY . /app
RUN npm install
# Run with: docker run -e DATABASE_URL="$(vault kv get ...)" myapp
CMD ["node", "server.js"]
# BAD -- visible via /proc/PID/environ
docker run -e DB_PASSWORD="secret123" myapp
# GOOD -- Kubernetes: secrets via sidecar on tmpfs
apiVersion: v1
kind: Pod
spec:
volumes:
- name: secrets
emptyDir:
medium: Memory # tmpfs
containers:
- name: app
volumeMounts:
- name: secrets
mountPath: /mnt/secrets
readOnly: true
.env is committed, removing it from .gitignore does not remove it from git history. Fix: Add .env to .gitignore in the initial commit. If already committed, use BFG Repo-Cleaner, then rotate all exposed secrets. [src4]::add-mask::, GitLab CI masked variables). [src1]secretsmanager:GetSecretValue on * allows any compromised service to read all secrets. Fix: Scope policies to specific secret ARNs. [src3]# Scan repo for hardcoded secrets
git secrets --scan
# Check for high-entropy strings with trufflehog
trufflehog git file://. --only-verified
# Verify Vault connectivity and authentication
vault status
vault token lookup
# List secrets in a Vault KV path
vault kv list secret/myapp/
# Check AWS Secrets Manager rotation status
aws secretsmanager describe-secret --secret-id myapp/db-credentials \
--query '{RotationEnabled: RotationEnabled, LastRotated: LastRotatedDate}'
# Scan Docker image for embedded secrets
docker history --no-trunc myapp:latest | grep -i -E 'password|secret|key|token'
# Verify SOPS encryption
sops --decrypt secrets/production.enc.yaml > /dev/null && echo "OK"
| Tool | Version | Status | Key Change |
|---|---|---|---|
| HashiCorp Vault | 1.20.x | Current (GA June 2025) | Secrets Operator for K8s, IBM RACF support |
| HashiCorp Vault | 1.18.x | LTS | Vault Proxy, Secrets Sync |
| AWS Secrets Manager | Current | GA | Cross-account sharing, Lambda rotation v2 |
| GCP Secret Manager | v1 | GA | Regional replication, IAM conditions |
| Azure Key Vault | Current | GA | RBAC model, soft-delete default |
| SOPS | 3.9.x | Current (CNCF Sandbox) | age encryption support, audit logging |
| hvac (Python) | 2.4.x | Current | Python 3.8+, KV v2 improvements |
| External Secrets Operator | 0.10.x | Current | Multi-provider, PushSecret CRD |
| Use When | Don't Use When | Use Instead |
|---|---|---|
| Any production credentials (DB, API keys, tokens) | Storing non-sensitive config (feature flags, app settings) | Environment variables or config files |
| Multi-service architecture sharing credentials | Single-developer local prototype | .env file with .gitignore |
| Compliance requires audit trail for credential access | Storing user passwords for authentication | Password hashing (bcrypt, argon2) |
| Credentials need automatic rotation | Managing public keys only | Standard key distribution / PKI |
| GitOps workflow needs encrypted secrets in repo | Small static secrets that never change | SOPS may be overkill; consider sealed secrets |
/proc/PID/environ on Linux, in docker inspect, and in CI/CD logs -- they are NOT a secure storage mechanism for production secrets