The provider runs where the keys are
Aikido found a North Korean RAT shipped as two Terraform providers on HashiCorp's registry. terraform init downloads a provider, terraform plan runs it as a local process, and the provider reads your cloud credentials, because that is what a provider is for. The payload decrypts only for one victim's variable names, so scanners that pulled the package saw inert code. Ten of the eighteen machines it reached were Macs. Bromure Agentic Coding gives that binary a VM, a fake secret key, and a list of places it may call.
Every control the industry has built for coding agents sits upstream of the
command: approve the plan, read the diff, gate the push. None of them sit
between a downloaded provider binary and ~/.aws.
You are adding a container to a stack. You ask the agent for a Docker provider,
it writes four lines of HCL, and you run terraform init. Terraform resolves the
provider from the registry, downloads it, checks its signature, and caches it.
Then you run terraform plan, and Terraform starts that binary as a process on
your machine and talks to it over RPC. It authenticates to your cloud account on
the way, using the same environment variables and ~/.aws profile you use
yourself.
Every step of that is the documented design, and this month it was also a malware delivery route.
What Aikido found
On September 22 Aikido reported a Go-based remote access trojan distributed through two Terraform providers and two Go modules. The Hacker News covered it the next day, calling it the first time attackers have used the registry HashiCorp runs to deliver a payload. Checkmarx, JFrog and SafeDep added analysis.
The providers are gocommunity-io/dockerd, with 222 downloads, and
kreuzwenker/docker, with 1,449. The second is one letter away from
kreuzwerker/docker, a real provider with 56 million reported downloads. The Go
modules are gocommunity.io/orderedbtree, published August 11, and
gogets.dev/btreex, published September 8.
Aikido ties the campaign to Graphalgo, which ReversingLabs documented in February and attributed to North Korean operators. The same signing key has appeared in npm payloads since April. People reach these packages through a job offer: a fake Web3 company approaches a developer on LinkedIn or Facebook and sends over a coding exercise. The exercise's repository is clean. The dependency it pulls is the malware.
A provider is a program, and Terraform gives it the keys
npm install runs a package's setup scripts and then gets out of the way.
terraform init fetches a whole program, and running that program is the main
event.
A Terraform provider is an executable binary that Terraform Core invokes over RPC. Terraform runs it because running it is what a provider is. You will not find an install hook to strip or a lifecycle script to audit, because execution here is the feature rather than a side effect of unpacking a tarball.
The binary also runs in the one place on your network with the shortest path to production. Purple Shield describes the machine: "A malicious provider therefore executes on the machine of the person deploying your infrastructure, with whatever access that person has." And: "a laptop that runs terraform apply usually has cloud provider keys, Git tokens, SSH keys, and Terraform state files within reach."
An npm stealer has to go hunting through your home directory for credentials. Terraform hands them to the provider.
Addressed to one person
These providers attack one target and ignore everybody else who installs them.
Both carry a hidden entry point in
/internal/provider/resource_docker_container_funcs.go that fires only when the
SHA256 of the containerName and networkID Terraform variables, concatenated,
equals one hard-coded value. The code then uses that hash as the AES key to
decrypt a file path inside the package, unzips what it finds there, decrypts each
file, and runs the result as a Go package with a detached go run .. They hid
the archive in the examples directory, in a file called
import-resource.sqlite3.
So the attacker has to know the names you gave two variables, and this attacker did: the job-offer lure is how they learned. Every researcher and every scanner that pulled the package got code that sits there and does nothing. You cannot detect what never decrypts.
The second stage is a remote access trojan. It reports system information first,
including hardware attributes, OS, hostname, and whether node is available on
the machine, then waits for instructions in two places. One is Slack: a workspace
called portfolio-devs, a channel called frontend-devs, and later a second
workspace with a channel named qa-announcements. Aikido counted 1,240 encrypted
messages there going back to July 16. The other is a smart contract on the
Arbitrum Sepolia testnet, at a hard-coded address, holding 1,402 transactions
back to August 6. The malware polls the blockchain every three seconds and Slack
every ten.
Both channels are ordinary HTTPS to destinations with good reputations. Your developer machines talk to Slack all day, and one of them talking to a public Ethereum RPC endpoint reads as normal in any week the team ships something on-chain. The same crew used a Sepolia contract in the npm package Checkmarx described last week, and they reuse it in the open because it works in the open.
Aikido recovered the victim list from the attacker's own Slack: 18 unique hostnames over 725 check-in messages. Three Windows, five Linux, ten macOS.
What changes in a Bromure workspace
The campaign came for a credential. In Bromure Agentic Coding, the VM's ~/.aws/config does not contain your secret
access key. It points at a credential-process helper that vends, over a virtio
socket, your real access key ID paired with a fake 40-character secret. The
manual lists the tools that pick this up with no configuration at all: every AWS
SDK, the aws CLI, boto3, and terraform. The provider reads the credential chain
as it expects to, gets a well-formed profile, and signs its requests with a
secret worth nothing.
The host repairs that signature on the way out. Its resigner recognises traffic
bound for *.amazonaws.com, strips the guest's doomed SigV4 signature, and
re-signs with the real secret before the request leaves your Mac. Your
terraform plan works. AWS answers a stolen copy of the credential with
InvalidSignatureException.
Then the fake tries to leave. The proxy scans the headers and body of every outbound request for the workspace's own placeholder values, using an Aho-Corasick automaton cheap enough to run on all of them. Send a fake to a host it was not minted for, a Slack webhook say, and the proxy answers HTTP 451 and forwards not a byte. The VM pauses. A red Credential brokering row lands in the Security Timeline, and Bromure marks the workspace compromised, so your next launch offers to wipe its disk and home while keeping your tokens, SSH keys and settings.
The C2 still has to get out of the machine. Egress is an ordered ruleset of allow and deny
rules, matched top to bottom, ending in default allow or default deny. A
workspace that deploys infrastructure needs registry.terraform.io,
releases.hashicorp.com and your cloud API, which leaves
portfolio-devs.slack.com and an Arbitrum RPC endpoint off the list. The host
enforces the rules twice: the virtual switch matches on destination IP and
DNS-snooped hostname, across TCP and UDP, and the proxy matches again on TLS
server name. The switch diverts port 80 and port 443 into the proxy with no
environment variable involved and nothing the guest can unset. Every verdict
becomes a Firewall row in the Security Timeline.
You approve the deletes
One more control sits between the provider and your account, and it is the one that counts when the attacker wants your infrastructure rather than your secrets.
AWS credentials carry a write policy, and new workspaces default to Prompt
before write. The proxy classifies each call by the action name in the
X-Amz-Target header or the Action= parameter: Get*, List* and Describe*
are reads and pass without a word, while Delete*, Terminate*, Revoke* and
their relatives count as destructive. A write pauses for a dialog titled
Allow write on "<scope>" from workspace "<name>"?, which shows the operation
verbatim rather than a summary, with Allow for 15 minutes, Allow once,
Allow for the rest of the session, and Don't allow. Refuse, and the guest
gets an AccessDeniedException the agent reports and moves on from.
You are the one who clicks that dialog, on your Mac. A process inside the VM cannot reach it, and neither can whoever is on the other end of the Slack channel.
Treat the toolchain as an executor
terraform init and terraform plan are download-and-run. Ask what a
provider executes inside, because it will execute.
Leave no live secret in reach
Give the provider a credential chain that satisfies it and a secret worth nothing. The host signs, on the far side of the boundary, scoped to the destination the key belongs to.
Name the destinations first
A deploy workspace needs a short, stable list: the registry, the release host, your cloud API. Write it once, set unmatched traffic to Deny, and a dead-drop contract has nowhere to poll.
Make the irreversible call ask
Let reads run quiet. Make Terminate* cost you one dialog on a screen the VM
cannot reach.
Nobody found a vulnerability here
HashiCorp's registry served the package it was asked for. Terraform verified the checksum it was given and ran the provider the way it runs every provider. No sandbox broke, no agent read a poisoned instruction, and no model misbehaved.
Two providers with 1,671 downloads between them reached at least 18 machines, most of them Macs, and sat there polling a smart contract every three seconds. Documented behaviour carried the whole chain. The one novel part, a payload that stays encrypted unless your variables are named a particular way, exists so that nothing can find it.
Decide these things before the session, then, because you will not catch them during it. You cannot scan a provider that has not decrypted its payload, and you will not pick the exfiltration out of traffic to Slack. Run the deploy somewhere the keys are decoys, the destinations are a list you wrote, and the machine that gets rooted is one you throw away.