Secrets

Keep keys out of the repository, out of the logs, and rotatable on a bad morning.

08of 16
≈ 12Min read
5Parts

Where a secret lives

PlaceVerdict
in the sourcenever — it is in the history, in every clone, and on every laptop
in a .env file, git-ignoredfine for local development
in the platform’s environment variablesthe normal answer for a small service
in a secret manager (Vault, AWS/GCP secret manager)the answer once there is more than one service
in the browserthere are no secrets in the browser — see below
A key in front-end code is a public key

Bundled, minified and obfuscated are all the same thing: the user can read it. If a third-party API needs a secret key, the call belongs on your server. A "publishable" key — Stripe’s, a maps key — is designed for this and is restricted by origin on their side.

Configuration that refuses to start

A service that starts with SESSION_SECRET undefined signs cookies with "undefined". Checking at boot turns a silent security failure into a deployment that does not go out — which is the outcome you want.

Commit a .env.example with every key name and no values, so the list is visible and a new machine fails with a readable message.

fail loudly at boot, not quietly at 3 a.m.
const REQUIRED = ["DATABASE_URL", "SESSION_SECRET", "MAIL_API_KEY"];

const missing = REQUIRED.filter((name) => !process.env[name]);

if (missing.length) {
  throw new Error(`Missing configuration: ${missing.join(", ")}`);
}

Secrets in logs, errors and URLs

Redact by key name, on the way into the logger, not at the call site — a denylist applied once, centrally, catches the console.log somebody adds next year. And never put a token, a password reset code or a personal identifier in a query string: a POST body or a header is invisible to every log on the path.

the three places they escape
console.log("request", req.headers);        // Authorization, Cookie
console.error(err);                         // the connection string in the message
fetch(`/reset?token=${token}`);             // the URL is in server logs, in the Referer,
                                            // in browser history, and in analytics

The morning after a leak

  • Rotate first. The key is compromised from the moment it was pushed, whatever the repository’s visibility. Deleting the commit does not reach the clones, the forks, the CI caches or the scrapers that read every public push within seconds.
  • Then revoke the old one, and check what it did while it was out — the provider’s audit log is the point of that log.
  • Then clean the history if you like, with git filter-repo or the platform’s tool, knowing it does not help with step one.
  • Then make it harder: a pre-commit secret scanner, and the platform’s push protection.

Rotation as a routine

A key that has never been rotated cannot be rotated in a hurry. Support two valid keys at once — the old and the new — so a rotation is: add the new one, deploy, switch, remove the old one. The same pattern as the expand-and-contract migration in the git course, and the reason a rotation becomes a fifteen-minute job instead of an outage.

Practice