One of the most common and most damaging mistakes in software is committing a secret — an API key, a database password, a token — into source control. It feels harmless in the moment and becomes a serious exposure the instant that repository is shared, made public, or breached. Automated scanners crawl code hosts constantly looking for exactly these strings.
The fix is a small shift in habit: code should never contain its own secrets or its environment-specific settings. Those come from outside, at run time, through configuration.
Configuration lives outside the code
The standard mechanism is the environment variable: a named value the running process reads from its environment rather than from the source. The same build then behaves correctly in development, staging and production simply because each environment supplies different values — different database URLs, different keys, different feature flags. The artifact stays identical; only the configuration changes.
This separation is not only about secrets. Anything that differs between environments — endpoints, limits, toggles — belongs in configuration, not baked into the code. The result is one artifact you can promote anywhere, and code that makes no assumptions about which environment it is in.
Keeping secrets out of the repository
For local development, a dotenv file holds your values and is listed in the ignore file so it never gets committed — with a checked-in example file that lists the names but not the values, so a new teammate knows what to provide. In deployed environments, secrets come from the platform's own secret store or CI secret settings, injected as environment variables at run time and never written into the repo.
The principle is consistent everywhere: the names of your settings can live in the codebase; the values, especially secret ones, must not. If you can read a real key by browsing the source, so can everyone else who ever sees it.
When a secret leaks anyway
If a key does land in a commit, treat it as compromised the moment it was pushed — do not just delete the line. Rotate it: generate a new key at the provider and revoke the old one, so the exposed value stops working. Removing it from the latest commit is not enough, because it still lives in the repository's history and in any clone.
Rotation is the real remediation; scrubbing history is secondary cleanup. Build the habit of rotating first and cleaning second, and add a secret scanner to your pipeline so the next near-miss is caught before it is ever pushed. Prevention is cheaper than every response.
Why a leaked .env file is worse than it looks at first glance
A `.env` file accidentally committed to version control is not just exposed to anyone with read access to the repository going forward — it remains in the repository's full history indefinitely, readable by anyone who clones it, even after the file is later deleted from the current working tree, because deleting a file in a new commit does nothing to remove it from the commits that came before. This is precisely why a leaked secret cannot be considered contained simply by deleting the offending file and committing that deletion; the actual, only reliable fix is rotating the leaked credential itself, treating the old value as permanently compromised the moment it entered version control history, regardless of whatever cleanup happens to the repository afterward.
Rewriting history versus rotating the credential: two different responses
Tools like `git filter-repo` or the BFG Repo-Cleaner can scrub a specific secret from every commit in a repository's history, which genuinely removes it from future clones — but this only helps if it happens before anyone else has already cloned or fetched the compromised history, since rewriting history does nothing to retroactively un-expose a copy that already left the original repository. This is exactly why rotating the actual credential is the fast, first, and non-negotiable response to any secret leak, while history rewriting is, at best, a secondary cleanup step taken afterward — treating history rewriting as sufficient on its own, without rotating the credential, leaves the leaked value fully valid and usable by anyone who already has a copy of the old history.
Secret scanning: catching the leak before the commit lands, not after
Pre-commit hooks and CI-integrated secret scanners exist specifically to catch a credential-shaped string before it is ever committed at all, pattern-matching against known formats for API keys, tokens, and connection strings from major providers, and either blocking the commit outright or flagging it loudly enough that it cannot be missed. This is a meaningfully better position than any of the recovery options discussed above, all of which activate only after a leak has already occurred — a scanner that runs on every commit attempt, rather than being a one-off manual review a team remembers to run occasionally, is what actually prevents the leak from happening in the first place rather than merely shortening how long it takes to notice and respond to one after the fact.
Separating secrets from ordinary configuration, deliberately
It is common practice to lump every environment variable together in one `.env` file, but genuine secrets — API keys, database passwords, signing keys — deserve materially different handling than ordinary, non-sensitive configuration like a feature flag or a log level: secrets warrant a dedicated secrets manager with access logging and rotation, discussed at more length elsewhere in this cluster of articles in the context of microservices, while non-sensitive config can reasonably live in a plain, even version-controlled configuration file. Conflating the two into one undifferentiated pile of environment variables makes it harder to apply the stricter handling secrets actually need, since nothing about a flat list of `KEY=value` pairs visually distinguishes which entries are genuinely sensitive from which ones would be harmless even if exposed.
Least privilege for secrets, applied per environment
A single, powerful credential shared across development, staging, and production is a bigger liability than the same credential scoped narrowly to just the one environment where it is actually needed, because a leak of a shared credential compromises every environment at once rather than just the one where the leak actually occurred — issuing separate, narrowly scoped credentials per environment, even when it takes more upfront setup than sharing one, contains the blast radius of any single leak to the one environment it happened in, which is the same least-privilege principle already discussed elsewhere in this cluster of articles in the context of microservices secrets management, applied here at the level of environments rather than services.
What to actually do in the first ten minutes after discovering a leaked key
The practical, ordered response to discovering a leaked credential: rotate it immediately at the provider, before doing anything else, since every minute the old value remains valid is a minute it remains exploitable regardless of what cleanup happens afterward; check the provider's own access or usage logs for any activity during the exposure window that was not the team's own; only then consider whether the exposed history is worth rewriting, understanding the limits on that discussed earlier; and finally, add the specific pattern that leaked to an automated secret scanner's rules if it was not already caught, so the exact same class of leak cannot recur silently the same way again.
Why CI secrets deserve the same scrutiny as production ones
It is common to treat CI-pipeline credentials — a deploy key, a package registry token — as lower-stakes than production secrets simply because they live in build tooling rather than the running application, but a compromised CI credential frequently has write access to the exact same production infrastructure a compromised production secret would, sometimes with even broader reach across every repository and environment the pipeline touches, which is exactly why CI secrets deserve the same rotation discipline, least-privilege scoping, and leak-scanning coverage as any credential embedded directly in the running application.
Why encrypting secrets at rest in the repository is not a full substitute
Tools like SOPS or git-crypt let encrypted secrets be safely committed directly to version control, decrypted only at deploy time by whichever systems hold the actual decryption key — a genuinely useful pattern for keeping secrets versioned alongside the code that depends on them, but it is not a substitute for a dedicated secrets manager's access logging and fine-grained per-service permissions discussed elsewhere in this cluster of articles, since anyone with both repository access and the shared decryption key can decrypt every secret in the repository, with no record of which secret was actually accessed by whom or when.
The blast radius of a single overly broad cloud credential
A cloud provider credential granted broad, account-wide permissions rather than scoped narrowly to exactly what a specific application needs turns any leak of that one credential into a leak of essentially everything the account can do, which is precisely why scoping every credential to the minimum permission set its actual use case requires — a deploy key that can only deploy, a storage credential that can only read one specific bucket — is worth the extra upfront configuration effort, since it directly determines how much damage any single future leak, however it eventually happens, is actually capable of causing.
Logging is a leak vector too, not just source control
A surprisingly common secondary leak path is an application logging its own full configuration or environment at startup for debugging purposes, which silently ships every secret straight into the logging pipeline discussed at length elsewhere in this library — any log aggregator, and everyone with access to it, becomes a second place a leaked credential can be read from, entirely independent of whether source control itself was ever touched at all, which is exactly why redacting known-sensitive environment variable names before logging configuration, rather than logging it wholesale, matters as a distinct discipline from source-control hygiene.
Why a `.gitignore` entry alone is not a guarantee, only a default
Adding `.env` to `.gitignore` prevents it from being tracked going forward, but it does nothing to protect a file that was already committed before the ignore rule was added, and it offers zero protection against a `git add -A` or `git add .` run from a directory where the ignore rule was somehow misconfigured or bypassed — which is exactly why the secret-scanning discipline discussed earlier matters as a second, independent layer rather than treating a `.gitignore` entry alone as a sufficient guarantee that a given file will never end up in the repository.
Why a compromised CI runner is a broader risk than a compromised laptop
A CI pipeline that injects production secrets to run its deploy step typically has those secrets available to every step and every script in that same pipeline run, which means a supply-chain compromise in any dependency the pipeline installs — a malicious package hijacking the build — can potentially exfiltrate those same secrets, a risk considerably harder to contain than a single developer's compromised laptop, since a CI pipeline's blast radius extends to every commit and every dependency it processes rather than to one person's own local environment.