TL;DR
The #1 secrets management best practice is to never commit secrets to git. Use environment variables or secret managers (AWS Secrets Manager, HashiCorp Vault). Rotate secrets regularly and automatically. Scan code and commits for leaked secrets. Use different secrets per environment.
"A leaked secret is a permanent secret. The moment it hits version control, assume it's compromised and rotate immediately."
Best Practice 1: Never Hardcode Secrets 2 min
Secrets in code get leaked through git history, logs, and error messages:
// WRONG: Hardcoded secrets
const stripe = new Stripe('sk_live_abc123xyz789');
const db = new Database('postgres://admin:password@db.example.com');
// CORRECT: Environment variables
const stripe = new Stripe(process.env.STRIPE_SECRET_KEY);
const db = new Database(process.env.DATABASE_URL);
// BETTER: Secret manager with validation
function getRequiredSecret(name) {
const value = process.env[name];
if (!value) {
throw new Error(`Missing required secret: ${name}`);
}
return value;
}
const stripe = new Stripe(getRequiredSecret('STRIPE_SECRET_KEY'));
Best Practice 2: Use Git Hooks to Prevent Leaks 5 min
Stop secrets before they enter version control:
# Environment files
.env
.env.local
.env.*.local
.env.production
# Key files
*.pem
*.key
*.p12
*.pfx
# IDE secrets
.idea/
.vscode/settings.json
# Cloud credentials
credentials.json
service-account.json
.aws/credentials
# Install gitleaks
brew install gitleaks # macOS
# or download from github.com/gitleaks/gitleaks
# .pre-commit-config.yaml
repos:
- repo: https://github.com/gitleaks/gitleaks
rev: v8.18.0
hooks:
- id: gitleaks
# Run manually
gitleaks detect --source . --verbose
# Scan git history
gitleaks detect --source . --log-opts="--all"
Best Practice 3: Use a Secret Manager 10 min
Environment variables are good; secret managers are better:
| Solution | Pros | Cons | Best For |
|---|---|---|---|
| AWS Secrets Manager | Rotation, audit, IAM | AWS lock-in | AWS deployments |
| HashiCorp Vault | Full-featured, multi-cloud | Complex setup | Enterprise, multi-cloud |
| GCP Secret Manager | GCP integration | GCP lock-in | GCP deployments |
| Doppler/Infisical | Dev-friendly, sync | Third-party SaaS | Startups, small teams |
import { SecretsManagerClient, GetSecretValueCommand } from '@aws-sdk/client-secrets-manager';
const client = new SecretsManagerClient({ region: 'us-east-1' });
async function getSecret(secretName) {
const command = new GetSecretValueCommand({ SecretId: secretName });
const response = await client.send(command);
if (response.SecretString) {
return JSON.parse(response.SecretString);
}
throw new Error('Secret not found');
}
// Usage
const dbCreds = await getSecret('prod/database');
const db = new Database({
host: dbCreds.host,
user: dbCreds.username,
password: dbCreds.password,
});
Best Practice 4: Rotate Secrets Regularly 10 min
Automated rotation limits exposure from compromised secrets:
// Terraform: Enable automatic rotation
resource "aws_secretsmanager_secret_rotation" "db_password" {
secret_id = aws_secretsmanager_secret.db.id
rotation_lambda_arn = aws_lambda_function.rotate_db.arn
rotation_rules {
automatically_after_days = 30
}
}
// Application: Handle rotation gracefully
class DatabasePool {
constructor() {
this.pool = null;
this.lastCredentialFetch = 0;
this.credentialTTL = 5 * 60 * 1000; // 5 minutes
}
async getConnection() {
if (this.shouldRefreshCredentials()) {
await this.refreshPool();
}
return this.pool.getConnection();
}
shouldRefreshCredentials() {
return Date.now() - this.lastCredentialFetch > this.credentialTTL;
}
async refreshPool() {
const creds = await getSecret('prod/database');
// Gracefully transition to new credentials
const oldPool = this.pool;
this.pool = createPool(creds);
this.lastCredentialFetch = Date.now();
if (oldPool) oldPool.end();
}
}
Best Practice 5: Environment Separation 5 min
Use different secrets for each environment:
- Separate secrets per environment (dev, staging, prod)
- Use prefixes or namespaces (prod/stripe, staging/stripe)
- Restrict production secret access
- Use less privileged keys in development
- Never copy production secrets to development
// Secret naming convention
const secretName = `${process.env.ENVIRONMENT}/api-keys`;
// Results in: dev/api-keys, staging/api-keys, prod/api-keys
// IAM policy restricts access
{
"Effect": "Allow",
"Action": "secretsmanager:GetSecretValue",
"Resource": "arn:aws:secretsmanager:*:*:secret:prod/*",
"Condition": {
"StringEquals": {
"aws:PrincipalTag/Environment": "production"
}
}
}
Best Practice 6: Audit and Monitor Secret Access 5 min
Track who accesses secrets and when:
// CloudWatch alarm for unusual secret access
{
"AlarmName": "UnusualSecretAccess",
"MetricName": "SecretAccess",
"Threshold": 100,
"EvaluationPeriods": 1,
"Period": 300,
"Statistic": "Sum",
"ComparisonOperator": "GreaterThanThreshold"
}
// Log secret access in application
async function getSecretWithAudit(secretName, reason) {
logger.info('secret.access', {
secretName,
reason,
requestedBy: getCurrentUser(),
timestamp: new Date().toISOString(),
});
return getSecret(secretName);
}
Emergency Response: If a secret is leaked, immediately rotate it. Then investigate how it was exposed, scan for unauthorized use, and implement controls to prevent recurrence. Have a documented incident response plan for secret leaks.
External Resources: For comprehensive secrets management guidance, see the OWASP Secrets Management Cheat Sheet and the Cryptographic Storage Cheat Sheet . These resources provide industry-standard security recommendations for protecting sensitive credentials.
What if I accidentally committed a secret?
Immediately rotate the secret (generate a new one and revoke the old). Then remove from git history using git filter-branch or BFG Repo-Cleaner. Treat the old secret as permanently compromised.
Should I encrypt secrets in environment variables?
Environment variables are typically visible to the process and its children. For high-security environments, use a secret manager that decrypts at runtime. For most applications, restricting access to the environment is sufficient.
How do I handle secrets in CI/CD?
Use your CI/CD platform's secret management (GitHub Secrets, GitLab CI Variables). Inject secrets at runtime, never store in config files. Use OIDC to assume roles instead of long-lived credentials when possible.