Safeguard
Best Practices

The complete workflow for finding and remediating hardcoded secrets in GitHub

GitGuardian found 12.8 million secrets leaked on public GitHub in 2023 alone, and over 90% were still valid five days later. Here's the fix workflow that actually closes the gap.

Safeguard Research Team
Research
8 min read

GitGuardian's State of Secrets Sprawl 2024 report scanned roughly 1.1 billion commits pushed to public GitHub in 2023 and found 12.8 million newly exposed secrets — a 28% jump year over year — spread across about 8 million commits that leaked at least one credential. The most striking number in that report isn't the volume, it's the persistence: more than 90% of those exposed secrets were still valid and usable five days after the leak. That gap between "detected" and "actually revoked" is where most breaches originating from source code actually happen. A git push with an AWS key in a config file, a Slack bot token in a test fixture, a Stripe secret key committed "just for a quick local test" — these don't need a sophisticated attacker, just someone running automated scanners against public repos, which happens continuously and at scale. Finding the secret is the easy 10%. The other 90% is a workflow: confirming the credential is live, revoking it at the issuer, rotating it into a secret manager, scrubbing it from Git history without breaking every collaborator's clone, and proving to an auditor it's done. This post walks through that full workflow, tool by tool.

Why isn't deleting the file in a new commit enough?

Deleting the file in a new commit isn't enough because Git never actually deletes anything — it just stops pointing the current tree at that blob. The credential still exists in every prior commit object, still gets pulled down by anyone who runs git clone or git fetch, and is still visible with a one-line git log -p -- path/to/file or by checking out an old SHA directly. Public GitHub repos are also mirrored and cached: GitHub's own network graph, forks, and third-party mirrors like Software Heritage can retain history even after a force-push scrubs the origin. This is precisely why GitHub's documentation and Git's own project maintainers are explicit that "remove and commit" is not a remediation step — it's cosmetic. The only way to remove a secret from a Git repository's actual history is to rewrite every commit that touched it, which changes commit hashes for everything downstream and requires every collaborator to re-clone or hard-reset. That operational cost is exactly why revocation has to happen before history rewriting, not instead of it.

What actually finds hardcoded secrets before they ship?

What actually finds hardcoded secrets before they ship is a layered detection approach, because no single pattern type catches everything. Issuer-specific regex (an AWS key always starts AKIA followed by 16 alphanumeric characters; a GitHub PAT starts ghp_) catches known formats with very low false-positive rates but misses anything novel. Generic patterns — JWTs, PEM-formatted private keys, database connection strings, OAuth refresh tokens — catch a second tier that issuer patterns miss. The third layer is entropy analysis: flagging strings with unusually high randomness that don't match any known format, which is how internally-generated tokens and one-off API keys get caught. GitHub itself ships native secret scanning and push protection, which checks outgoing pushes against partner-provided token formats from over 100 service providers and can block the push outright before the secret ever reaches a public remote — a meaningfully different guarantee than scanning after the fact. Safeguard's secrets engine runs all three pattern layers, plus history and container-layer scanning, and adds a fourth signal: live verification by calling the issuer's own API (for example, AWS sts:GetCallerIdentity or GitHub's /user endpoint) so a finding is marked verified only when the credential is confirmed live, not just format-matched.

Why does a verified finding change what you do next?

A verified finding changes what you do next because it collapses the triage step entirely — you already know the secret is live, so you skip straight to incident response instead of spending analyst time confirming exploitability. A pattern match alone tells you a string that looks like an AWS key exists in a commit; it doesn't tell you whether that key was rotated eighteen months ago and is now dead weight, or whether it's an active credential with production IAM permissions right now. Making a low-privilege, read-only call to the issuing service — checking caller identity, checking token validity, checking balance-read access — answers that question definitively without needing write access or touching production data. This is the difference between a security team drowning in thousands of "possible secret" tickets, most of which are stale or already-revoked noise, and a team that gets a short, accurate list of things that are actually exploitable today. It's also the basis for meaningful severity scoring: a verified, high-privilege credential (a cloud admin key, a root token) is a different incident than an unverified pattern match for a low-scope key, and treating them identically burns down an on-call team's trust in the alerting.

What's the correct order of operations once a live secret is found?

The correct order of operations is revoke, rotate, purge, notify — and it has to happen in that order, not purge-first. Revoke the credential at the issuer immediately (AWS IAM key deactivation, a GitHub PAT revocation, a Stripe key rollover, a Slack token revoke) so it stops working the moment you've confirmed the leak, regardless of what happens to the Git history afterward. Rotate next: generate a new credential, update it in whatever secret manager your services read from — AWS Secrets Manager, HashiCorp Vault, GCP Secret Manager, Azure Key Vault — and confirm the dependent services picked up the new value before you consider the incident closed. Only after the credential is dead and replaced should you touch history, because rewriting Git history is slow, disruptive, and doesn't undo the exposure window that already happened — treating it as step one just delays the part that actually stops the bleeding. Notify last: tell the credential's owner and any team running services that depended on it, and log the timeline for audit purposes. Safeguard runs this exact sequence as a gated playbook, where each step — auto-revoke, auto-rotate, or manual approval — is a policy decision your team controls rather than something that happens silently.

How do you actually rewrite Git history to remove a secret?

You rewrite Git history with a purpose-built tool, not raw git rebase, because manually editing dozens or hundreds of historical commits by hand is error-prone at any nontrivial repo size. git filter-branch, Git's original built-in history-rewriting command, is now explicitly deprecated by the Git project itself, which points users to git filter-repo (maintained by Elijah Newren, distributed at newren/git-filter-repo on GitHub) as the modern replacement — it's dramatically faster on large repos and lets you target specific paths, blobs, or text patterns with path-aware precision. BFG Repo-Cleaner remains popular for simpler jobs — stripping a specific file or a specific string across history — because its command line is shorter (bfg --delete-files id_rsa versus the more verbose filter-repo syntax), but it operates on blobs without full path context, so it can't do path renames or the more surgical rewrites filter-repo supports; a documented BFG-to-filter-repo migration path exists for teams that outgrow it. Either tool requires a force-push afterward, and every collaborator must re-clone or hard-reset rather than pull — a well-documented, unavoidable consequence of the fact that every commit hash downstream of the rewritten one changes. Safeguard's remediation playbook can trigger either tool for the purge step, with the force-push warning surfaced explicitly before anything runs.

How Safeguard Helps

Safeguard treats secrets as one continuously scanned surface rather than a one-time repo audit: current HEAD, full Git history on connect and on demand, container image layers, packaged artifacts (jars, wheels, gems), CI build logs, and even vendor SBOMs and model weights are all covered by the same three-layer detection stack of issuer-specific patterns, generic patterns, and entropy-based matching. Every issuer-pattern finding gets live-verified against the real API — AWS, GitHub, Stripe, Slack, OpenAI, and more — so severity reflects whether a credential is actually exploitable right now, not just whether a regex matched. When a finding is verified, Safeguard's remediation playbook runs revoke, rotate (directly into AWS Secrets Manager, Vault, GCP Secret Manager, or Azure Key Vault), purge from history via git-filter-repo or BFG with the force-push implications called out up front, and notify — each step gated so your team decides what runs headlessly versus what needs sign-off. A pre-push git hook (safeguard install-hook --hook pre-push) catches most of this before it ever leaves a laptop, and the Secrets dashboard tracks time-to-revoke and leaked-and-open counts so "we caught it" and "we actually fixed it" stop being two different questions.

Never miss an update

Weekly insights on software supply chain security, delivered to your inbox.