Unlisted, Not Private
A full sweep of 33 public attack surfaces turned up 23,912 machine credentials that still work. We graded each by the privilege it carries, and 1,277 of them survive revocation of the leaked key.

On this page(7)
Table of Contents
A full sweep of 33 public attack surfaces turned up 23,912 machine credentials that still work today. This report grades each key by the privilege it actually carries.
Everything we assumed was hidden was open
The attack surfaces in this study are mostly the ones teams wave through as "not externally exposed." That covers 30,990 share conversation links across ChatGPT, Claude, Gemini and Grok, the .mcp.json family of agent config files sitting in GitHub repositories, and container image layers on AWS ECR Public.
What the three have in common is that anyone can download them without logging in. All three yielded working credentials. The ECR image layers, for one, exposed five GitHub PATs with control over an organisation's entire runner fleet.
Plenty of teams treat "not indexed by a search engine" as if it were the security condition "access is restricted." Those are two different things.
How the verification ran, and how the surfaces were chosen
Unlike the account a person logs in with, a machine credential authenticates code to a system: API keys, service account tokens, OAuth client secrets, database connection strings. Every candidate collected was sent to its issuer's official endpoint for a validity check, and every figure quoted here counts only the 23,912 that passed. Privilege scope was established for 7,468 of them, and the severity distribution further down uses those 7,468 as its population.
Verification queries went to read-only endpoints only, and rather than following access chains we recorded the scope string alone. Third-party account IDs and IAM usernames were masked at the point of recording. There is a reason the scope was drawn this tightly. Three canary tokens were mixed into the corpus, and the moment our verification queries landed, their owners were notified. That put a live case behind the principle that verification stops at read-only endpoints.
For surface selection we defined a rule instead of a fixed URL list: when a new AI or workflow tool invents its own config file format, is that file structurally designed to hold keys. Four families qualified.
- Agent configs.
.mcp.json,.cursor/,.continue/,.aider.conf.yml. Writing key values straight into anenvblock is the file design itself. - Workflow exports. n8n, Flowise, Dify, Langflow. Connection details are serialised into JSON as they stand.
- Extension packages. VS Code
.vsix, Chrome.crx. They download without an account, and store review stops at the bundle surface. - AI share links. ChatGPT, Claude, Gemini, Grok. Whatever was typed into the conversation is published with the URL.
Where did the valid credentials come from?
By raw count, GitHub config files led with 20,777, followed by Google OAuth clients at 3,649 and GitHub public repositories at 3,230. Of the 33 surfaces studied, 11 returned zero valid credentials, among them Common Crawl, Docker Hub and Terraform modules. Those stayed in scope and under monitoring.
A high detection count does not automatically mean higher risk. The worst case on GitHub config files, first by volume, was write access to 16 production CMS spaces at a large Nordic insurer. Supabase was small at 643 valid credentials yet carried the highest CRITICAL density in the set, with 670 organisation management tokens alone. MCP and agent configs produced just 31 valid credentials, but the path did not exist two years ago, which makes it worth watching.
By issuer, auth, payments and media took the top places: Google OAuth at 7,143, then Razorpay (2,261), RapidAPI (1,094), MercadoPago (942) and Cloudinary (932).
The figure worth pausing on is that 904 valid credentials, 3.8% of the total, appear on no GitHub-family host at all. 307 of those turn up on neither GitHub nor GitLab nor Bitbucket. These are exposed assets that a scanning scope limited to source repositories cannot reach, while an attacker collects them just as easily from outside. It is the same point GitGuardian's State of Secrets Sprawl 2026 makes when it reports that 28% of security incidents begin outside the repository.
Severity grades, and the hole in "revoke the key"
Severity here is graded by how far an attacker holding the key can reach, rather than by count.
By the numbers, LOW is the largest grade at 4,532, while the two CRITICAL grades together come to 1,744. What decides a CRITICAL rating is not an impressive permission name but the answer to one question: can this key mint a replacement for itself? That is the difference between revoking the original key and leaving the attacker's access intact.
Database connection strings came to 6,042 in total, with the mongodb.net form accounting for 3,862, or 63.9%. A mongodb+srv://user:pw@... structure has nowhere to hold the account and the password separately, so one string carries the whole account. That is why most of the CRITICAL account-owner grade runs straight to database administration.
When an incident lands, the last step in the response procedure is usually "revoke the affected API key." This study found 1,277 valid credentials where revocation alone could not close access, because those keys also held permission to issue new ones. Supabase organisation management tokens accounted for 670, MongoDB atlasAdmin for 488, SendGrid key creation for 122. If an attacker minted a backdoor key on acquiring the original, revoking the original leaves their access running.
Another 1,152 cases had keys for different services tangled into a single file. Even after removing the overlap, 2,296 credentials were not resolved by a single revocation.
A key registered as leaked, still authenticating
Verifying 172 Alibaba Cloud keys surfaced an odd pattern. For 148 of them, a query to the RAM endpoint (GetAccountAlias) returned 403 Forbidden along with a message stating the AccessKey was registered as a leakage risk. The key had already been identified as exposed inside the issuer.
Calling the STS endpoint (GetCallerIdentity) with that same key returned a normal 200 OK. The internal path that detects exposure and the IAM path that blocks access run separately. So a key the system had registered as leaked kept authenticating successfully for anyone holding it.
The blast radius of a single leaked key is considerable.
The 670 Supabase management tokens leaked through .mcp.json could control 1,211 organisations and 2,607 projects in full, and the 30 Atlassian admin tokens led straight into internal Confluence wikis. Runbooks on those wikis carry credentials for still more systems, so one Atlassian token becomes the collection path for an organisation's wider credential set.
That reach was established from privilege scope. No attempt was made to walk the routes.
New leak paths from AI tooling, and the limits of .gitignore
Leak patterns have shifted sharply with the arrival of AI tooling. The surface is still the GitHub repository, but new paths have appeared inside it that did not exist two years ago.
.mcp.json308.cursor/mcp.json302.vscode/mcp.json144.gemini/settings.json43.claude/settings.local.json43.kiro/settings/mcp.json31.kilocode/mcp.json28
Agent config files alone produced 1,248 valid keys, 5.3% of the total. AI and LLM API keys (Pinecone 609, Tavily 567, Context7 522 among them) came to 2,998, or 12.5%, which shows the kind of key being leaked moving into the AI ecosystem. AI share links, by contrast, contained a credential in 0.384% of 30,990 links, with a Wilson 95% interval of 0.321% to 0.459%. Smaller than the attention they get.
The problem is that the .gitignore files development teams write cannot keep up with this pace.
The classic mistake is writing only a directory name such as .claude/ into the ignore rule. Because that never covers the real leaked filename, .claude/settings.local.json, 53.1% of repositories published their config file as-is. On top of that, 89.8% of the 4,049 leaked files went public the very day the commit landed. Collection bots pick them up immediately, so there is no window in which a .gitignore rule could act.
Response, and the limits of this verification
To contain exposure, an organisation should work in this order.
- Start with files git already tracks, using `git log`.
.gitignoredoes not apply retroactively to anything already committed. - Replace key values in config files with environment variable references. 53.1% of repositories wrote the directory name and missed the file that leaked.
- Check re-issue permission during key rotation. Revocation is not the end of it; confirm that the key left behind no sub-accounts or additional keys.
- Widen the scope beyond the git repository. Package registries, container image layers and extension marketplaces all qualify. External exposure scanning covers this band, working from an attacker's vantage point without internal access.
- Put a pre-commit gate in place. With 89.8% going public on the day of the commit, the block has to sit before the commit.
The limits of this study were equally clear. Of the 60,865 credentials that came back invalid, only 12.4% (7,535) carried an explicit "revoked" status. The remaining 75.3% were unexplained 401 and 403 errors or no response at all. Whether an issuer puts the cause of invalidation in its response deserves a place in future service selection.
Identifying the owning organisation proved harder still. Issuers returned an account email for 1,566 of 23,681 valid credentials, and 309 of those addresses were company domains. Four further verification steps brought the confirmed count down to 3.
The heaviest drop came between 309 and 25. A company-domain email tells you who created the account, not that the asset belongs to the company. The company name, organisation name and workspace title recorded in account settings are values anyone can type in freely, so none of them serve as evidence either. An early draft of this study nearly said that 306 accounts were not company assets. Failing a verification test and not being a company asset are separate findings.
Because the study ran on a principle of never recording an unmeasured value as zero, the blanks stayed in. Privilege scope was never established for 16,444 credentials, and the cause of invalidation is unknown for 75.3%. Filling those blanks would push the numbers up, not down.
What this study establishes comes down to one thing. Being absent from an index is not the same condition as having access restricted. Share links, agent config files and container image layers were all operated on the assumption that nobody would look, and all of them were handing out live keys, 23,912 of them, with no login required.
Read next
Your Dashboard Says 14,000 Secrets. The Number That Matters Is 525.
Every secret scanner hands you a big number, and almost nobody can act on it. When we verified each finding against the service that issued it, a five-figure detection count became a three-figure inventory of credentials that actually work. This is what that collapse means for how you prioritize, what you suppress, and what you tell your board.
Your Slack Webhook Is Write-Only, Until an AI Agent Reads the Channel
A leaked Slack incoming webhook is usually triaged as low severity: write-only, one channel, no data access. The moment an AI agent reads that channel and can act with tools, that write-only primitive becomes an indirect prompt injection path into the agent's privileges. Here is the full kill chain, the exact preconditions, and how to defend it.
Unrotated API Keys: Why Years-Old Credentials Still Run Production (NHI Kill Chain #3)
A single AWS key, never rotated for 3 years, spread across 7 systems. When a supply chain attack hit a Terraform CI plugin, the key gave attackers full infrastructure access. Inside the Aged Key kill chain and how to defend against long-lived credentials.
Get the next one in your inbox
Monthly NHI research brief from the Cremit team. One email, high signal.
