Back to all posts
Published on · by Renaud Deraison

The package really was Red Hat's

Between late May and June 1, 2026, a worm called Miasma pushed credential-stealing code into 32 packages under the @redhat-cloud-services npm scope — Red Hat's own namespace, ~117,000 weekly downloads, signed by Red Hat's real publishing pipeline. There was no typosquat to catch and no unknown maintainer to flag. The trust signal was the vendor's name on the scope, and the vendor's name is exactly what the attacker rode in on. Here is why 'prefer reputable publishers' stopped being a defense, and what changes when the agent running the install lives in a per-profile Bromure VM.

The supply-chain attacks we wrote up in May had a tell. A Git URL where a version range should be. A Bun runtime appearing out of nowhere. An optional dependency that failed on purpose. Miasma has no tell. The packages were @redhat-cloud-services/sources-client and thirty-one of its siblings — genuinely Red Hat's, under Red Hat's own npm scope, published by Red Hat's own pipeline with a valid signature. Nothing was faked. The attacker did not need to fake anything. The name on the scope was the whole exploit.

A coding agent resolving @redhat-cloud-services/vulnerabilities-client does not hesitate, and neither would you. It is a first-party dependency from one of the largest enterprise vendors in the industry. There is no maintainer to vet, because the maintainer is Red Hat. There is no name to squint at for a transposed letter, because the name is spelled exactly the way it should be. Every heuristic a careful developer or a careful agent applies before running npm install returns green. So the install runs, and a preinstall hook fires, and the hook is a 4.2-megabyte blob of obfuscated JavaScript that starts reading the filesystem for keys.

The whole incident is a demonstration of one uncomfortable fact: reputable publisher is not a security control. It feels like one. Most of the supply-chain advice of the last three years leans on it. And on May 29 it bought exactly nothing.

What Miasma did.

The campaign — the string Miasma: The Spreading Blight first appears in a commit dated May 29, 2026, per OX Security — was caught by Aikido and OX Security and later analyzed by Socket, JFrog, Wiz, ReversingLabs, Microsoft and others. BleepingComputer and The Hacker News both covered it on June 1.

The shape, stripped to its mechanics:

  • The entry point was a compromised Red Hat employee GitHub account, used to push malicious commits into the @redhat-cloud-services source repositories.
  • A GitHub Actions workflow carried an _index.js script that authenticated to npm's trusted-publishing endpoint using an OIDC token — the same keyless mechanism npm now recommends over long-lived publish tokens. From npm's side, Red Hat's CI published Red Hat's packages. The signature was real.
  • The published packages carried a "preinstall": "node index.js" hook and an obfuscated payload of roughly 4.2 MB.
  • On install, the payload swept for GitHub Actions secrets, AWS credentials, Google Cloud credentials, Azure service principals, HashiCorp Vault tokens, Kubernetes service-account tokens, npm and PyPI publishing tokens, SSH keys, Docker credentials, GPG keys, and .env files — then encrypted and exfiltrated whatever it found.
  • It self-propagated by using the stolen access to commit through the GitHub API, reading action.yml files over GraphQL and writing new workflows back through mutations so the changes appeared, in Red Hat's own words on the commit log, verified and signed.

In total, 32 packages across 96 versions were hit, packages with roughly 117,000 weekly downloads, and the broader campaign touched 309 GitHub repositories. Socket's assessment was that this is "effectively a Mini Shai-Hulud campaign: it uses the same core tactics of install-time execution, credential harvesting, CI/CD targeting, encrypted exfiltration, and potential downstream propagation." Red Hat's statement was that "the packages are strictly limited to internal development, and the malicious code was never published for customer consumption" — which is true, and also not much comfort to the developers and CI runners that pulled @redhat-cloud-services/* as a transitive dependency in the window before the packages were yanked.

The defense everyone recommends is the one that broke.

We wrote about a similar worm three weeks ago — the TanStack compromise, where the giveaway was a prepare script hanging off a pinned Git URL and a Bun runtime that appeared from nowhere. The honest lesson from that post was: don't trust the lockfile, don't trust the signature. Miasma is the next turn of the same screw, and it is worth being precise about what is different, because the difference is the whole point.

The standard supply-chain hygiene stack has three rungs. Pin your versions. Verify provenance. Prefer reputable publishers. Miasma walks straight through all three. Pinning does nothing, because the malicious versions are the published versions. Provenance does nothing, because the provenance is valid — the OIDC trusted-publish flow genuinely was Red Hat's CI, and downstream the worm minted workflow commits that GitHub itself marked verified and signed. And the third rung, prefer reputable publishers, is not just defeated here — it is the attack surface. The reputation of the @redhat-cloud-services scope is the reason the packages get pulled without a second look. The more trusted the namespace, the more useful it is to whoever takes it over.

There is no version of "read the package more carefully" that catches this. The package is fine. The package is Red Hat's. The problem is that install-time code execution with the developer's ambient credentials is the contract, and a trusted name does nothing to change what that code can touch once it runs.

DEVELOPER LAPTOP / CI RUNNER — host secrets visible to whatever the install runsRED HAT'S OWN PIPELINEcompromised employeeGitHub account → pushGH Actions: OIDC publishsignature: VALIDprovenance: realnpm: @redhat-cloud-servicessources-clientvulnerabilities-clientrbac-client … (32 pkgs)~117k weekly downloadspreinstall: node index.jsCODING AGENT — no reason to hesitatetool: bashnpm i @redhat-cloud-services/sources-clientfirst-party vendor scope ⇒ green↳ preinstall runs node index.js↳ 4.2 MB obfuscated payloadHOST FILESYSTEM & ENV — all real, all readable by the preinstall hook~/.aws, GCP, Azure SPNscloud keys (real)Vault tokens, kube SA tokenscluster access (real)~/.npmrc, PyPI tokenpublish here (real)~/.ssh, GPG keyringsigning + push (real)$GITHUB_TOKEN, .env filesCI secrets (real)tar | encrypt | exfiltrateSELF-PROPAGATIONstolen GitHub accessread action.yml (GraphQL)commit poisoned workflowsshow up as: verified, signed309 repos in the campaignthe next scope inherits the trust
Miasma on a developer machine or CI runner that resolves @redhat-cloud-services directly. A compromised employee account pushes to Red Hat's repos; the OIDC trusted-publish flow signs the malicious versions as genuinely Red Hat's; npm serves them with a valid signature. The agent installs a first-party dependency with no reason to hesitate. The preinstall hook runs node index.js — a 4.2 MB blob — which sweeps the host for AWS, GCP, Azure, Vault, Kubernetes, npm/PyPI tokens, SSH and GPG keys, and .env files, encrypts them, ships them out, and uses stolen GitHub access to commit fresh poisoned workflows that show up as verified and signed. No typosquat, no fake maintainer, no Git-URL trick. The trust signal is the namespace, and the namespace is real.

The same install inside Bromure Agentic Coding.

Bromure Agentic Coding runs your coding agent inside a per-profile Linux VM — its own kernel, its own filesystem, its own network stack, on Apple's Virtualization framework. A profile is a coherent scope of work: this client, this internal product, this open-source library. The agent does its npm install-ing in there, and the host — your real keychain, your real cloud credentials, your real SSH keys — is on the other side of a hardware-enforced boundary the preinstall hook cannot cross.

The credentials do not live in the profile. They live on the host, behind a credential broker. When the agent needs to push a commit or publish a package, it does not read a token off the guest filesystem — there is none to read. It asks the broker, over a Unix domain socket, to use a credential on its behalf. The broker holds the real GitHub App private key, mints a short-lived installation token scoped to the repo the agent was already working in, and — for a profile configured to require it — surfaces an authorization prompt the developer answers on the host before the request goes out. The token is minted and attached to the outbound request entirely on the host side; it never enters the guest's memory or disk. The principle, which is as old as ssh-agent: broker the credential's use, never its value.

So walk Miasma's sweep through that boundary. node index.js reads ~/.aws/credentials and finds a stub or nothing. It reads ~/.npmrc and finds no publish token. It reads the environment for $GITHUB_TOKEN and finds nothing to steal — the installation token is minted and spent on the host, never written into the guest, and the broker only mints one at all when the developer answers the authorization prompt. The GPG keyring, the SSH private key, the Vault token, the kubeconfig: host-side, brokered if exposed at all, absent if not. The 4.2 MB payload runs to completion exactly as designed. It just exfiltrates a guest that was never holding the developer's keys.

AGENT ON THE HOSTnpm i @redhat-cloud-services/sources-clientpreinstall → node index.jsreads the host directlyREAL CREDENTIALS — read in plaintext~/.aws/credentialsAKIA… real~/.npmrc _authTokennpm_… real$GITHUB_TOKENghp_… real~/.ssh/id_ed25519private keyVault, kube SA, GPGall real→ encrypt + exfiltrate→ republish, propagateAGENT IN A PER-PROFILE BROMURE VMGUEST — what the preinstall hook can see~/.aws, ~/.npmrcstub / absent~/.ssh, GPG, Vaultnot on diskneed to push?→ ask broker over Unix socketauthorize prompt → used host-side("give me the key" is not a verb)HOST — broker holds the real keysGitHub App private keymints short-lived tokens, host-sidevalue never crosses the boundary
Left: the agent runs on the host, so the preinstall hook reads real keys directly — the Miasma chain completes. Right: the agent runs in a per-profile Bromure VM. The real credentials sit on the host behind a broker. When the agent legitimately needs to push, it asks the broker over a Unix socket and the broker mints a short-lived, repo-scoped installation token used host-side — gated behind an authorization prompt the developer answers on the host — and the token never lands in the guest. The malicious preinstall hook, reading the guest filesystem and environment, finds stubs and no token at all. The blast radius is one profile, not the developer's whole keychain.

The push the proxy refuses to forward.

Stealing the key is only half of Miasma. The other half is propagation: it used stolen GitHub access to commit poisoned workflows back through the API, and that is what turned one bad install into 309 repositories. The broker handles the theft. Guardrails, added in Bromure Agentic Coding 2.0, handle the misuse.

Guardrails are a host-side policy engine that lives inside the same MITM proxy the brokered traffic already flows through, so a compromised agent in the guest cannot route around them. Each request is classified by what it actually does to the resource, and each resource — GitHub, AWS, Kubernetes, Docker registries, DigitalOcean, GitLab, Bitbucket, hosted databases — can be set to off, block destructive, or read-only. Put a profile's GitHub guardrail in read-only mode and a git push — the git-receive-pack the worm needs to write its workflow back — returns a hard 403, while git fetch keeps working. A DELETE against the Kubernetes API, a manifest deletion in a registry, a Terminate* call to EC2: same treatment. The agent just sees a normal API failure. Miasma's propagation step is a write the proxy declines to forward, whether or not the agent ever got near a real credential.

What about the persistence?

This is where the per-profile model is honest about a cost. Miasma is a worm; its whole ambition is to come back. On a disposable-disk fantasy, you wave that away — the disk is gone after the task. A Bromure profile is long-lived, so a payload that writes itself into a startup script inside the profile can survive into the next agent session in that profile. We are not going to pretend otherwise.

What that persistence inherits, though, is a guest with no host keys and a broker that only speaks in short-lived, scope-limited tokens. The worm wakes up in the same sandbox it died in. It can read the same stubs. It can ask the broker to use a credential for the one repo this profile is authorized for — and the developer still has to answer the authorization prompt for that to go anywhere, with Guardrails free to refuse the push outright. It cannot reach the host keychain, the other profiles, or the developer's cloud credentials, because those were never inside the boundary to begin with. Persistence buys continued presence in a box with nothing in it.

And every bit of that — the preinstall firing, node index.js loading a multi-megabyte blob, the file written into a startup path, the egress attempt — lands in the hypervisor-level session trace. When Aikido publishes the indicators the next morning, the question "did this profile ever run Miasma?" is a grep, not an incident-response engagement.

Where this does not save you.

The broker scope is the whole game.

If a profile is provisioned to publish to your npm scope today, and that profile installs Miasma today, the broker will let it publish. Brokering works because the grant is narrow and short-lived. A profile that does not need to publish should not be able to. Scope it on purpose.

It does not review the diff.

Miasma propagated by committing workflows that appeared verified and signed. A read-only GitHub guardrail blocks the push outright — but a profile that legitimately needs to push runs in block-destructive mode, and Guardrails classify by method, not by what is in the diff. Neither isolation nor a method-level guardrail stops an agent from being talked into committing a poisoned workflow it is allowed to push. Read the diff. The trace tells you which diffs to read.

The clipboard is shared by default.

Bromure ships with host/guest clipboard sharing on, because pasting a stack trace into a chat is something humans do all day. For a sensitive profile, isolate the clipboard. The control exists; it is just not the default.

The trace is an audit log, not an IDS.

The session trace records the preinstall, the blob, the egress. It does not, on its own, decide the destination is hostile. It captures enough that once someone names the indicator, your answer is two seconds away.

The next scope is already trusted.

The lesson of the TanStack worm was that the lockfile and the signature are not defenses. Miasma adds the uncomfortable corollary: neither is the publisher. The @redhat-cloud-services scope did nothing wrong by being trusted — being trusted is the entire purpose of a vendor namespace, and it is exactly what made it worth attacking. The next campaign will ride in on a scope you trust just as much, signed just as validly, by a pipeline that was genuinely the vendor's right up until it wasn't.

You cannot fix that by trusting more carefully. You fix it by arranging things so that "which scope published this" stops being the question your keychain depends on. Bromure Agentic Coding is the configuration where the agent does its installing inside a per-profile VM, the real credentials stay on the host behind a broker, and the worst a preinstall hook can do is exfiltrate a box that was never holding your keys. It is free, open-source, and shipped today.