Public GitHub gets the headlines. A secrets detection vendor's 2025 report counted about 23.8 million new hardcoded secrets on public GitHub in 2024, up 25% on the year before. That is a large number and it is the one most teams worry about, because a public leak is visible to everyone, including the people who scan the commit feed for keys.
The less-quoted number from the same report is the one that should change how you think about the problem. 35% of private repositories contained at least one secret, against 4.6% of public ones. AWS IAM keys appeared in 8% of private repositories, about five times the public rate. Private code leaks at several times the rate of public code, because nobody is scanning it and nobody expects to be caught.
A private repository is not a vault. It is one clone away from a laptop, one laptop away from a worm, and one worm away from a public repository named after a sandworm. The keys inside it were valid when they were committed and, as the next section shows, most of them stay valid.
Leaked keys do not expire on their own
The same report found that 70% of secrets leaked in 2022 were still valid in 2025. Three years is a long time for a credential to sit in a commit, and it means the realistic threat is not the attacker who sees the push in real time. It is the attacker who gets read access to the repository at any point in those three years.
The report also measured where else credentials end up once they exist in code. 98% of secrets found in container images were in image layers rather than the final filesystem, which is to say in the build history a docker history or layer extraction reveals. 2.4% of corporate chat channels and 6.1% of issue tracker tickets contained leaked credentials. A key that starts in a private repository travels: into the image built from it, into the ticket about the bug it caused, into the message asking a colleague to try it.
How private keys become public ones
Two incidents in 2025 show the path from "never left the building" to "in an attacker's hands".
The Shai-Hulud npm worm, described by Krebs on Security on 2025-09-16, ran an open-source secret scanner on every developer machine it infected, collected npm tokens and cloud keys, and published them to public GitHub repositories. The developer's private repositories, cloned to a laptop, were the source. A dependency install was the delivery mechanism. The secrets never had to be in a public repository to end up in one.
In August 2025, attackers who had obtained OAuth tokens for a third-party chat integration used them to export Salesforce records at scale. Help Net Security reported that the attackers then searched the exported data for AWS access keys (the AKIA prefix), passwords and data warehouse tokens. The first credential opened a CRM. The second set, pasted into support cases and notes over years, opened everything else.
In both cases the secret's location was private. In both cases that provided no protection once an attacker had any foothold at all.
Where the keys hide now
The classic location is the config file: settings.py, application.properties, a .env committed by mistake. Those are still common, and still what most scanners look for first.
A newer location is the tooling that AI coding agents use. An MCP server configuration has an env block for the credentials the server needs, and the easy mistake is to write the value inline rather than a ${VAR} reference that resolves at runtime. The OWASP MCP Top 10 project lists this as its first entry, MCP01:2025 Token Mismanagement & Secret Exposure. Vulkro's mcp-audit rule MCP-004 flags exactly that shape: a literal credential in an MCP env block. Documentation: vulkro mcp-audit.
{
"mcpServers": {
"db": {
"command": "db-mcp",
"env": { "DATABASE_URL": "postgres://app:S3cr3tPassw0rd@db.internal/prod" }
}
}
}The fix is one edit, "DATABASE_URL": "${DATABASE_URL}", and the credential leaves the file. The point is that the file is new, it lives in the repository, and most teams have not added it to their list of places to look.
The third location is history. A key removed in a later commit is still in every clone's .git directory, and the report's 70% figure says it probably still works.
Finding them without sending them anywhere
Secret detection is part of a normal vulkro scan .. The scan runs on the machine, reads the files, and reports the matches. It makes no network call during the scan, so the key it finds is not transmitted to a service to be found. For teams whose policy forbids a scanner that phones home, VULKRO_OFFLINE=1 or --offline makes that a hard guarantee rather than a default: any command that would need the network is refused. Documentation: offline mode.
VULKRO_OFFLINE=1 vulkro scan .There is one deliberate exception, and it is opt-in. --validate-secrets checks each detected credential against the live provider API and tags it [live], [dormant] or [unknown]. That is a network call, it is off by default, and under VULKRO_OFFLINE=1 it is refused rather than silently skipped. Use it when you need to know whether a key found in a three-year-old commit is one of the 70% that still work. Do not use it from a machine that is not allowed to talk to the provider. Documentation: vulkro scan.
Scanning the working tree finds the keys that are there now. Scanning git history, the "secrets across git history" capability, finds the ones that were removed but never rotated. If your repository is older than a year and has never been scanned, the second set is usually larger than the first.
What a scan does not tell you
A match on an AKIA prefix is a key. A match on a 32-character hex string next to the word token is a probable key. A scanner cannot tell whether a probable key is a real credential, a test fixture or a hash, and the honest output says so with a confidence level rather than a flat severity. Treat a high-confidence secret finding as a rotation task. Treat a low-confidence one as a question for the author.
A scanner also cannot tell you whether a key has already been used. Validation says whether it is still accepted. The provider's access logs say whether anyone else has presented it. Rotating a live key is the first step; reading the logs for the window it was exposed is the second, and no code scanner does that part.
What to do this week
- Run
vulkro scan .on your three most sensitive private repositories and count the secret findings before you read them. If the count surprises you, the 35% figure applies to you. - Add the MCP configuration files in your repositories to the scan path. If any
envblock has a literal value, replace it with a${VAR}reference. - Pick one repository older than a year and scan its history, then rotate everything that comes back, whether or not it looks dormant.
- Decide, in writing, whether
--validate-secretsis permitted on your network. If not, run withVULKRO_OFFLINE=1so the decision is enforced. - Read the offline mode docs if the scanner itself is subject to a no-egress policy.
Vulkro Core comes with a 14-day trial. Start it here and run the first scan offline.