Skip to main content
A DPRK-linked group is mailing DevOps candidates Terraform take-home repos whose lock file points at a fake registry. terraform init then runs the attacker's provider.

The Terraform Lock File Is Code: Review It Before You Init

KU
Kiril Urbonas
5 days ago • 6 min read•1 view

A DPRK-linked group is mailing DevOps candidates Terraform take-home repos whose lock file points at a fake registry. terraform init then runs the attacker's provider.

Key takeaways

A DPRK-linked group is mailing DevOps candidates Terraform take-home repos whose lock file points at a fake registry. terraform init then runs the attacker's provider.

Treat .terraform.lock.hcl as executable input, because that is what it is. SentinelOne Labs describes a campaign attributed to TraderTraitor (also tracked as UNC4899), a DPRK-linked Lazarus subgroup, in which fake job interview repositories carry a weaponized lock file. When the candidate runs terraform init, Terraform treats the attacker's registry as the source of truth and installs a provider that is really a backdoor loader. The fix is cheap: read the hostnames in the lock file before you init, and configure Terraform so it can only install from hosts you chose.

What the attack actually does#

The lure is a take-home exercise for a cloud or infrastructure-as-code role. SentinelOne lists repository names such as terraform-candidate-repo, Northwind-IAC and novacart-interview. Each contains a .terraform.lock.hcl that points provider installs at a typosquatted registry. The published examples are lookalikes of HashiCorp's own domain, such as registry.hashicorp-aws[.]com. Run terraform init and Terraform downloads the provider and, on plan or apply, executes it as a local process with your user's privileges.

The payloads are two Rust backdoors for Apple Silicon Macs, FLATROOF and ROOFDECK. SentinelOne found them on the machine of a DevOps engineer at an India-based IT services provider with no cryptocurrency ties. Most of us filed this actor under crypto exchanges and moved on. A cloud engineer with source control access and cloud credentials is a better target than a wallet, and the report's own advice is to treat that group as sensitive for endpoint monitoring.

Why the lock file is the weak spot#

Everyone reviews main.tf. Almost nobody reads the lock file, because it is generated, verbose and full of h1: and zh: hashes that look like noise. A reviewer skimming a pull request sees a wall of base64 and scrolls past it.

The lock file also carries the one field an attacker needs. Every entry starts with the full provider source address, hostname included, like provider "registry.terraform.io/hashicorp/aws". Change the hostname and the hashes that follow are hashes of the attacker's package, which will verify perfectly against themselves. Checksums prove that the package you got matches the lock file. They say nothing about whether the lock file itself was honest.

This is the same trust problem we covered in hardening CI against npm worms: a lockfile pins what you resolve, and it is only as trustworthy as whoever wrote it. When the repo comes from a stranger, the lock file is part of the attack surface, not a defence.

Read the hosts before you run anything#

Before terraform init on any repository you did not write, list the registry hostnames the lock file names. The upstream default is registry.terraform.io, and the SentinelOne guidance is to treat anything else as suspect.

Also check required_providers in the .tf files, since a source address can name a host too. Here is a check you can run locally or drop into CI:

bash.bash
# scripts/check-lock-hosts.sh
#!/usr/bin/env bash
set -euo pipefail
[ $# -gt 0 ] || exit 0
allowed='^(registry\.terraform\.io|tf-mirror\.example\.com)$'
bad=$(grep -hoE '^provider "[^"]+"' "$@" | cut -d'"' -f2 | cut -d/ -f1 | sort -u | grep -vE "$allowed" || true)
if [ -n "$bad" ]; then
  echo "Unexpected provider registry hosts:" >&2
  echo "$bad" >&2
  exit 1
fi
bash.bash
$ chmod +x scripts/check-lock-hosts.sh
$ find . -name .terraform.lock.hcl -not -path '*/.terraform/*' -print0 | xargs -0 ./scripts/check-lock-hosts.sh
$ grep -rnE 'source\s*=\s*"[^"]*\.[^"/]+/' --include='*.tf' .

The first command gates the lock files. The last line finds explicit hostnames in source arguments, which you should read by eye. A mismatch against your allowlist is a failed build, and it should fail before any job that holds cloud credentials runs init. Run it as the first step of the pipeline, ahead of the provider download.

Make Terraform refuse the wrong hosts#

A grep is a review habit, and habits lapse. The durable control is the CLI configuration file, where provider_installation decides which install methods apply to which providers. Terraform's documentation describes three methods: direct, filesystem_mirror and network_mirror. Each takes include and exclude patterns, and when both are set the exclusions win.

hcl.hcl
# ~/.terraformrc
provider_installation {
  network_mirror {
    url     = "https://tf-mirror.example.com/providers/"
    include = ["registry.terraform.io/*/*"]
  }
  direct {
    include = ["registry.terraform.io/hashicorp/*"]
  }
}

With this config, a provider whose address sits on any other host matches no method, so init has nothing to install it with. A poisoned lock file turns into an error instead of a compromise. For CI, use the mirror alone and drop direct entirely, so runners never talk to a public registry. Terraform reads this file from your home directory on Unix, so bake it into the runner image, not the repository.

Two related habits pay off here. Run terraform providers lock -platform=linux_amd64 -platform=darwin_arm64 when you generate your own lock file, so it records checksums for every platform your team uses and nobody regenerates it from an unknown source. And generate it from a mirror you control. Our notes on fixing provider version conflicts cover the lock file's normal behaviour, which is worth knowing before you start treating diffs in it as security events.

Run untrusted repos somewhere disposable#

Everything above still assumes you noticed. For interview exercises and unknown samples, the safer default is not to run them on your laptop at all. Use a throwaway VM or a devcontainer with no mounted home directory, no cloud credentials in the environment, and no access to your SSH agent or keychain. The backdoors in this campaign went after browser data and keychains, and a container that never sees them has nothing to hand over.

The decision, concretely#

  • Received a Terraform repo from someone you do not know? Read the lock file hosts first, and run it only in a disposable VM or devcontainer with no credentials.
  • Running init in CI? Add a host allowlist check as the first step and fail the build on any host outside registry.terraform.io or your own mirror.
  • Managing engineer laptops or runners? Ship a ~/.terraformrc with provider_installation that names your mirror and excludes everything else.
  • Reviewing a pull request that touches .terraform.lock.hcl? Read the hostname on every changed provider line, not just the version.

The call we'd make#

Ship the provider_installation config to every runner and laptop, run the host check in CI, and stop initializing strangers' repos on a machine that holds credentials. The first two take an afternoon and turn this whole attack class into a failed build. The third is a habit, and it is the one that would have protected the victim SentinelOne found.

React

Get the DevOps Troubleshooting Cheat Sheet

Subscribe and get our free one-page reference for the errors that eat an afternoon — CrashLoopBackOff, OOMKilled, Terraform state locks, and more — plus new guides as we publish them.

Share this post
KU

About Kiril Urbonas

DevOps Engineer

549 articles
View all articles by Kiril Urbonas

You might have missed

Evergreen posts worth revisiting.