A provider entry looks ordinary: a plausible namespace, a name you half-recognise, a version constraint someone pasted into a module three sprints ago. On 23 September 2026, Aikido Security reported that malicious Terraform providers had been published to HashiCorp's official registry, and researchers describe it as the first time that centralised registry has been used as a distribution vector for a malicious payload. Most write-ups will tell you which packages to delete and stop there.
The more useful question, if you run terraform init in CI or on a laptop that holds cloud credentials, is narrower: when that command finishes, what has actually been verified, and what has merely been recorded? This piece covers both campaigns from that week, then walks the trust model honestly: what the Terraform registry supply chain guarantees by design, and which of those guarantees the attackers are bending rather than breaking.
Four packages, one very specific trigger
Aikido's report names four artifacts. Two are Terraform providers: gocommunity-io/dockerd, with roughly 222 downloads, and kreuzwenker/docker, with roughly 1,449. Two are Go modules: gocommunity[.]io/orderedbtree and gogets[.]dev/btreex. The attackers stood up both gocommunity[.]io and gogets[.]dev as fake package ecosystems so the module paths would resolve to something that looked like a real project.
kreuzwenker/docker is the one that matters. The widely used community Docker provider lives under kreuzwerker. Swap the r for an n and you have a namespace that survives a code review at a glance. Classic provider typosquatting, aimed at a string almost nobody reads character by character.
What separates this from commodity package malware is the targeting. The provider payload only activates when the SHA-256 of the containerName and networkID Terraform variables, concatenated, matches a hardcoded value. Anyone else who installs it gets a provider that behaves normally and looks clean under inspection. JFrog observed the same discipline in the payload's decryption: it unpacks only when the victim solves a linear system with one specific matrix, and the Go module variants hide the same trigger behind mathematical or "price value" checks. You can run the artifact in a sandbox all afternoon and see nothing.
When the trigger does fire, the second stage is a Go remote access trojan. It collects system information and takes commands over two channels: an Ethereum smart contract on the Arbitrum Sepolia testnet, used as a blockchain dead drop and polled roughly every three seconds, and Slack workspaces including portfolio-devs[.]slack[.]com and portfolio-testers[.]slack[.]com, polled roughly every ten seconds. It can execute code or delete itself.
Aikido ties the campaign to Graphalgo, first documented in February 2026 by ReversingLabs and attributed to North Korean actors who pose as Web3 companies and approach developers through LinkedIn and job forums. Aikido counted roughly 18 unique infected systems across Windows, Linux and macOS since July 2026. Checkmarx, JFrog and SafeDep corroborated parts of the analysis.
Two caveats on the numbers. Download counts on any registry include mirrors, scanners and researchers, so 1,449 is a ceiling on exposure, not a victim count. The 18 infected systems is the figure that actually describes impact. We found no public HashiCorp statement on these packages and no confirmation of removal at the time of writing; treat the registry listing state as something to check yourself rather than assume.
If these landed anywhere in your estate, the researchers' advice is blunt and correct: isolate the host, rotate every credential that host could reach, and re-image it. An implant that can decrypt and execute payloads on its own has earned that level of distrust.

The same week, from the lock file side
Reported 21 September 2026, a second North Korea-linked actor tracked as TraderTraitor ran the attack from the opposite end. Instead of publishing to the real registry, it planted malicious provider references directly in .terraform.lock.hcl files inside fake job-interview repositories. Names seen include Northwind-IAC, novacart-interview and terraform-candidate-repo. The targets were DevOps, cloud and FinTech engineers, chosen because they hold AWS, GCP and API keys.
Run terraform init on one of those repos and it fetches attacker-controlled code from a typosquatted registry host, registry.hashicorp-aws[.]com. The payloads are FLATROOF, also tracked as macOS.Gaslight, a Rust ARM64 macOS backdoor that exfiltrates over Telegram, persists via ~/Library/LaunchAgents/loginwindow.plist, and collects browser data, keychains and process listings. The second is ROOFDECK, which provides interactive shells and file transfer and uses a Nostr dead drop for command and control.
The recommended countermeasures are boring and effective: read .terraform.lock.hcl before running init on any repo you did not write, restrict provider installation to registries you approve, and watch developer endpoints for new LaunchAgents and for quarantine attributes being stripped.
Why malicious Terraform providers clear the checks you think exist
Both campaigns work because the provider supply chain verifies real things, just not the thing most engineers believe it verifies.
The registry is a namespace index. Anyone can publish under a namespace they control. A namespace that resembles a known vendor is not verified to belong to that vendor, and there is no rule that kreuzwenker must prove a relationship to kreuzwerker. That is a design fact, not a bug. What the attacker exploits is that engineers read source = "kreuzwenker/docker" as an identity claim.
Provider binaries are GPG-signed by the publisher. The signature proves the artifact came from the same party that published under that namespace and has not been altered in transit. It does not say that party is trustworthy. A typosquat namespace signs its own malware perfectly well, and the signature check passes. Design fact; what the attacker exploits is the word "verified" in the output.
.terraform.lock.hcl pins version constraints and records hashes. It protects you from the artifact changing underneath a version you already accepted, which is genuinely useful and the reason supply-chain guidance keeps pushing lock files. It does not protect you from having pinned the wrong provider in the first place. Campaign B is exactly that: ship the victim a lock file that already pins the attacker's provider, and the lock file's integrity guarantee now works for the attacker, faithfully reproducing the malicious artifact on every machine.
Providers are executables. They run on whatever machine is doing the plan, which is usually a CI runner or an engineer's laptop with cloud credentials in the environment. A provider does not need to wait for apply to do damage; init fetches it and plan runs it. This is the part that turns an IaC supply chain attack into a credential compromise, and it maps onto the same blast-radius thinking we covered in what a stale token costs when nobody is watching it.
Mechanism | What it actually proves | What engineers commonly assume | What the attacker exploits |
|---|---|---|---|
Registry namespace | Someone controls that namespace | The namespace belongs to the vendor it resembles | One-character typosquat reads as legitimate in review |
GPG signing key | Same publisher signed this build; bytes unmodified | The publisher was vetted | Attacker signs their own malware; check still passes |
| This exact artifact matched when the lock was written | The provider itself was reviewed | A poisoned lock file pins malicious code reproducibly |
Version constraint ( | Upper bound on the range | The code inside won't change | New patch releases install without re-review |
| Nothing has been applied to your cloud yet | Nothing untrusted has executed | Provider binary already ran locally with your credentials |
Download count | Number of fetches | Community vetting | Mirrors and scanners inflate it; low counts still infect |
Nothing in that list is broken. The malicious Terraform providers Aikido found pass each check in turn, which is why signature status and download counts were poor signals here.

Hardening terraform init without wrecking your pipelines
You can't count on a registry catching the next batch of malicious Terraform providers before you install one, so terraform init security comes down to controls on your side of the fetch. Four changes, in the order they pay off. None are exotic, and most teams have done one of them and assumed it covered the rest.
Pin exactly, and pin the source string as carefully as the version.
terraform {
required_version = "~> 1.9"
required_providers {
docker = {
# The 'w' matters: kreuzwenker/docker was the typosquat, kreuzwerker is real
source = "kreuzwerker/docker"
# Exact pin. "~>" lets a new patch land without anyone re-reviewing the artifact
version = "= X.Y.Z"
}
}
}
Generate lock hashes for every platform you run on. A lock file written on an engineer's Mac often carries hashes only for darwin_arm64. When CI on Linux hits it, Terraform has to go back to the registry for a hash it can't verify against the file, which weakens the guarantee precisely where it matters.
terraform providers lock \
-platform=linux_amd64 \
-platform=linux_arm64 \
-platform=darwin_arm64
# In CI: fail the build if init wants to change the lock file at all
terraform init -lockfile=readonly
Restrict where providers may come from. A CLI config file, pointed at by TF_CLI_CONFIG_FILE, decides which sources Terraform will even consider. This is the control that would have stopped Campaign B outright. A poisoned lock file naming registry.hashicorp-aws[.]com is inert if direct installation is off.
# ~/.terraformrc or a file referenced by TF_CLI_CONFIG_FILE in your runner image
provider_installation {
network_mirror {
url = "https://tf-mirror.internal.example.com/providers/"
}
direct {
# Nothing is fetched from any upstream registry; the mirror is the only source
exclude = ["*/*/*"]
}
}
If you're not ready to run a mirror, a filesystem_mirror block over a directory your platform team populates gets you most of the way, with the same direct { exclude = [...] } line doing the actual enforcement. Vendoring providers into an internal mirror also gives you one place to scan and one place to block, the same argument for putting security checks inside the pipeline rather than beside it.
Run plan on a disposable runner, not on a laptop with production credentials. An ephemeral container with a short-lived, scoped role means a provider that executes still lands somewhere with minutes of access to a narrow set of permissions, rather than on a machine holding SSH keys, browser sessions and a ~/.aws/credentials file. This is ordinary least-privilege and workload-isolation practice applied to a step most teams never classified as code execution.
Reviewing a repo you didn't write
Three minutes, before the first init, on anything from a candidate, a vendor, a conference workshop or a blog post.
Open .terraform.lock.hcl and read it as a list of things that are about to execute. Every provider "registry.terraform.io/namespace/name" block should correspond to something in your required_providers, and nothing else should be there.
Check every source = "namespace/name" against the real namespace: on the registry, in the provider's own repository, character by character for anything that resembles a known vendor. This is the step that catches typosquats, and the only one that does.
Then grep for hosts that aren't the registry you expect:
# Any provider source pinned to a non-default registry host
grep -rn 'registry\.' --include='*.tf' --include='*.hcl' . \
| grep -v 'registry\.terraform\.io'
Anything that comes back deserves an explanation before you run anything. Private registry entries are legitimate; hashicorp-aws[.]com is not.
What to change this week
Search every repo for provider
sourcevalues whose namespace is one edit away from a vendor you actually use. Start with Docker, Kubernetes and the major cloud providers.Replace
~>with=on provider versions in anything that touches production.Re-run
terraform providers lockwith a-platformflag for every OS and architecture in your fleet, and add-lockfile=readonlyto CIinit.Ship a
TF_CLI_CONFIG_FILEin your runner image withdirect { exclude = ["*/*/*"] }and a mirror you control.Move
planoff laptops for any workspace with production credentials.Alert on new files under
~/Library/LaunchAgentsand on quarantine attributes being removed on developer macOS endpoints.If any of the four named artifacts appear in your history, isolate, rotate and re-image the host. Don't negotiate with a self-deleting implant.
Decision rule for any repo you didn't author: treat terraform init as running a stranger's binary with your credentials, because that is what it does. If you wouldn't curl | bash from that repo's authors, read the lock file first.
For more on shrinking what a compromised endpoint can reach, see our cloud security practices for engineering teams, and the rest of our security coverage.





