Two malicious Terraform providers were still live on the public registry two days after disclosure
Aikido published Go malware found in two Terraform providers and two Go modules on 22 September. Two days later both providers were still installable from registry.terraform.io, and one had shipped two new versions since the report.
By Parminder Kumar Sharma · · 23 min read

The download counter is still moving
At 11:40 UTC on 24 September 2026 I asked the Terraform Registry API what it knew about two providers. It answered both times.
kreuzwenker/docker: still served, latest version 4.7.0, 1,466 downloads, tier community. gocommunity-io/dockerd: still served, latest version 4.5.7, 248 downloads, tier community.
Aikido Security named both as carriers of Go malware on 22 September. The coverage that followed on 23 September repeated download counts of 1,449 and 222. Two days after publication the registry's own counter reads 1,466 and 248. That is 43 further downloads across the two providers, taken while the names were sitting in every threat feed in the business.
The counter is not the striking part. kreuzwenker/docker published version 4.6.0 at 14:50 UTC on 22 September, the day the research went out, and version 4.7.0 at 05:17 UTC on 23 September, fourteen hours and twenty seven minutes later. Both timestamps come from the registry's own per-version records, not from the publisher. The account kept shipping after it was named, and the registry kept ingesting.
Say straight away what that does not establish. It does not establish that 1,714 machines are compromised. A download is a download: a scanner, a mirror, a researcher and a CI runner all look identical to a counter. It does not establish that HashiCorp has done nothing, because a takedown in progress and a takedown not started look the same from outside. It does not even establish infection where the provider was installed, because Aikido's own analysis says the providers stay inert unless a specific runtime condition is met.
It establishes one thing precisely, and that one thing is the story. At the time of writing, an infrastructure team that mistypes six letters in required_providers still receives the malicious binary, from the official public registry, over TLS, with a signature that verifies.
Queried directly against registry.terraform.io at 11:40 UTC on 24 September 2026, two days after the research was published
| Field | What the registry returned | What it does not tell a consumer |
|---|---|---|
| kreuzwenker/docker | Live. 1,466 downloads. Latest 4.7.0, published 23 September 2026. | Whether any download was a real pipeline, and whether the payload ever activated. |
| gocommunity-io/dockerd | Live. 248 downloads. Latest 4.5.7, published 2 September 2026. | Same. The registry counts requests, not installations or infections. |
| Tier for both | community | Nothing about review, scanning or trust. The legitimate provider carries the same tier. |
| Registry warning field | Empty for both providers | No flag, banner or advisory was attached to either entry when I checked. |
What the research actually found
Aikido reported four packages: two Terraform providers and two Go modules. The names are useful to defenders and are reproduced here as indicators. Nothing in this briefing tells anyone how to obtain or run them.
The four packages named in Aikido's research, with publication dates from the research and, where I could check them, from the registries themselves
| Package | Channel | First published |
|---|---|---|
| kreuzwenker/docker | Terraform Registry | Version 4.5.0 on 4 September 2026, per the registry API. A typosquat of kreuzwerker/docker. |
| gocommunity-io/dockerd | Terraform Registry | Version 4.5.2 on 27 August 2026, per the registry API. Aikido dates the providers to early September. |
| gocommunity.io/orderedbtree | Go module proxy | 11 August 2026, per Aikido. Carried the malware in plaintext. |
| gogets.dev/btreex | Go module proxy | 8 September 2026, per Aikido. Displayed as November 2025 by the proxy and by pkg.go.dev. |
The mechanism matters more than the count, and it is the detail most likely to be lost in the retelling. Aikido reports that both providers carry a hidden entry point that fires only when the SHA256 hash of two Terraform variable values, concatenated, equals one specific digest. That same hash is then used as an AES key to decrypt an archive shipped inside the package, and the decrypted Go code is run as a detached process. Until that condition is met, the provider behaves like a Docker provider.
Read what that design buys the attacker. The malicious provider is not a broadcast, it is a lock that opens for one set of keys. Aikido's phrasing is that the provider "is inert except under specific runtime conditions, suggesting the malware is delivered as part of a targeted attack". A team that installed the typosquat by accident, and never used the container name and network identifier the attacker chose, would see nothing at all. The 1,466 downloads and the number of victims are not the same number and are not even close.
The second stage is a Go remote access trojan with two independent command channels: a Slack bot token, and an Ethereum smart contract on the Arbitrum Sepolia testnet. It checks in with platform, architecture, hostname, username, home directory, whether node is present on the machine, and the time. After check-in it generates an ephemeral key pair and derives a shared key with a hardcoded attacker public key, so that hosts sharing a channel cannot read each other's traffic.
It polls the blockchain every three seconds and Slack every ten. That is 28,800 and 8,640 outbound requests per host per day, which is the single most useful detection fact in the whole report: an infected runner is loud, in a pattern, to two destinations most infrastructure pipelines have no reason to contact. Aikido makes the same point, recommending that organisations without a business need to touch blockchains alert on traffic to HTTP-based blockchain services.
A Terraform provider is not a library
Everything about how seriously to take this follows from one sentence in HashiCorp's own plugin documentation: "Terraform Plugins are written in Go and are executable binaries invoked by Terraform Core over RPC." The same page adds that plugins "are executed as a separate process and communicate with the main Terraform binary over an RPC interface".
Read that as an operations person rather than as a plugin developer. A provider is not source code that your build compiles and your reviewers read. It is a pre-built executable that terraform init downloads and that terraform plan starts as a child process on whatever machine ran the command. The same documentation lists, among the primary responsibilities of a provider plugin, "Authentication with the Infrastructure Provider". The provider is the component that holds the credential, because holding the credential is its job.
In most organisations the machine running the plan is not a laptop. It is a CI runner carrying an assumed role, a workload identity or a key in an environment variable, because that is how the pipeline reaches the cloud at all. So the blast radius of a malicious provider is not a developer's browser profile and local SSH key. It is whatever the pipeline is permitted to do, which in a great many places is more than any individual employee is permitted to do.
Nothing in Terraform constrains that. The documentation describes an RPC boundary, and an RPC boundary is a protocol, not a sandbox. No isolation, seccomp profile, syscall filter or credential broker between the provider process and the runner is stated anywhere in the plugin documentation, because none exists to state. Once terraform plan launches the binary, the binary is simply a process on your build agent with your build agent's privileges.
Aikido arrives at the same place from the attacker's side, noting that Terraform users "are more likely to be involved in infrastructure deployment" and that by targeting DevOps machines the actor "may get an even more direct pathway to critical production credentials". That is the whole reason a registry with a few thousand downloads is worth an actor's time when npm has millions. The conversion rate is terrible and the prize is enormous.
What the registry checks when you press publish
HashiCorp's publishing documentation opens with the answer: "Anyone can publish and share a provider by signing into the Registry using their GitHub account and following a few additional steps."
The additional steps are these. The repository must be public and named in the pattern terraform-provider-NAME, in lowercase. There must be at least one GitHub release tagged with a semantic version prefixed with v. The release assets must include the per-platform zip files, a registry manifest, a SHA256SUMS file and a GPG signature of that file. And the publisher supplies HashiCorp with the public half of the signing key: "The Terraform Registry will validate that the release is signed with this key when publishing each version, and Terraform will verify this during terraform init."
Every one of those checks is an integrity check, and every one of them passed for the malicious providers. They all answer the same question: did these bytes come, unaltered, from the holder of the key this account registered? None of them answers whether the code should be trusted, whether the publisher is who the name suggests, or whether the name is a near miss for something with fifty seven million downloads. HashiCorp's documentation describes no code review, no malware scanning and no name similarity check at publication, and I could not find any such claim anywhere in the registry documentation.
That is not a scandal on its own. Every open registry in the world is in the same position, and a registry that promised to vet code would be making a promise it could not keep. The problem is the gap between what the machinery does and what the badge on the page suggests it does.
Which brings us to the tiers. HashiCorp defines them in a single sentence that is more honest than the way they are usually read: "The Registry uses tiers and badges to denote the source of a provider." The source. Not the quality, not the review status, not the safety.
Provider tiers as defined on HashiCorp's providers overview page, with the guarantee each one actually carries
| Tier | HashiCorp's definition | What it does not establish |
|---|---|---|
| Official | Owned and maintained by HashiCorp, in the hashicorp, IBM, IBM-Cloud and ansible namespaces. | Not a security assertion about any particular release, only about who owns it. |
| Partner Premier | Written and maintained by technology partners who meet the partner premier requirements. | The requirements are a partner programme, not a code audit of each version. |
| Partner | Written, maintained, validated and published by third party companies against their own APIs. | Validated by the partner, against the partner's own API. Not by HashiCorp. |
| Community | Published by individual maintainers, groups of maintainers, or other members of the Terraform community. | Nothing. It means a GitHub account claimed a namespace. Both malicious providers sit here, and so does the real one. |
One letter, and a version number that overtakes the real one
Yes, typosquatting is the mechanism, at least for one of the two providers. The legitimate provider is kreuzwerker/docker, published by the Berlin consultancy of that name and one of the most widely used community providers in existence. The malicious one is kreuzwenker/docker. An r becomes an n, in the middle of a word most English speakers do not know how to spell in the first place, inside a string that appears once in a configuration file and is never read again.
The legitimate provider and its typosquat, as the Terraform Registry API returned them on 24 September 2026
| Attribute | kreuzwerker/docker (legitimate) | kreuzwenker/docker (malicious) |
|---|---|---|
| Downloads | 57,129,756 | 1,466 |
| Tier badge | community | community |
| Latest version and date | 4.6.0, 1 September 2026 | 4.7.0, 23 September 2026 |
| Source repository | Live on GitHub | Returns 404. The registry entry is still live. |
The version numbering deserves attention on its own. The real provider reached 4.6.0 on 1 September 2026. The typosquat opened at 4.5.0 on 4 September, three days later, which is close enough to look like a slightly stale mirror of the same project. It then shipped 4.6.0 and 4.7.0 on 22 and 23 September. At the moment anyone glances at both registry pages to work out which is the real one, the fake is a version ahead. Roughly one download in thirty nine thousand went to the fake, which sounds reassuring until you remember that the payload only opens for a target the attacker has already selected.
gocommunity-io/dockerd is a different trick and worth naming separately, because a team that greps only for near misses of names it already uses will not catch it. There is no popular provider called dockerd. The package name borrows the credibility of the Docker daemon, and the namespace borrows the credibility of a community that does not exist. Aikido reports that gocommunity.io and gogets.dev are websites the actor registered, each within a day of the matching GitHub organisation, each presenting itself as a vanity naming system for Go packages, each with no mechanism for anyone else to publish and both "appear non-functional". They exist so that an import path looks like it belongs somewhere.
One date correction while we are here. Aikido places the provider publications in "early September". The registry's own record shows gocommunity-io/dockerd version 4.5.2 published at 02:16 UTC on 27 August 2026. If you are scoping an exposure window for your own pipelines, start on 27 August, not 1 September.
The Go modules, and dates that are not dates
The Go side of this campaign carries a separate lesson, and I checked it myself rather than take it on trust, because it is the kind of claim that is easy to state and easy to get slightly wrong.
Aikido reports that gogets.dev/btreex was first published on 8 September 2026, and that the actor used forged commits in the source repository to backdate the history to November 2025. Because Go treats commit dates as authoritative when deriving version timestamps, the module proxy and pkg.go.dev display the falsified dates. I queried proxy.golang.org directly. For version 1.2.3 it returns a time of 27 November 2025. The six tags it serves run from 25 October 2025 to 27 November 2025. pkg.go.dev renders the line "Published: Nov 27, 2025".
Against a real first publication of 8 September 2026, that is 285 days of manufactured history on the newest tag and 318 days on the oldest.
This matters because age is one of the very few heuristics a busy engineer applies to an unfamiliar dependency. A module that appears to have been quietly maintained since last autumn, with six tags and a steady cadence, reads completely differently from one that appeared last week. The technique is not improvised. ReversingLabs documented git log rewriting in the same campaign in April 2026, where the actor copied legitimate repositories wholesale and rewrote their entire history so that every commit appeared to come from accounts it controlled, complete with invented employees who had no GitHub presence at all.
The second thing I checked was the Go checksum database, and the answer is instructive. sum.golang.org returns a signed, notarised entry for gogets.dev/btreex version 1.2.3. That is the system working exactly as designed, and it is worth being precise about what it buys you. The transparency log guarantees that the bytes you fetch for a given module version are the same bytes everybody else fetches, and that they have never been swapped after the fact. It makes no claim whatsoever about what those bytes do.
Immutability is not safety. In this case it means every Go developer on earth is cryptographically guaranteed to receive the identical malware.
Would a lock file, a checksum or a mirror have stopped it?
The honest answer is mostly no, and it is worth working through why, because three of the four controls people reach for here are the wrong shape for this attack.
The dependency lock file would not have stopped it. .terraform.lock.hcl is trust on first use, and HashiCorp says so plainly: Terraform "will also verify that each package it installs matches at least one of the checksums it previously recorded in the lock file, if any". If any. On the first terraform init for a provider you have never used, there are no previously recorded checksums, so Terraform records whatever it is handed. If the source address you typed was the typosquat, the lock file faithfully pins the typosquat and then defends it against tampering for the rest of its life.
Version constraints would not have stopped it. A constraint is an argument about which versions of a provider you have already decided to trust. The decision to trust happens one line earlier, in a source string that a human typed and that no tool second-guesses.
Checksum verification would not have stopped it. The malicious releases were correctly signed with the key the registry held for that account, and the zip checksums matched. Everything that could be verified, verified. That is the point of the story.
A private registry or a mirror with an allowlist would have stopped it, and of the four it is the only one that would. Terraform's CLI configuration supports a provider_installation block in which a filesystem_mirror or network_mirror carries include patterns, and in which omitting the direct method removes access to origin registries altogether. Under that configuration a provider address nobody has approved does not resolve at all. It does not download and fail a check; it simply is not there.
That is the difference worth internalising. A lock file verifies what you asked for. A mirror decides what you are allowed to ask for. Typosquatting is an attack on the asking, so only the second one touches it.
The four controls usually proposed for this class of attack, assessed against what Terraform's own documentation says each one does
| Control | What it stops | What it does not stop |
|---|---|---|
| Dependency lock file, committed | A changed artefact for a version already recorded, and drift between a laptop and a runner. | The first install of a provider you never meant to install. Trust on first use records it. |
| required_providers version constraints | An unexpected major or minor version of a provider you chose. | Anything about which publisher you chose. The constraint applies after the source address. |
| Registry signature and checksums | Tampering in transit and a substituted release. | A correctly signed release from an account that is not who you thought it was. |
| Private registry or mirror with an allowlist | Any provider address that is not on the list, including every typosquat, at resolution time. | A malicious version of a provider that is on the list. Pair it with pinned versions. |
There is a fifth control which is not a Terraform control at all, and it decides the size of the incident rather than whether it happens. The malware ran wherever the plan ran, with that runner's identity. If your plan role can read every secret in the subscription, so could it. Most plan roles are considerably broader than a plan needs, because they were copied from the apply role on a Friday afternoon and never revisited.
The recruitment route, and whether it appears here
The Graphalgo campaign that this overlaps with was documented by ReversingLabs on 11 and 12 February 2026, with a follow-up on 9 April. The original report describes a route that is now familiar: developers approached through LinkedIn, in Facebook groups and on forums including Reddit, by a fake blockchain company called Veltrix Capital, and handed a coding task hosted in a GitHub repository. The repository itself is clean. The malicious behaviour arrives through a dependency, in the first wave from npm and PyPI, in the April wave from a GitHub release artefact referenced deep inside a lock file's transitive dependencies.
The DevOps angle is not new with the Terraform providers. It was in the February report, which records an advertisement for a fake job aimed specifically at DevOps candidates, and interview repositories named in the pattern test-devops-monitoring and test-devops-orchestrator. ReversingLabs counted 192 malicious packages across npm and PyPI, 106 and 86, with the first published on 2 May 2025. That is 510 days of continuous operation to the day I am writing this.
On attribution, be careful about the word. ReversingLabs attributes the campaign to the Lazarus Group "based on the similarity with the techniques used in their previous campaigns", and lists them: fake interviews as the approach, coding tasks as the infection vector, blockchain and crypto targets, multistage encrypted malware, patience between a benign first version and a malicious later one, token protected command and control, and git commits carrying GMT plus nine timestamps, which is North Korea's time zone.
That is a well argued inference from tradecraft, and the researchers present it as one. No indictment, sanction, law enforcement action or government attribution is cited in any of the three reports. When this reaches your board pack, "North Korean state sponsored" should carry the phrase "assessed by the researchers" alongside it. The security consequence is identical either way; the evidentiary standing is not.
Does the recruitment route appear in the Terraform leg? Not on the record. Aikido does not report a fake company, a recruiter, an interview task or a LinkedIn approach in connection with these four packages. What it reports instead is a different social engineering surface entirely: the two fake ecosystem websites, registered within a day of their GitHub organisations, built to make an import path look institutional.
My reading, and I label it as inference rather than finding: the two legs of this campaign use different front doors because they are aimed at different people. The npm and PyPI leg recruits an individual developer and depends on that person running a task on their own machine. The Terraform and Go leg depends on a name surviving a glance in a code review, plus an activation gate that only opens for a target already chosen. The first needs a person to fall for a job offer. The second needs a repository to fall for a spelling.
What a UK organisation running Terraform should do this week
In the order worth doing, and with the expensive items deliberately last so that the cheap ones actually happen.
Take this with you
Terraform supply chain: the order to work in
- Search every Terraform configuration, module and lock file in every repository for the four names: kreuzwenker/docker, gocommunity-io/dockerd, gocommunity.io/orderedbtree and gogets.dev/btreex. Search source addresses and go.mod files, not only lock files, because a provider can be named in a module you vendored.
- If you find one, treat the runner and the workstation as compromised rather than the file as dirty. Aikido is explicit: the second stage runs detached, so deleting the package does not stop what it started. Isolate, rotate every credential that touched that machine, and reimage.
- Rotate in the order that matters for a pipeline: cloud roles and keys first, then version control tokens, then registry publishing tokens, then SSH keys. Review what those credentials did during the exposure window, which for the providers starts on 27 August 2026.
- Produce an inventory of every provider source address in use across the estate, with the namespace and the download count of each. Most teams have never seen this list and it is usually shorter and stranger than expected.
- Read the namespaces out loud, in a room, with someone who does not maintain the code. Typosquats are caught by ears and by fresh eyes, not by the person who typed them.
- Confirm that .terraform.lock.hcl is committed in every repository, and that CI fails rather than regenerates when the lock file does not match. A lock file that CI silently rewrites is not a control.
- Run terraform providers lock with a -platform flag for every platform your people and your runners use, so that the lock file carries checksums for the CI runner and not only for the laptop that ran init first.
- Cut the plan role. A plan needs read access to the resources under management and a state backend. It very often holds write access and secret read across a whole subscription because it was copied from the apply role.
- Separate plan credentials from apply credentials, and make apply require a human approval that a provider process cannot forge on its own.
- Alert on outbound traffic from build agents to blockchain RPC endpoints and to the Slack API, unless you have a stated business need for either. An infected host in this campaign makes roughly 28,800 blockchain requests and 8,640 Slack requests a day, which is a pattern, not a needle.
- Decide, as a policy question with a named owner, whether your pipelines may install providers from the public registry at all, or only from a mirror carrying an approved list. This is the only control on the list that stops the next typosquat.
- Record that a GitHub takedown is not a registry removal, and check the distribution channel itself before you close an incident.
# Terraform CLI configuration: install providers only from an approved
# local mirror. Omitting the "direct" method removes access to origin
# registries entirely, so an unapproved source address does not resolve.
# Place at ~/.terraformrc, or point TF_CLI_CONFIG_FILE at it on the runner.
provider_installation {
filesystem_mirror {
path = "/opt/terraform/providers"
include = ["*/*"]
}
}
# In the configuration itself, pin the publisher as carefully as the version.
# The source line is the trust decision; the version line only narrows it.
terraform {
required_providers {
docker = {
source = "kreuzwerker/docker"
version = "~> 4.6.0"
}
}
}
The question that exposes the gap
The registry answered every question it was asked. It confirmed the release was signed with the key the account had registered. It confirmed the checksums matched. It confirmed the namespace belonged to the GitHub account that claimed it. It was never asked whether the code should be allowed to run on a machine holding production credentials, because nothing in the chain asks that, and nothing in the chain is built to.
So the question to put to your own organisation is not whether somebody would have spotted kreuzwenker. Almost nobody spots kreuzwenker. The question is this: if a provider you already trust ships a new version tonight, and your pipeline installs it at two in the morning and runs it as a child process on the runner that holds your cloud role, what in your environment would notice, and how much would that binary be able to reach before anything did?
Sources
- PrimaryGraphalgo campaign spreads to Terraform providers and Go Modules. The primary research: package names, the SHA256 activation gate, the second-stage RAT, the dual Slack and blockchain C2, victim telemetry and indicators of compromise.Aikido Securityaccessed 2026-09-24
- PrimaryTerraform Registry API record for kreuzwenker/docker. Used to verify the provider is still served, its tier, its download count and the publication timestamps of every version.HashiCorpaccessed 2026-09-24
- PrimaryTerraform Registry API record for gocommunity-io/dockerd. Used to verify liveness, tier, download count and per-version publication dates.HashiCorpaccessed 2026-09-24
- PrimaryTerraform Registry API record for the legitimate kreuzwerker/docker provider. Used to verify its download total and that it carries the same community tier as the typosquat.HashiCorpaccessed 2026-09-24
- PrimaryHow Terraform works with plugins. The source for providers being executable binaries run as a separate process and communicating over RPC, and for provider responsibility for authenticating to the infrastructure provider.HashiCorpaccessed 2026-09-24
- PrimaryPublish providers. The source for who may publish, the repository naming requirement, and the GPG signing key the publisher supplies and the registry validates.HashiCorpaccessed 2026-09-24
- PrimaryProviders overview and the tier table. The source for what official, partner premier, partner and community mean, and for tiers denoting the source of a provider.HashiCorpaccessed 2026-09-24
- PrimaryThe dependency lock file. The source for committing .terraform.lock.hcl to version control, for trust on first use, and for the h1 and zh checksum schemes.HashiCorpaccessed 2026-09-24
- Primaryterraform providers lock. The source for pre-populating checksums for every platform a team builds on, including the CI runner's platform.HashiCorpaccessed 2026-09-24
- PrimaryCLI configuration file. The source for the provider_installation block, filesystem_mirror, network_mirror, direct, and the include and exclude patterns used to block the public registry.HashiCorpaccessed 2026-09-24
- PrimaryGo module proxy version list and per-version info for gogets.dev/btreex. Used to verify the module is still served and that the proxy reports commit timestamps from October and November 2025.Googleaccessed 2026-09-24
- PrimaryGo checksum database record for the malicious module. Used to show that the transparency log notarises the hash of a known-malicious module.Googleaccessed 2026-09-24
- Primarypkg.go.dev page for gogets.dev/btreex, used to verify the displayed publication date of 27 November 2025.Googleaccessed 2026-09-24
- PrimaryGo modules reference. The source for how modules are published from version control with no approval step, for the proxy and checksum database, and for commit time being used in version timestamps.Go projectaccessed 2026-09-24
- PrimaryFake recruiter campaign targets crypto devs, 11 February 2026. The original Graphalgo report: the fake company, the DevOps interview tasks, the recruitment channels and the basis for the Lazarus attribution.ReversingLabsaccessed 2026-09-24
- PrimaryInside the fake crypto developer recruitment hack, 12 February 2026. The technical companion: 192 packages, the npm and PyPI split and the campaign start date.ReversingLabsaccessed 2026-09-24
- PrimaryGraphalgo fake recruiter campaign returns, 9 April 2026. The source for git log rewriting, look-alike maintainer names and the move of payloads to GitHub release artefacts.ReversingLabsaccessed 2026-09-24
- Reported byCoverage of the Aikido research, 23 September 2026. Used only as the source of the widely repeated download figures of 222 and 1,449, which this briefing re-checks against the registry.The Hacker Newsaccessed 2026-09-24


