They changed the address, not the package
For fourteen hours on August 31, 2026, some requests to registry.coder.com were answered by servers an attacker had added to Coder's own Cloudflare origin pool. The hostname was right, the certificate validated, the modules had the names and versions they always had, and the code that came back went looking for SSH keys, OIDC tokens and database passwords. Coder does not have the attacker's logs, so its advisory asks you to check your own outbound traffic. That is a question most machines running a coding agent cannot answer.
The hostname was Coder's. The certificate validated. The module carried the name and the version your template already pinned. For fourteen hours on August 31, some of the servers answering belonged to someone else.
A platform engineer updates a workspace template on a Monday morning.
terraform init fetches the modules the template names (Docker, Zed, aider)
from registry.coder.com, the same place they came from last month. Terraform
prints its usual wall of green and the build succeeds. No tool flags anything,
that morning or that month.
On September 1, Coder published GHSA-vx42-ghc9-gw65, rated critical at CVSS 9.0. In its words: "An unidentified malicious actor gained access to Coder's Cloudflare infrastructure and added unauthorized IP addresses to the pool used for Coder's module registry." Between 07:35 and 21:45 UTC on Monday, August 31, some requests for Coder modules were served by machines the attacker controlled. Coder builds self-hosted cloud development environments; BleepingComputer lists Dropbox, Palantir, Square, Mercedes-Benz, KKR, EnBW, US government bodies and defense contractors among its users.
Nobody touched the package
Every supply-chain habit a careful team has is a check on the artifact. Is the name spelled right. Is the publisher who they claim. Was this version published yesterday. Does the lockfile hash still match. Read the diff before you bump.
The attacker changed which machines answered when your client asked for the
address, and left the artifact alone. They published no package, took over no
maintainer account, registered no look-alike name and edited nothing in Coder's
registry. The name resolved the way it always resolves. The TLS handshake
completed against a valid certificate for the real hostname. The module was
called docker, and zed, and aider, because those are the modules a Coder
template asks for, and it carried the version number the template already
pinned.
What came back was different. The poisoned copies shipped a shell script named
dlp.sh, with its own build for the docker, aider, rstudio-server, windows-rdp
and zed modules, and its own hash for each in the advisory. The advisory describes
the payload as code "designed to identify a number of credentials and exfiltrate
those to a remote server with a lookalike domain name compared to one Coder
regularly uses." That domain is coder-infra[.]com, registered on August 28,
three days before anyone used it, resolving to 199.91.220.205, with the callback
at http://www[.]coder-infra[.]com/cli/check.
The list of what it went after reads like an inventory of a development platform's trust: provisioner environment variables and secrets, user OIDC tokens, SSH keys, external authentication credentials, and coderd configuration variables including database passwords and auth-provider credentials.
You did not need to do anything unusual to be in the window. Per the advisory: "Users who use the main Coder module registry (registry.coder.com) for their template source and who updated components during the time period when this code was available may have installed the malicious packages." Creating a template, bumping a template version, running a dry run, or building a workspace with module caching off is ordinary Monday work.
"Review firewall, proxy, DNS, and VPC flow logs"
That is step five of Coder's remediation, and the most instructive line in the advisory. Steps one through four are things Coder can help with: SQL to find cached modules from the window, SQL to purge them, patched releases (2.37.0, 2.36.4, 2.35.7 and 2.34.9, all out on September 1), and a list of credentials to rotate. Step five reaches past all of that, because the servers that would say who was hit belong to the attacker. Coder cannot see them, and says as much: it cannot conclusively identify every compromised deployment.
So the question comes back to you, and it is a question about evidence. Did
anything on the machine that ran that module connect to coder-infra[.]com? For
a production VPC most teams can answer within the hour, because somebody on the
platform team already owns the flow logs.
Now ask it about the machine where your coding agent works. An agent runs
terraform init, npm install, make, a container build, a test suite, a
script it found in the repository, a binary a module downloaded. Each of those
opens connections, and nobody is writing them down where you could search them a
week later. The agent's own transcript holds the tool calls the model made. It
has no row for what a shell script inside a Terraform module did with the
network, because the model never saw that happen.
You cannot buy an answer to step five after the fact. Either something recorded the request as it left the machine, or there is nothing to search.
What the script would have found
Take the payload at its word and let it run. It sweeps the environment for
credentials, reads the SSH keys, grabs the OIDC and external-auth tokens, picks
up the configuration variables with the database password in them, and posts the
lot to coder-infra[.]com.
Bromure Agentic Coding gives each workspace its own Ubuntu VM under Apple's hypervisor, with a host-side proxy as the guest's only route to the network. Run the same sweep there and it comes back with a full set of credentials, all of them fakes.
ANTHROPIC_API_KEY holds an sk-ant-api03-brm-… decoy, GH_TOKEN a ghp_
string of the right length, and ~/.git-credentials, ~/.docker/config.json and
~/.kube/config hold matching placeholders. The shapes survive, so gh,
docker and kubectl accept them without complaint. The proxy keeps your real
values encrypted on your Mac and swaps them onto the wire after the request has
left the VM, only when it is bound for the host that credential was minted for.
No switch turns this off; it is how credentials work in the app.
The SSH keys are the cleanest case: there are none in the VM to read.
SSH_AUTH_SOCK points at a bridge over a virtual socket to an agent on the host,
and that protocol has a request meaning "sign this challenge" and no request
meaning "hand me the key." ls ~/.ssh/id_* comes back empty while git push
keeps working.
AWS deserves its own paragraph, because Terraform is what runs against it. The
VM's ~/.aws/config points at a helper that vends your real access key ID paired
with a fake secret key. Terraform, the AWS SDKs, boto3 and the aws CLI all
read that helper without any setup, then sign their requests with the fake and
produce a well-formed signature AWS would reject. The host strips that signature
and re-signs with the real secret on the way out. A script scraping the
environment for AWS material walks away with a key ID and forty characters that
authenticate nothing.
Then it has to send its haul somewhere, and that is where the workspace's egress
firewall sits. Set Unmatched traffic to Deny and the VM reaches the hosts you
listed and nothing else. Enforcement happens twice, in two places the guest
cannot reach: the virtual network switch matches every flow by destination IP and
DNS-snooped hostname across all protocols, and the proxy matches again by TLS
server name. Nobody put a three-day-old domain on that list. Rule edits reach
running sessions without a reboot, so you can add deny any coder-infra[.]com
while the build is still going.
If the traffic does go out, because the workspace runs in the default
allow-everything posture or because the destination looked ordinary, the decoys
do their second job. The proxy scans every outbound request, headers and body,
for a fake credential headed somewhere it was never minted for. A
sk-ant-api03-brm-… string addressed to coder-infra[.]com is not ambiguous.
The proxy blocks it with HTTP 451 before a byte reaches the destination, pauses
the VM, and raises an alert naming the credential and both hosts. Bromure marks
the workspace compromised, and it will not boot again until you wipe its disk
and home.
You get step five answered while it happens, instead of reconstructing it from logs a week later.
One command answers the advisory
The host proxy writes a metadata record for every request out of the VM,
whichever process made it: time, host, port, method, path, status, latency,
and bytes measured before any credential swap. Set the
workspace's Session trace to Activity only and you get that record
with no request bodies stored at all. Then
bromure-cli trace hostnames my-workspace prints every distinct host the
workspace has contacted, with counts. Coder's step five, in one line, for
the machine where the code ran.
What the engines decided
The Security Timeline (Window → Security Timeline…) is the separate view of what Bromure's engines decided: every egress-firewall verdict, every credential the proxy brokered, every supply-chain block, colour-coded and filterable. A refused connection to a domain registered three days ago shows up there as a red Firewall row while the build is still running, not as a grep through a month of flow logs after somebody else's advisory tells you where to look.
The guest can reach one thing on your Mac: a shared folder, a live read-write
window into your real filesystem that the compromise wipe leaves alone by design.
Share the project directory rather than your home directory, and a script hunting
for id_rsa and .env walks a Linux home full of checkouts and package caches.
When you cannot check the source, watch the exit
Provenance checks all reduce to one move: compare what arrived against something you already trust. A name, a publisher, a signature, a hash in a lockfile. The move works while the attacker has to put something into the channel for you to compare against. An attacker holding a stretch of the channel puts nothing in it, and Coder's origin pool was a stretch of the channel. The BdThemes compromise in August had the same shape, where a hijacked storage bucket changed a JSON feed that 350,000 WordPress sites fetched, with no release and no modified file.
Fourteen hours is one working day. Coder caught it, shipped patches the next morning, published hashes and indicators, and told readers what to check. The advisory still ends by asking each reader to go look at their own outbound traffic, because the record on the other side is gone.
Rotate the keys the advisory lists, purge the cached modules, take the patch. Then decide what the next one gets to reach, because the next one will arrive through a channel you had no reason to doubt. Install Bromure Agentic Coding, give the build its own machine, and have the answer waiting when the advisory shows up.