API keys, database passwords, signing keys and OAuth client secrets turn a minor bug into a full breach. Public repositories are scanned continuously, and a key committed for thirty seconds has been harvested. Secrets management keeps these values out of code, logs and images, and makes them replaceable at short notice.
In this lesson you will learn where secrets should and should not live, how to load them safely, how to catch leaks before they reach a remote, and how to rotate credentials without downtime.
| Location | Why it is a problem |
|---|---|
| Source code and Git history | Anyone with repository access has it forever, even after "removal" |
| .env files committed or emailed | Same as source; also spreads to laptops and chat logs |
| Container images | docker history and layer inspection expose build-time secrets |
| Client-side JavaScript or mobile apps | Anything shipped to users is public |
| Log lines, crash reports, CI job output | Widely readable and retained for months |
The rule: a secret exists only in a secrets store and, briefly, in the memory of the process that needs it.
Environment variables are the standard hand-off between a secrets store and a process, and they are fine when used carefully. The sample config.js centralises them: validated once at startup, frozen, and imported everywhere else so no module reads process.env on its own. Add these habits:
# .gitignore: local development files never enter the repository
.env
.env.*
!.env.example # a template with placeholder values is safe to commitKeep .env.example with fake values so newcomers know which variables exist. In production do not use .env files at all; the platform injects variables from the secrets store. For the most sensitive keys, prefer fetching from the store at startup, since environment variables are visible to every process running as the same user.
A secrets manager (AWS Secrets Manager, Google Secret Manager, Azure Key Vault, HashiCorp Vault) provides encrypted storage, access control per secret, an audit trail of every read, and versioning. The application authenticates to the store with an identity it already has (an IAM role, a Kubernetes service account) rather than with yet another static key:
const { SecretsManagerClient, GetSecretValueCommand } = require("@aws-sdk/client-secrets-manager");
const client = new SecretsManagerClient({}); // credentials come from the instance role
const { SecretString } = await client.send(new GetSecretValueCommand({ SecretId: "shop/prod/database" }));
const dbConfig = JSON.parse(SecretString);Better than storing long-lived passwords is not having them: dynamic database credentials that expire after an hour, OIDC tokens for CI, and cloud roles instead of access keys remove the secret that could leak.
Prevention is cheap; revocation after a public leak is not.
npm install --save-dev gitleaks-cli # or install the gitleaks binary
npx gitleaks git --pre-commit # hook: refuse commits that contain key-like strings
gitleaks git --log-opts="--all" # scan full history when auditing an existing repositoryRun the same scanner in CI, enable your Git host's push protection, and treat any hit as a real leak: rotate the credential even if the commit was never pushed.
Every secret should have an owner, a maximum age and a rotation procedure. The procedure that avoids outages is dual-key rotation: create the new credential, deploy so the application accepts both, switch traffic to the new one, verify, then revoke the old one. For signing keys this is the kid pattern from the JWT lesson. Practise rotation on a schedule so the emergency version after a suspected leak is routine rather than an experiment.
A developer removed an API key from the repository in a follow-up commit. What must still happen?
.env.example, never .env.Next lesson: Rate Limiting and DoS Resilience — keeping the service available when traffic is hostile or simply excessive.