Skip to content
Paul Marinos
Menu

Secrets in Source Control

Why a committed secret is burned the moment it lands and deletion is not remediation — the git-history problem, rotation as the only real fix, and the scanning and push protection that enforce the discipline.

A secret committed to source control is endemic and high-impact, and it is the first of the two trusted-supply failures — the risk is in what you shipped, not in a request. A key in the repository is compromised the moment it reaches somewhere anyone else can read, and the single most important thing to understand is that removing it later does not help.

API_KEY = "sk_live_a1b2c3d4e5f6" # committed once, in the history forever

Delete that line in the next commit and the key is still in the repository’s history, recoverable by anyone with a clone via git log -p or a checkout of the old commit. On a public repository it is worse than that: automated scrapers watch the public event firehose and pull new secrets within minutes of the push, so a key exposed and “removed” ten minutes later should be assumed harvested.

The consequences follow from that one fact:

  • Assume any committed secret is burned. The question is how fast to rotate, not whether to.
  • Rotate, do not just delete. Rotation — issuing a new credential and revoking the old — is the remediation. Removing the line addresses the symptom, not the exposure.
  • Purging history is cleanup, not a fix. Rewriting history (BFG, git filter-repo) removes the artifact but does nothing about the window in which it was public. Rotate first, purge second.

The durable answer is that the secret never enters the code:

  • Environment injection or a secret manager: the application reads the secret from the environment or fetches it at runtime from a real store — Vault, Secrets Manager, Key Vault — so there is nothing to commit. That page covers the bootstrap-identity problem this creates and how federation solves it.
  • Kill the reason to hardcode. Where the platform can supply an identity — workload identity, attached roles — there is no static secret to place at all, which removes the failure mode rather than guarding it.
  • .gitignore is not a control. It stops the obvious .env commit and nothing else; a developer who adds a key to a tracked config file sails past it.

Enforce it, because discipline fails eventually

Section titled “Enforce it, because discipline fails eventually”

Individual vigilance does not scale across a team and a few years, so the control has to be in the pipeline:

  • Secret scanning with push protection — pre-commit and pre-receive scanning that blocks the commit before the secret ever lands, which is far cheaper than detecting it after. Push protection is the important half: detection-after-push still requires rotation.
  • Entropy and pattern detection catches the generic key that no fixed rule anticipated, alongside the known-format detectors for provider tokens.

A leaked-then-used key is often the initial-access event in an incident timeline, which is why this small mistake carries outsized weight — the detection view treats credential use from an unexpected place as a high-value signal precisely because so many intrusions start exactly here. The enforcement lives in CI/CD, the correct storage is a secrets architecture question, and as a trusted-supply failure it is the sibling of dependency risk in the subsection’s structure.

Graph View

Last updated:

Spotted an error on this page? Report it.