Coder registry compromise: the origin pool was the identity

Attackers joined the Cloudflare pool behind Coder's module registry and served credential-stealing Terraform modules. TLS held. The trust behind it didn't.

A registry is a server you trust by its name. Your tooling looks up the hostname, checks the TLS certificate, and accepts whatever comes back. For about fourteen hours on August 31, every one of those checks passed at registry.coder.com — and some of what came back was malware.

Per Coder’s security advisory, an attacker got into the company’s Cloudflare account and added their own servers to the pool that sits behind the module registry. Between 07:35 and 21:45 UTC that day, a deployment fetching Terraform modules from the registry could get a tampered copy instead. The tampered modules stole whatever the provisioning process could see: environment variables and provisioner secrets during template operations, and — during workspace builds — OIDC tokens, SSH keys, external auth tokens, and in some setups database credentials. The stolen data went to coder-infra[.]com, a lookalike domain registered three days before the attack. Coder rated the issue critical (CVSS 9.0), shipped patched releases on September 1, and published indicators and detection queries. How the attacker got into the Cloudflare account has not been disclosed as of publication, and no CVE has been assigned — trade coverage adds context but nothing more on the initial access.

The summary is the least interesting part. What this incident shows unusually clearly is two things: where a registry’s identity actually lives, and whose logs decide, afterward, whether you were affected.

The registry was whoever the pool said it was

Nothing in the delivery path broke. DNS gave the right answer. The TLS certificate was valid. From the client’s side, the registry still looked like itself — because “itself” was never more than whatever servers currently sit behind the hostname. The attacker didn’t have to break DNS or TLS. They changed what the hostname points to, by editing the server pool in a CDN dashboard.

That means the Cloudflare account was the real thing everyone was trusting — for every module the registry ever served. Adding a server to the pool is, in effect, deploying a new registry to production. But in most organizations that change doesn’t go through anything like a deploy pipeline. No review, no second approver, no diff anyone reads. One credential and one dashboard click, and a public hostname now means something else. If your product is distribution, your CDN settings are release infrastructure, and they deserve the same approval gates.

Nothing checked the modules themselves

The modules carried no identity of their own. When Terraform fetches a module, it verifies the connection, not the content: there is no signature a client could have checked against a Coder publishing key. So a module counted as genuine exactly when the server delivering it did — and the server’s identity, as we just saw, was pool membership.

Coder’s advisory publishes SHA-256 hashes of the malicious versions. Those are useful for hunting infections after the fact, but they don’t let a client verify a module before running it. The Principles for Package Repository Security — a framework from OpenSSF’s Securing Software Repositories Working Group, co-authored with CISA — puts build provenance and transparency logs at the highest maturity level. In plain terms: most registries don’t have them. Until artifacts carry proof of who built them, every registry client is one dashboard edit away from running a stranger’s code — and that is not specific to Coder or to Terraform.

Coder cannot tell you whether you were hit

Coder says it found “no indication that any customer data maintained by Coder was impacted.” Read that carefully: it only covers data Coder holds. Whether your deployment pulled a poisoned module is a question Coder cannot answer, because the evidence sits on your side — in your module cache, your provisioner logs, your outbound network records. So the advisory hands you homework: SQL queries to run against your own database, a string to search for in provisioner logs (data.external.telemetry), and the exfiltration domain to look for in network logs.

That is the honest shape of this kind of disclosure — and it is also a test most environments quietly fail. The real question is: can you reconstruct what your build infrastructure downloaded, and where it connected, during a specific fourteen-hour window, five days after the fact? If provisioner logs rotate weekly, if CI’s outbound traffic isn’t logged at all, if nobody can list which registries the pipeline pulls from — the investigation doesn’t reach an answer, it just stops. “We found no evidence” quietly turns into “nothing happened,” and credentials get rotated on faith or not at all.

What we’d check this week

  • List every registry your provisioning and CI pull from at build time, and note which ones verify anything beyond TLS. For most Terraform module sources, the answer is nothing.
  • Using only logs you already have, try to answer: what outbound connections did your build and provisioner hosts make on August 31? If you can’t answer for a normal day, you can’t answer for a bad one.
  • List who can change DNS, CDN, and origin-pool settings for anything you serve or fetch. Do those changes need a second approver? Do they trigger an alert someone actually reads?
  • Write down every credential your template and provisioning runners hold while they run — environment variables, OIDC tokens, SSH keys. Everything on that list was stealable here.
  • Compare how long you keep logs with how late disclosures arrive. This advisory landed the day after the window closed; many land weeks later. If logs expire faster than disclosures arrive, the trail is gone before you know to look.

Sources


Axowl builds identity and audit infrastructure where control-plane changes and credential use leave records that can’t be quietly rewritten. How it works.