← Back to all posts
Published on · by Renaud Deraison

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.

The documented path, and the one branch that was added to itthe registrykreuzwenker/dockerinitthe machine that deploysTerraform Coreplan → start, then RPCthe provider processa binary you downloadedthe credentials~/.aws, tokens, keysthe branch nobody asked for, inside the providersha256(containerName + networkID)match → AES key → decrypt → detached go run .every other configuration, and every scannerhash does not match; the payload is never decryptedthe one configuration it was addressed toa RAT, reporting hostname and hardware, then waiting18 machines checked in over 725 messages: 3 Windows, 5 Linux, 10 macOS
The chain has no exploit in it. terraform init resolves a provider from the registry and caches the binary; terraform plan starts it as a local process and speaks to it over RPC; the provider reads the ambient credential chain, which is the protocol. Graphalgo's providers add one branch: a hidden entry point that hashes two of your own Terraform variables, and only when the hash matches one hard-coded value does it use that hash as an AES key to decrypt a payload hidden in an example fixture and run it with a detached 'go run'. For every other configuration the code is inert.

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.

On the machine that deploysthe provider process, running as youreads ~/.aws → real secret keyreads git tokens, SSH keys, tfstateyour home directory is the blast radiusoutbound, unquestionedportfolio-devs.slack.com · every 10sArbitrum Sepolia RPC · every 3sboth destinations have good reputationsthere is no anomaly to alert onrecovery: rotate everything the laptop could reachIn a Bromure Agentic Coding workspacethe provider process, in the guestreads ~/.aws → access key ID + fake secretUbuntu 24.04, under the hypervisora disk you replace with a menu itemoutbound, matched against your rulesetslack.com → no rule → default denySepolia RPC → no rule → default denyand if the fake is carried off-scopeHTTP 451, no bytes forwarded, VM pausedrecovery: wipe and launch, then rotate nothing
The same provider binary, two machines. On a laptop it runs as you, reads a real secret key out of the credential chain, and reaches Slack and a public RPC endpoint because nobody wrote a rule against it. In a Bromure workspace it runs inside an Ubuntu guest under the hypervisor, finds an access key ID paired with a fake secret, and meets an ordered egress ruleset on the way out; carrying the fake somewhere it was not minted for returns HTTP 451 and pauses the VM. AWS calls that do belong upstream are re-signed on the host, so terraform still works, and destructive AWS actions pause for a dialog that renders on the Mac.

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.