Skip to content
Truffle says

Truffle says

trufflesecurity.com • September 30, 2026

tl;dr We scanned 224 million public GitHub repositories, a snapshot of public code assembled to train AI models (The Stack v3), and found 543,699 unique credentials that still authenticated when we tested them in July 2026. The median one had been sitting in a public default branch for 784 days. The oldest was committed in 2009 and still works. Just under 200,000 of them were pushed after GitHub turned push protection on by default. The block does work where it applies, roughly halving the rate at which the credentials it recognises reach public code, but 51.8 percent of what is still live is a shape it does not recognise, and nothing it helps the half million already there.

tl;dr We scanned 224 million public GitHub repositories, a snapshot of public code assembled to train AI models (The Stack v3), and found 543,699 unique credentials that still authenticated when we tested them in July 2026. The median one had been sitting in a public default branch for 784 days. The oldest was committed in 2009 and still works. Just under 200,000 of them were pushed after GitHub turned push protection on by default. The block does work where it applies, roughly halving the rate at which the credentials it recognises reach public code, but 51.8 percent of what is still live is a shape it does not recognise, and nothing it helps the half million already there.

We scanned a snapshot of 224 million public GitHub repositories that was put together for training large language models. The dataset is called The Stack v3, and its crawl closed on 7 August 2025. We walked all 4,096 metadata shards of it: 224,553,295 repositories and 58,467,468,698 files.

Two properties of the corpus shape everything below. It keeps only the default branch as it stood at crawl time, so there is no commit history to dig through and no record of rewrites or deleted branches. And every file carries the timestamp of its last modification.

Between 27 and 28 July 2026 we tested each candidate credential against the service that issued it. 543,699 came back live. That is eleven months after the crawl closed, and for most of them, years after they were first committed. That is more than double the 221,303 working credentials we found when we scanned 7.6 petabytes of Hugging Face training data .

The file timestamp is the only per-file clock this corpus offers, so we use it as the leak date. The assumption is that a file holding a credential has not been edited since the credential was put there. That holds up well for a config file or a checked-in environment file, and it errs in a useful direction: a file edited after the fact reads as newer than the leak, which makes our ages younger than the truth rather than older.

One credential usually appears more than once. The same key turns up across forks, across files, sometimes across both. We key each credential by its own value rather than by the file it sits in, then take the earliest date across every copy. That gives 543,699 dated credentials drawn from 1,103,438 separate exposures.

Leak density has never been higher

The corpus crawl closed on 7 August 2025, so 2025 covers seven months. The per-million-files view is the fair comparison across periods.

Live credentials by the year their file last changed. Switch to the monthly view to see GitHub’s secret scanning milestones in place.

The rate keeps climbing

Raw counts rise every year because the corpus grows. Files last touched in 2014 number 451 million. For 2024 the figure is 10.4 billion. Dividing the credentials found in a period by the files from that same period gives a density that can be compared across time.

That density has moved in one direction. It was 3.72 live credentials per million files in 2014, 9.54 in 2022, and 11.09 in 2023. In 2025 it reached 11.62, the highest in the whole dataset, measured on files touched in the seven months before the crawl closed.

GitHub has spent the last few years making secret scanning free, then automatic, for public code. Alerts became generally available and free for every public repository on 28 February 2023. Push protection reached general availability for public repositories in May 2023. On 29 February 2024 GitHub turned push protection on by default , so a push to a public repository carrying a recognised secret gets blocked unless the developer chooses to bypass it.

Those three dates split our findings into three groups.

Every one of these was still working when we checked

Alerts were free and push protection was available for any maintainer who switched it on. These credentials were committed anyway, and nothing revoked them in the years that followed.

Age is measured from the last modification of the file holding the credential to the day it was verified. The Stack v3 keeps no commit history, so this is the closest available leak date.

Live credentials grouped by the protection that was in place on the day they leaked.

245,959 credentials predate free alerts. 97,897 arrived while scanning was free and push protection was one setting away. 199,843 landed after the block became the default, and were still answering to their providers more than two years later.

Alongside alerts and push protection, GitHub runs a secret scanning partner program that forwards a leaked token to the provider that issued it. What happens is up to that provider. GitHub recommends treating every reported secret as compromised, but the program does not require partners to revoke anything. That helps explain why credentials from partner providers are still live years later, including an AWS key last touched in November 2009.

Where the block does not reach

Push protection blocks the secrets it recognises, which means partner patterns precise enough to be safe to block on: more than 200 token types from over 180 providers. That list is the reason the curve looks the way it does.

Our three largest live categories divide neatly on that list. Google Cloud service account material leads at 69,041 credentials and is covered. MongoDB connection strings at 51,067 are not, and neither are the 33,343 live Google API keys behind them. Connection strings and private keys count as generic patterns, which are off by default and only block if an organisation goes and turns them on. Add it up and 51.8 percent of every live credential in this corpus is a shape that a default-configured public repository will happily accept.

Google keys make the point most sharply, because the omission is deliberate rather than an oversight. Google API keys are on GitHub’s pattern list and are marked as not push protected, and google_gemini_api_key appears as its own entry with the same answer. A Gemini key is a billable credential attached to a model endpoint; a Maps key with the identical AIzaSy prefix is meant to ship in a web page. One pattern cannot tell those apart, so nothing gets blocked, and the ones that matter ride in with the ones that do not. We found 31,374 live Gemini keys with a median leak date of February 2025. That entire population is younger than push protection.

The oldest live credential we found was last touched on 13 June 2009. It is a set of database credentials inside an Erlang web server config, and it was still valid 16.1 years later. Behind it: an FTP login in a GPS logger’s C source from September 2009, copied into 62 repositories. An AWS key in a Rails S3 config from November 2009. A credential embedded in a URL in a dynamic DNS README from the last day of 2009.

We are not naming these repositories, because the credentials in them still work.

2,636 live credentials come from files last modified before 2015. A quarter of everything we found is older than four years. The 90th percentile is 6.3 years.

The gap is revocation

Every credential above was found with a pattern match and then confirmed by asking its provider whether it still works. The second step is the one that decides whether any of this matters. A leaked key that has been revoked is trivia. A leaked key that still authenticates is access.

To see which step is doing the work, we went back to everything the scan matched rather than only what verified. Most of that larger pile is not credentials at all. Patterns built around bare hex strings collide with commit hashes and integrity digests, and a source code corpus produces those by the million: one detector alone accounted for 826,669 candidates, of which exactly one carried a recognisable credential prefix. So we threw out anything that could not identify itself and kept only matches with a vendor prefix such as ghp_ or npm_ , a connection string with a password in it, a PEM private key block, or a service account JSON. That leaves 4,941,427 values that are unambiguously credentials, whether or not they still work.

One family has to come out before any of that can be read as a survival rate. 3,289,226 of those values are Google API keys, and we only count one as live if it authenticates to Gemini. The same AIzaSy prefix is used for Maps, Firebase and everything else Google ships, and we never tested those, because a Maps key in public code is not a secret in the first place. A survival rate needs a dead column you can trust, and for that family the dead column is mostly keys that were never asked. So Google API keys are excluded from every rate below, which leaves 1,652,201 credentials whose liveness means what it says. Private keys are out of the rate for the same class of reason: confirming one needs the host it belongs to, so all of them fail verification whether they work or not.

Sorted by whether the issuer runs an automated kill switch, the split is not subtle:

npm tokens. 101,886 committed, 1 still live.

npm tokens. 101,886 committed, 1 still live.

GitHub tokens. 73,048 committed, 260 still live.

GitHub tokens. 73,048 committed, 260 still live.

Hugging Face tokens. 30,437 committed, 15 still live.

Hugging Face tokens. 30,437 committed, 15 still live.

Google Cloud service accounts. 126,963 committed, 69,041 still live.

Google Cloud service accounts. 126,963 committed, 69,041 still live.

Postgres connection strings. 12,985 committed, 11,465 still live.

Postgres connection strings. 12,985 committed, 11,465 still live.

MySQL connection strings. 2,421 committed, 1,806 still live.

MySQL connection strings. 2,421 committed, 1,806 still live.

SendGrid keys. 22,800 committed, 9,189 still live.

SendGrid keys. 22,800 committed, 9,189 still live.

MongoDB is missing from that list on purpose, and it is worth saying why, because it is the largest single group of live credentials we hold after Google Cloud. All 51,067 of them verified, and not one came back dead. A clean sweep across fifty thousand samples is not a revocation policy, it is a measurement artefact: the MongoDB detector only reports a URI it managed to connect to, so a dead Atlas string never enters the data at all. The credentials are real and they work. The survival rate is unusable, so we split the connection strings by engine and left MongoDB out of every rate. Doing that turned up something the combined number was hiding: Redis URLs survive at 2.9 percent, because the ones people commit overwhelmingly point at localhost.

What keeps a leaked credential alive is revocation, not the block at push time

Each panel is the of that year's leaks from that family which still authenticated when we tested them. The dashed line is 29 February 2024, when push protection became the default. Hollow markers are years where nothing at all survived, which a log axis cannot otherwise place. Every credential counted is structurally unambiguous: a vendor prefix, a connection string carrying a password, or a service account key. A year is drawn only where the family committed at least 120 credentials in it. Private keys are excluded because confirming one needs the host it belongs to, so they cannot be scored either way. Stripe includes test-mode keys, which an issuer has little reason to revoke.

The of each year’s leaks from a family that still authenticated in July 2026. The default log scale is there because the families span five orders of magnitude; the other two modes make the comparison within a panel easier to read.

Survival runs from 0.001 percent to 88 percent inside one corpus and one crawl. Push protection is not what separates them. Every credential in that list was committed to a public default branch and is still sitting there, which is to say the block did not stop any of them. What separates them is whether the provider has a pipeline that takes a leaked token and kills it. GitHub and npm revoke their own tokens and almost nothing survives. Nobody revokes a Postgres URL, and almost everything does.

Push protection is a good control and it stops secrets at the door. It has nothing to say the 543,699 already inside, and it was never meant to. Alerts do cover history, but only where an owner enabled them, read them, and then went and rotated the key. That last step is the one that gets skipped, and it is the only one that changes the outcome. Where a provider automated it, the outcome changed on its own.

The block does work at the door

Everything above is credentials that already leaked, which says nothing whether the block reduced how many leak in the first place. That question the corpus can answer, because push protection does not cover every credential shape. The shapes it blocks and the shapes it ignores sit in the same repositories, written by the same people, and were exposed to the same rollout. If the block does anything, the two groups should come apart when it turned on, and not before.

Which shape is which is a matter of record rather than judgement, and we had it wrong at first. GitHub publishes the list , and two entries are counterintuitive. Google Cloud service account credentials are blocked. Google API keys are not, and neither are Gemini keys specifically. Private keys and database connection strings count as generic patterns, which are not blocked by default and only become so if an organisation opts in. So the group GitHub protects is the vendor-prefix tokens: GitHub, AWS, Slack, SendGrid, Stripe, GCP service accounts. The group it leaves alone is connection strings, private keys and Google API keys.

Measured as credentials per million files touched in the same month, the protected group ran between 6.6 and 8.9 for the twenty months to February 2024. It then fell through the spring and settled near 3.6, where it has stayed for more than a year. Nothing similar happened to the shapes the block ignores. Google API keys drifted from 56 to 51, private keys from 4.07 to 3.22, MySQL strings from 0.036 to 0.023. Postgres strings went the other way entirely and grew by a factor of five. Across the twelve months either side of the rollout the protected group fell 53 percent and the unprotected group fell 7 percent.

Two things make that gap hard to explain away. The decline is spread across the protected group rather than carried by one family: Slack tokens fell 64 percent, GitHub tokens and AWS access keys 59 percent each, SendGrid 56 percent, GCP service accounts 41 percent, and only Stripe is close to flat at 13 percent. Take the median family instead of the totals, so that no single large family can steer the result, and it is 57 percent against 15 percent. And running the same comparison at every other possible cutoff date gives a difference of roughly zero for everything up to August 2023, which is what says the gap belongs to the rollout rather than to a trend that was already under way.

Three things keep this from being cleaner than it is. The separation is a ramp from March to July 2024 rather than a step on 29 February, which fits a staged rollout but means we cannot pin it to the date. Generic patterns can be switched on per organisation, so some of the control group was partly treated, which pushes the 53 percent toward zero and makes it a floor. And the same years saw AWS and Google push hard toward short-lived credentials and workload identity, which would move these families in the same direction without any help from GitHub; the timing across six unrelated providers argues against that being the whole story, but it cannot be ruled out.

So both things are true, and they answer different questions. The block cut the rate at which covered credentials reach public code by half. It did not change what happens to a credential once it is there, and it does not reach the shapes that survive: 51.8 percent of every live credential in this corpus is a connection string, a Google API key or a private key, none of which are blocked by default. The families where nothing survives are the ones whose vendors revoke. The families that survive almost entirely are the ones nobody blocks and nobody revokes.

The practical version for anyone shipping public code:

Treat a committed credential as burned the moment it lands, whether or not anything flagged it. Rotate first, clean up the history second.

Treat a committed credential as burned the moment it lands, whether or not anything flagged it. Rotate first, clean up the history second.

Scan your own history rather than trusting that a block at push time covered you. Anything committed before February 2024 was never in scope for it.

Scan your own history rather than trusting that a block at push time covered you. Anything committed before February 2024 was never in scope for it.

Prefer credentials that expire on their own. Most of what we found would have aged out harmlessly if it had ever been given a lifetime.

Prefer credentials that expire on their own. Most of what we found would have aged out harmlessly if it had ever been given a lifetime.

Check whether your provider participates in a revocation programme. Detection without revocation leaves the key working.

Check whether your provider participates in a revocation programme. Detection without revocation leaves the key working.

Corpus. The Stack v3, all 4,096 metadata shards, covering 224,553,295 repositories and 58,467,468,698 file entries. Default branch only, crawl closed 7 August 2025.

Corpus. The Stack v3, all 4,096 metadata shards, covering 224,553,295 repositories and 58,467,468,698 file entries. Default branch only, crawl closed 7 August 2025.

Detection. Pattern matching with live verification against each issuing provider, run 27 and 28 July 2026. Only verified-live results are counted here.

Detection. Pattern matching with live verification against each issuing provider, run 27 and 28 July 2026. Only verified-live results are counted here.

Deduplication. Credentials are keyed by value, so one key found in 62 repositories counts once. The scan returned 544,069 distinct live credentials. 543,701 of them could be traced back to a file in the corpus metadata, and 543,699 of those carry a usable date. The 1,103,438 exposures behind that figure are the individual file and repository occurrences.

Deduplication. Credentials are keyed by value, so one key found in 62 repositories counts once. The scan returned 544,069 distinct live credentials. 543,701 of them could be traced back to a file in the corpus metadata, and 543,699 of those carry a usable date. The 1,103,438 exposures behind that figure are the individual file and repository occurrences.

Leak date. The last-modification timestamp of the file, taking the earliest across all copies of a credential. Dates outside 2008 to the crawl cutoff were discarded as unreliable.

Leak date. The last-modification timestamp of the file, taking the earliest across all copies of a credential. Dates outside 2008 to the crawl cutoff were discarded as unreliable.

Density. Credentials in a period divided by all corpus files whose timestamps fall in that same period, expressed per million files.

Density. Credentials in a period divided by all corpus files whose timestamps fall in that same period, expressed per million files.

Candidate cohort. The revocation comparison uses a wider set than the rest of the post: every match, verified or not, whose raw value is structurally self-identifying. Matches that are only bare hex, base64 or a UUID are excluded, because those shapes are indistinguishable from ordinary file content. A credential counted as not live is one that failed verification, which covers both revoked keys and keys that were simply deleted at the far end.

Candidate cohort. The revocation comparison uses a wider set than the rest of the post: every match, verified or not, whose raw value is structurally self-identifying. Matches that are only bare hex, base64 or a UUID are excluded, because those shapes are indistinguishable from ordinary file content. A credential counted as not live is one that failed verification, which covers both revoked keys and keys that were simply deleted at the far end.

Verification scope. Google API keys are verified against Gemini only. Maps, Firebase and other AIzaSy keys the prefix, were not tested, and are not sensitive, so that family is excluded from every survival rate. It is still usable as a control for the prevention comparison, which depends on how often the shape is committed rather than on whether it works. Private keys cannot be verified from the value alone and are excluded from survival rates for the same reason.

Verification scope. Google API keys are verified against Gemini only. Maps, Firebase and other AIzaSy keys the prefix, were not tested, and are not sensitive, so that family is excluded from every survival rate. It is still usable as a control for the prevention comparison, which depends on how often the shape is committed rather than on whether it works. Private keys cannot be verified from the value alone and are excluded from survival rates for the same reason.

Prevention comparison. Difference in differences on median monthly credentials per million files, twelve months either side of March 2024, treated against control by GitHub’s published push-protection coverage. Medians rather than sums, because single months carry test-fixture dumps: November 2023 alone holds 105,104 Stripe keys and 100,752 npm tokens with almost none live. Restricted to families already present in 2021, since OpenAI and Anthropic keys barely exist before the window and their growth is product adoption rather than policy. Six treated families against six control families, reported both pooled and as a median across families, because pooling lets one large family speak for the group.

Prevention comparison. Difference in differences on median monthly credentials per million files, twelve months either side of March 2024, treated against control by GitHub’s published push-protection coverage. Medians rather than sums, because single months carry test-fixture dumps: November 2023 alone holds 105,104 Stripe keys and 100,752 npm tokens with almost none live. Restricted to families already present in 2021, since OpenAI and Anthropic keys barely exist before the window and their growth is product adoption rather than policy. Six treated families against six control families, reported both pooled and as a median across families, because pooling lets one large family speak for the group.

Detector-conditioned families. MongoDB URIs are excluded from every rate. The corpus holds 86,377 of them and every one is verified, with no unverified counterpart, because the detector only emits a finding once it has opened the connection. Any survival or commit rate built on that family is conditioned on the credential working. Connection strings are therefore split by engine, using a scheme label joined back to the findings by credential hash.

Detector-conditioned families. MongoDB URIs are excluded from every rate. The corpus holds 86,377 of them and every one is verified, with no unverified counterpart, because the detector only emits a finding once it has opened the connection. Any survival or commit rate built on that family is conditioned on the credential working. Connection strings are therefore split by engine, using a scheme label joined back to the findings by credential hash.

The counts here describe one snapshot of one branch per repository. Anything that was force pushed away, moved to a non-default branch, or committed and reverted before August 2025 is invisible to this method. The real population is larger.

Thoughts, research findings, reports, and more from Truffle Security Co.

Thoughts, research findings, reports, and more from Truffle Security Co.

DOING IT THE RIGHT WAY

#trufflehog-community

Data processing agreement

Acceptable use policy

#trufflehog-community

Data processing agreement

Acceptable use policy