TL;DR
The #1 secrets management best practice is to never commit secrets to git. Everything else follows from that. Keep values in environment variables, or in a secret manager like AWS Secrets Manager or HashiCorp Vault if you want rotation and an audit trail. Automate rotation, scan your commits, and keep production credentials out of development.
"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
A secret in code doesn't stay in code. It ends up in git history, in log lines, and in the stack trace some framework helpfully prints to a public error page:
// 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
The cheapest fix in this whole article is a hook that refuses the commit:
# 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 fine. A secret manager buys you two things they can't: rotation without a redeploy, and a record of who read what.
| 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
Rotation is how you put an expiry date on a leak you have not noticed yet:
// 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
Separate credentials per environment, and mean it:
- Namespace by environment (
prod/stripe,staging/stripe,dev/stripe) - Restrict who can read the production namespace
- Use test-mode or reduced-scope keys in development
- Never copy a production secret into a local
.env, not even once
// 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
You want to know who read a secret, and when. That log is what turns a suspicion into an answer:
// 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: Rotate first. Investigate second. Once the old credential is dead you can take your time working out how it escaped and whether anyone used it, and that ordering is the whole plan. Write it down somewhere your future panicking self will find it.
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?
Rotate it now, before anything else. Generate a new one, revoke the old one, then scrub git history with git filter-branch or BFG Repo-Cleaner. Treat the old secret as permanently burned, because forks and clones keep a copy you can't reach.
Should I encrypt secrets in environment variables?
Environment variables are readable by the process and everything it spawns, so encrypting the value at rest buys you less than it sounds. If that matters, use a secret manager that decrypts at runtime. For most apps, controlling who can reach the environment is enough.
How do I handle secrets in CI/CD?
Use the platform's own secret store, such as GitHub Secrets or GitLab CI Variables. Inject at runtime and keep them out of config files entirely. Where the provider supports OIDC, assume a role instead of storing a long-lived credential.
Further Reading
Put these practices into action with our step-by-step guides.
Scan for Exposed Secrets
Check your codebase for leaked API keys and credentials.