JSTGTECH
← Back to blog

Leaked credentials in public repos get used in minutes

4 min read

GitGuardian’s 2026 State of Secrets Sprawl report counted 28.65 million new hardcoded secrets pushed to public GitHub commits in 2025 alone — a 34% jump year over year and the largest single-year increase they’ve recorded (GitGuardian). That’s not a backlog of old mistakes; it’s this year’s output. And the same report found that 64% of secrets that leaked back in 2022 are *still valid* today (GitGuardian). Meanwhile Unit 42’s tracking of the EleKtra-Leak campaign found attackers detecting and using exposed AWS IAM credentials within roughly five minutes of them landing on GitHub (Unit 42). Committing a secret to a public repo isn’t a “someone might find this eventually” risk. It’s closer to broadcasting it live.

Root cause

Almost none of this is malicious. It’s .env files added to a repo before anyone wrote a .gitignore entry for them, database URLs hardcoded into a config “just for local testing,” service-account JSON dropped into a test fixture, or a CI job that echoes an environment variable into build logs that are themselves public. GitGuardian’s report also flags AI-assisted coding as a growing contributor — commits with AI-generated code leak secrets at roughly double the baseline rate, and secrets tied to AI services specifically were up 81% year over year (GitGuardian), which tracks: an assistant that’s never seen your .gitignore will happily paste a working API key straight into a code sample.

The part engineers consistently underestimate is that deleting the secret in a follow-up commit doesn’t delete it from the repo. Git preserves history — the credential is still sitting in an earlier commit object, reachable by anyone who clones the repo or even just browses the commit log on GitHub. Force-pushing a rewritten history to your own branch doesn’t help either if the repo was ever public even briefly: forks, local clones, and GitHub’s own caches can retain the blob indefinitely. The only thing that actually neutralizes a leaked secret is invalidating the credential itself — history rewrites are cleanup, not remediation.

Blast radius

This is the part that should change how you triage. Automated scanners watch GitHub’s public event stream continuously, and multiple independent research efforts have clocked exploitation in the single-digit minutes: Unit 42 measured attackers weaponizing exposed IAM keys from the EleKtra-Leak campaign in about five minutes (Unit 42), and independent write-ups of live incidents describe leaked AWS keys being grabbed and turned into GPU cryptomining fleets within 4 to 11 minutes of the push, run across every region the credential could reach (TechRadar). The economics are simple: spinning up GPU instances for Monero mining costs the attacker nothing (it’s your bill) and the scanning infrastructure is fully automated, so there’s no human in the loop slowing things down. There is no “we’ll rotate it during the next sprint” window here — by the time a human notices the commit, the credential has often already been used.

Remediation

The controls that matter, roughly in order of leverage:

  • Push protection. GitHub’s secret scanning can block a push before a known-format secret ever lands in the repo, and its partner program automatically notifies the issuing provider (AWS, Stripe, and ~150 others) when a valid secret is detected in a public repo, sometimes triggering revocation faster than the committer even sees the alert (GitHub Docs). Turn this on org-wide, not per-repo.
  • Pre-commit scanning. Tools like gitleaks or trufflehog run locally before a commit is even made, catching secrets in formats GitHub doesn’t recognize (internal API keys, custom tokens) and catching them before they touch history at all, which is strictly better than catching them after.
  • Treat any committed secret as compromised, full stop. Don’t reach for git filter-branch or a force-push as the first response — rotate the credential at the source first. A scrubbed history with a still-valid key behind it protects nobody; an invalidated key with a messy history is a Tuesday.
  • Shrink the blast radius with short-lived credentials. The strongest structural fix is having fewer long-lived secrets to leak in the first place. This site’s own CI/CD (see the GitHub Actions OIDC to AWS tutorial) uses OpenID Connect federation instead of storing AWS access keys as repo secrets — GitHub Actions requests a short-lived, auto-expiring token scoped to a specific IAM role for each run, so there’s no long-lived AWS key sitting in secrets storage to leak in the first place. Where OIDC isn’t an option, aggressive credential expiry and scoped-down IAM policies get you most of the same benefit.

The bigger lesson

Prevention controls — push protection, pre-commit hooks, .gitignore discipline — are worth having, but GitGuardian’s own numbers show they aren’t closing the gap: secrets leaked have grown 152% since 2021 against a 98% growth in GitHub’s developer base (GitGuardian), meaning the leak rate is outpacing the population producing the leaks. Some secret will eventually get past whatever scanner you’ve configured, because prevention is a filter and filters have gaps by definition. What actually determines whether that leak becomes an incident report or a non-event is how fast you can rotate the credential and how little damage it can do in the window before you notice. That’s why the real investment isn’t just “stop secrets from leaking” — it’s “assume one will leak, and make sure it’s short-lived, narrowly scoped, and rotated before a bot ever gets a chance to use it.”

Related posts