Secrets Management Patterns
Environment Variables Best Practices
# .env — local dev only, NEVER commit
DATABASE_URL=postgresql://user:pass@localhost/mydb
JWT_SECRET=change-me-in-production
# .env.example — commit this (no real values)
DATABASE_URL=postgresql://user:password@localhost/dbname
JWT_SECRET=generate-with-openssl-rand-hex-32
# .gitignore
.env
.env.local
.env.*.local
*.pem
*_rsa
*_dsa
secrets/
# Validate required secrets at startup — fail fast
import os
REQUIRED_SECRETS = ["DATABASE_URL", "JWT_SECRET", "REDIS_URL", "STRIPE_KEY"]
def validate_secrets():
missing = [s for s in REQUIRED_SECRETS if not os.environ.get(s)]
if missing:
raise RuntimeError(f"Missing required secrets: {', '.join(missing)}")
validate_secrets() # called at module load
HashiCorp Vault
import hvac
client = hvac.Client(url="https://vault.internal:8200")
# AppRole authentication (for services)
client.auth.approle.login(
role_id=os.environ["VAULT_ROLE_ID"],
secret_id=os.environ["VAULT_SECRET_ID"]
)
# Read secret
secret = client.secrets.kv.v2.read_secret_version(
path="myapp/database",
mount_point="secret"
)
db_password = secret['data']['data']['password']
# Dynamic secrets — Vault generates short-lived DB credentials
db_creds = client.secrets.database.generate_credentials(name="myapp-role")
# db_creds['data']['username'], db_creds['data']['password']
# Automatically revoked after TTL (e.g., 1 hour)
# Token renewal
client.auth.token.renew_self()
AWS Secrets Manager
import boto3, json
from functools import lru_cache
secrets_client = boto3.client('secretsmanager', region_name='us-east-1')
@lru_cache(maxsize=None)
def get_secret(secret_name: str) -> dict:
response = secrets_client.get_secret_value(SecretId=secret_name)
return json.loads(response['SecretString'])
# Usage
db_config = get_secret("prod/myapp/database")
db_url = f"postgresql://{db_config['username']}:{db_config['password']}@{db_config['host']}/{db_config['dbname']}"
# Rotation — Lambda function triggered automatically
def rotate_secret(event, context):
step = event['Step']
secret_id = event['SecretId']
token = event['ClientRequestToken']
if step == 'createSecret':
# Generate new credentials
new_password = secrets.token_urlsafe(32)
secrets_client.put_secret_value(
SecretId=secret_id,
ClientRequestToken=token,
SecretString=json.dumps({**current, 'password': new_password}),
VersionStages=['AWSPENDING']
)
elif step == 'testSecret':
# Test AWSPENDING version
pass
elif step == 'finishSecret':
secrets_client.update_secret_version_stage(...)
Docker / Kubernetes Secrets
# Kubernetes Secret
apiVersion: v1
kind: Secret
metadata:
name: app-secrets
type: Opaque
stringData:
database-url: postgresql://...
jwt-secret: changeme
# Use in Pod
envFrom:
- secretRef:
name: app-secrets
# External Secrets Operator (sync from Vault/AWS SM)
apiVersion: external-secrets.io/v1beta1
kind: ExternalSecret
metadata:
name: app-secrets
spec:
refreshInterval: 1h
secretStoreRef:
name: vault-backend
kind: ClusterSecretStore
target:
name: app-secrets
data:
- secretKey: database-url
remoteRef:
key: secret/myapp/database
property: url
Secret Scanning
# Pre-commit hook with gitleaks
gitleaks protect --staged
# .gitleaks.toml to customize patterns
# truffleHog — scan git history
trufflehog git file://. --since-commit HEAD --only-verified
# GitHub secret scanning — enable in repository settings
# detect-secrets baseline
detect-secrets scan > .secrets.baseline
detect-secrets audit .secrets.baseline
# Pre-commit config
# - repo: https://github.com/Yelp/detect-secrets
# hooks:
# - id: detect-secrets
# args: ['--baseline', '.secrets.baseline']
Rotation Checklist