Secrets & Configuration
Environment variables, secret managers, config as code — how to handle sensitive data without leaking it.
The Configuration Spectrum
From least sensitive to most:
Real-World Analogy
Like keeping your bank PIN vs. your display name — your PIN (secret) is stored securely and never shown, while your display name (config) can be shared openly. Mixing them up is like writing your PIN on a sticky note on your monitor.
// 1. Build-time config (baked into image)
// Feature flags, API URLs, log levels
// Stored in: config files, committed to git
// 2. Runtime config (environment-specific)
// Database host, cache TTL, rate limits
// Stored in: environment variables, config maps
// 3. Secrets (never in code or logs)
// API keys, database passwords, TLS certs
// Stored in: secret manager (AWS Secrets Manager, Vault, etc.) Environment Variables
// The twelve-factor app way: configure via environment
const config = {
port: parseInt(process.env.PORT || "3000"),
databaseUrl: process.env.DATABASE_URL!,
redisUrl: process.env.REDIS_URL || "redis://localhost:6379",
logLevel: process.env.LOG_LEVEL || "info",
nodeEnv: process.env.NODE_ENV || "development",
};
// Validate at startup — fail fast if config is missing
function validateConfig(config: Record<string, unknown>): void {
const required = ["databaseUrl"];
for (const key of required) {
if (!config[key]) {
throw new Error(`Missing required config: ${key}`);
}
}
}
validateConfig(config); Never commit .env files to git. Add .env to .gitignore immediately. Committed secrets stay in git history forever — even if you delete the file, anyone can find it with git log.
Secret Managers
// AWS Secrets Manager / GCP Secret Manager / HashiCorp Vault
// Store secrets centrally, rotate them automatically, audit access
import { SecretsManager } from "@aws-sdk/client-secrets-manager";
const client = new SecretsManager({ region: "us-east-1" });
async function getSecret(name: string): Promise<string> {
const response = await client.getSecretValue({ SecretId: name });
return response.SecretString!;
}
// Load secrets at startup
const dbPassword = await getSecret("prod/database/password");
const stripeKey = await getSecret("prod/stripe/api-key");
// Benefits over env vars:
// - Automatic rotation (e.g., rotate DB password every 30 days)
// - Audit log (who accessed which secret, when)
// - Fine-grained access control (IAM policies)
// - Encryption at rest Kubernetes Secrets
# Create a secret
apiVersion: v1
kind: Secret
metadata:
name: db-credentials
type: Opaque
data:
username: cG9zdGdyZXM= # base64 encoded (NOT encrypted!)
password: c3VwZXJzZWNyZXQ=
---
# Use in a pod
spec:
containers:
- name: api
env:
- name: DB_USERNAME
valueFrom:
secretKeyRef:
name: db-credentials
key: username
- name: DB_PASSWORD
valueFrom:
secretKeyRef:
name: db-credentials
key: password Kubernetes Secrets are base64-encoded, not encrypted. Anyone with cluster access can read them. For real secret management, use an external secret manager (Vault, AWS Secrets Manager) with a Kubernetes operator like External Secrets.
Secret Rotation
// Secrets should be rotated regularly
// The rotation pattern:
// 1. Generate new secret
// 2. Update the application to accept both old and new
// 3. Switch to new secret
// 4. Revoke old secret
// Database password rotation (dual-password approach):
async function rotateDbPassword(): Promise<void> {
const newPassword = generateSecurePassword();
// 1. Set new password (old still works)
await db.execute(`ALTER USER app_user SET PASSWORD = '${newPassword}'`);
// 2. Update secret manager
await secretManager.updateSecret("prod/db/password", newPassword);
// 3. App picks up new password on next connection pool refresh
// (or trigger a rolling restart)
} Key Takeaways
- Never commit secrets to git — use
.envfor local dev, secret managers for production - Validate config at startup — fail fast if required values are missing
- Use a secret manager for production — env vars don’t provide rotation, auditing, or encryption
- Rotate secrets regularly — automate it so it’s painless