TrademarkTrademark
Features
Documentation

Terraform Supply Chain Attacks: Risks, Real Incidents, and How to Defend Against Them

A Terraform supply chain attack compromises the providers, modules, CI/CD pipeline, or signing infrastructure that produce your infrastructure, not a misconfigured resource. Here's how they happen and how to defend against them.
Ryan FeeAugust 12, 2026
Terraform Supply Chain Attacks: Risks, Real Incidents, and How to Defend Against Them
Key takeaways
  • A Terraform supply chain attack targets the provider, module, CI/CD pipeline, or signing infrastructure that builds your infrastructure, not a misconfigured resource. One poisoned dependency can compromise every workspace that pulls it.
  • The Terraform dependency lock file cryptographically pins providers with SHA256 checksums, but modules only get a version constraint. BoostSecurity Labs found several hundred of the public registry's 13,000+ modules exploitable through tag mutation and pull_request_target abuse, neither of which the lock file catches.
  • The 2025 tj-actions/changed-files compromise (CVE-2025-30066) showed how one poisoned GitHub Action can leak secrets out of thousands of CI pipelines, including the ones holding the cloud credentials Terraform runs use to call terraform apply.
  • HashiCorp itself has already been a supply chain casualty: the 2021 Codecov Bash Uploader compromise exposed HashiCorp's GPG release-signing key, forcing a key rotation and re-signing of existing Terraform releases.
  • Defense means treating modules with the same rigor as providers: pin exact versions or commit SHAs, source both from a private registry instead of pulling straight from the public one, gate risky provisioners with policy as code, and run CI/CD service accounts at least privilege.

This post is part of a series on IaC Security: Securing Your Terraform and OpenTofu Infrastructure.

Most Terraform security advice is about what you write: don't hardcode a password, don't open port 22 to the world, encrypt the bucket. A supply chain attack skips all of that. It doesn't touch your HCL at all. It compromises something upstream that your HCL trusts by default: a provider binary, a module source, a GitHub Action in your pipeline, or the signing key that's supposed to prove a release is genuine. Get one of those, and you inherit access to every workspace that pulls it in on the next init or apply.

This isn't a hypothetical. HashiCorp has already had its own release-signing key exposed by a third-party compromise. Security researchers have shown, at scale, that hundreds of modules in the public Terraform Registry could be hijacked without tripping a single lock file check. And a 2025 GitHub Actions compromise proved that the CI pipeline running terraform apply with your cloud admin credentials is just as much a target as the registry itself. Here's what a Terraform supply chain attack actually looks like, what's already happened, and what closes the gap.

What is a Terraform supply chain attack?

A Terraform supply chain attack is a compromise of something Terraform pulls in and trusts automatically, rather than a bug in your own configuration. The dependencies in scope fall into four groups:

  1. Providers — the plugins (aws, azurerm, google, and thousands more) that translate your HCL into API calls against a cloud or SaaS platform.
  2. Modules — reusable configuration, whether from the public registry, a private registry, or a Git source, that your own configurations call.
  3. CI/CD dependencies — the GitHub Actions, GitLab CI templates, and installer scripts your pipeline runs alongside terraform/tofu commands.
  4. Signing and distribution infrastructure — the GPG keys, checksums, and download endpoints that are supposed to prove a Terraform release or provider binary is what it claims to be.

Compromise any one of these and the attacker doesn't need to trick a human into approving a bad pull request. The malicious code runs the moment a workspace does what it's supposed to do: run init, plan, or apply. That's what separates a supply chain attack from a misconfiguration. A misconfigured security group is a mistake in code someone reviewed. A supply chain attack is malicious code that was never in the diff at all.

The Terraform and OpenTofu supply chain attack surface

Layer What it is Typical attack Does the lock file catch it?
Providers Plugins like aws, azurerm, google Backdoored binary, typosquatted namespace, compromised maintainer account Yes — SHA256 checksum in .terraform.lock.hcl
Modules Reusable HCL from a registry or Git source Tag mutation/hijacking a published version, typosquatting a popular module name No — only a version constraint is recorded
CI/CD pipeline GitHub Actions, GitLab CI templates, installer scripts Poisoned third-party Action exfiltrates secrets from the job that runs apply No — outside Terraform's scope entirely
Signing infrastructure GPG keys and checksums used to verify releases Key exposure via a compromised build dependency Partially — verification only works if the key itself is trustworthy

The asymmetry between the second and third rows and the first is the crux of the problem. Most teams think "we pin our providers, we're covered." Pinning a provider is real protection. It does nothing for a module, and nothing at all for the CI job that resolves both.

Real Terraform supply chain incidents (not hypotheticals)

HashiCorp's GPG key, exposed by someone else's compromise (2021)

In 2021, attackers modified Codecov's Bash Uploader script, a tool widely embedded in CI pipelines to send test coverage data, so that it exfiltrated environment variables and secrets from any CI/CD environment that ran it. HashiCorp was one of the organizations affected. The exposure included the GPG private key HashiCorp used to sign Terraform and Vault releases. HashiCorp rotated the compromised keypair, published a new one, and re-signed existing releases to restore trust in the verification chain. Nothing in this incident required a single Terraform user to do anything wrong. The compromise happened three steps removed, in a coverage-reporting tool, and it still reached the key that every terraform init implicitly trusts.

Registry-wide module hijacking research (2024)

Security researchers at BoostSecurity Labs analyzed the entire public Terraform Registry, more than 3,000 providers and 13,000+ modules, and found several hundred modules that could be hijacked through two techniques neither Terraform nor the lock file defends against:

  • Tag mutation ("tag attacks"): Where a repository lacks tag protection rules, anyone with write access can move a release tag to point at a different, malicious commit. Because module version resolution follows the tag, not a fixed hash, a trusted, already-published module version can be silently swapped out from under every configuration that references it.
  • "Pwn request" exploitation: Workflows triggered on pull_request_target can be tricked into running with elevated permissions or secret access when the workflow checks out and runs code from the pull request itself. A malicious PR author can use this to publish a new module version as if they were the legitimate maintainer.

The researchers also demonstrated a runtime path: a malicious module using a local-exec provisioner, or an HTTP data source, to exfiltrate state data or run arbitrary commands the moment someone runs apply. No public incident from this research was disclosed; the vulnerabilities were reported to maintainers responsibly. That's arguably the more sobering point: this is a live, exploitable weakness in the public registry's trust model, not a one-off.

tj-actions/changed-files: when the CI pipeline is the target (2025)

In March 2025, security teams discovered that a popular third-party GitHub Action, tj-actions/changed-files, had been compromised (CVE-2025-30066). The modified action dumped CI/CD secrets into workflow run logs, which are readable by anyone who can view the workflow run in a public repository. Because so many pipelines pull this action in as a dependency without pinning it to a fixed commit, the compromise reached an enormous number of unrelated projects, not through Terraform at all, but through the same class of pipeline that runs terraform apply with cloud credentials in hand. If your Terraform CI/CD job pulls in third-party Actions by tag or branch, this is the exact failure mode: the thing your pipeline trusts gets swapped out, and it has your secrets by the time anyone notices.

A signing-key expiry as a reminder of how much rides on the trust chain (2026)

Not every trust-chain failure is an attack, but the April 2026 HashiCorp GPG key expiry is worth knowing about for the same reason: it showed how much of the ecosystem depends on that one signature staying valid. HashiCorp's release-signing key's self-signature lapsed on April 18, 2026, and tools that shipped an embedded copy of the old key, including Atlantis, hc-install, tenv, and tfswitch, started failing signature verification on live CI runs. Nobody's infrastructure was compromised, and the underlying key was already extended through 2030 before this happened. But it's a clean illustration of the same underlying dependency this whole post is about: your terraform init trusts a signature it didn't generate, verified by tooling you probably didn't write, and when that link breaks, everything downstream feels it. For the full breakdown of what broke and the exact fixes, see HashiCorp GPG Key Expired: Fixing Terraform Download Errors.

Run terraform init and look at .terraform.lock.hcl. For every provider, you'll see a block like this:

provider "registry.terraform.io/hashicorp/aws" {
  version     = "5.94.1"
  constraints = "~> 5.0"
  hashes = [
    "h1:6Zn6y+0jw5S9iiRLzeCTIJ5+uMhKW6PLdD+bZuiPJ6c=",
    "zh:2c... (one entry per platform)",
  ]
}

That hashes block is a cryptographic commitment. If a provider binary doesn't match one of those hashes, terraform init refuses to proceed. That's real protection against a provider being swapped out after the fact.

Now look at the module block in the same file:

module "vpc" {
  source  = "terraform-aws-modules/vpc/aws"
  version = "~> 5.0"
}

There's no hash. A module version constraint like ~> 5.0 resolves to whatever the registry currently says version 5.x points at, and if a maintainer's repository lacks tag protection, that can be moved without anyone downstream noticing. This is precisely the asymmetry BoostSecurity's research exploited. It's not a flaw in how you wrote your configuration; it's a gap in what Terraform's dependency model verifies at all.

How to defend against a Terraform supply chain attack

None of these controls are exotic. Most teams already have half of them. The point is running all of them together, because a supply chain attacker only needs one gap.

Pin and commit your provider lock file. This is table stakes, but it's worth stating precisely: commit .terraform.lock.hcl to version control, never hand-edit it, and run terraform providers lock -platform=<each platform your pipeline uses> so CI and every developer machine verify against the same checksums. See What are Terraform Lock Files for the exact mechanics.

Treat modules like providers, not like a git clone. Pin to an exact version, or better, a Git commit SHA, instead of a branch name or a loose constraint. A commit SHA can't be silently moved the way a tag can.

Source modules and providers from a registry you control. Pulling directly from the public Terraform Registry means trusting every maintainer's repository hygiene, including their tag protection settings. A private module registry puts your platform team in the publish path: modules get vetted once, published to an internal catalog, and consumed by logical name instead of a Git reference anyone could redirect. Scalr's implementation, free up to 50 runs per month, auto-syncs from your VCS on a tagged SemVer release, so publishing stays a normal Git workflow rather than a manual upload. The same logic applies to providers: Scalr's private provider registry requires every version to be GPG-signed and checksum-verified against a key you manage before it's accepted, which is the exact protection modules lack by default in the public registry.

Pin third-party CI/CD dependencies to a commit SHA. This applies directly to the tj-actions lesson: uses: tj-actions/changed-files@v45 trusts whatever v45 currently points to. uses: tj-actions/changed-files@a1b2c3d... trusts one specific, auditable commit. Dependabot can still keep that pinned SHA current; you just review the diff instead of trusting a moving tag blindly.

Run CI/CD service accounts at least privilege. The tj-actions and Codecov incidents both turned a compromised dependency into a real breach because the CI job it ran inside held broad secrets. Scope the credentials your Terraform runs use as narrowly as the environment they touch, use OIDC for short-lived cloud credentials instead of long-lived static keys, and separate publish permissions (who can push a new module or provider version) from consume permissions (who can pull one) with role-based access control.

Gate dangerous runtime behavior with policy as code. A malicious module doesn't need a CVE if it can run a local-exec provisioner or exfiltrate state through an HTTP data source. Policy as code run against the plan can flag or block provisioners, unexpected data sources, and provider sources outside an approved allowlist before apply ever runs.

Keep dependencies current without opening the door to floating versions. Dependabot for Terraform and OpenTofu automates version bump pull requests you review, which is a meaningfully different security posture than a version constraint that resolves to "whatever's newest" at init time.

Scan for what slips through. Static analysis won't catch a hijacked tag, but it will catch a lot of what a malicious module tries to do once it's running. Layer it in alongside the controls above; see Getting Started with Terraform Vulnerability Scanning.

Where this is headed

The public registry ecosystem this all runs on has grown faster than its trust model has. Both Terraform and OpenTofu inherited the same module-versus-provider asymmetry, and the incidents above, HashiCorp's own key exposure, registry-wide module hijacking research, a CI Action compromise that reached thousands of unrelated pipelines, all landed in the last few years, not the distant past. We don't expect that to slow down.

Today, Scalr already narrows this attack surface in a few concrete ways: private module and provider registries that put your platform team in the publish path instead of the public registry's namespace, GPG signing and checksum verification on every private provider version, policy as code to allowlist approved sources and gate risky provisioners before apply, and usage reports that show which workspaces are running which module or provider version, so if one is ever disclosed as compromised, finding your exposure is a lookup, not an audit.

We're actively investing in hardening this layer further, and we'll have more to share on that soon. If securing what your Terraform and OpenTofu runs pull in and execute is a priority for your team, it's worth checking back here, this is a space we intend to keep building in publicly.

Key Sources Used

1. HashiCorp is the latest victim of Codecov supply-chain attack – BleepingComputer

2. Erosion of Trust: Unmasking Supply Chain Vulnerabilities in the Terraform Registry – BoostSecurity Labs

3. CISA: Supply Chain Compromise of tj-actions/changed-files (CVE-2025-30066) and reviewdog/action-setup (CVE-2025-30154)

4. GitHub Action tj-actions/changed-files supply chain attack – Wiz Blog

5. Terraform Dependency Lock File – HashiCorp Developer Docs

Frequently asked questions

What is a Terraform supply chain attack?

A Terraform supply chain attack is a compromise of something Terraform trusts and pulls in automatically, rather than a mistake in your own HCL. That includes a malicious or hijacked provider, a poisoned module, a compromised CI/CD dependency (like a GitHub Action) running in your pipeline, or a break in the signing chain used to verify Terraform releases and providers. Because a single dependency can be reused across hundreds of workspaces, one compromised link can affect infrastructure well beyond the team that introduced it.

Are Terraform modules protected by the dependency lock file the same way providers are?

No. The .terraform.lock.hcl file records a SHA256 checksum for every provider version, so a provider binary that doesn't match the recorded hash fails init. Modules don't get that treatment: the lock file only records a version constraint for a module, with no cryptographic hash tying it to specific source content. That asymmetry is exactly what BoostSecurity Labs' 2024 registry research exploited to hijack already-published, trusted module versions without tripping any lock file check.

Has Terraform or HashiCorp ever actually been hit by a supply chain attack?

Yes. In 2021, the Codecov Bash Uploader supply chain compromise exfiltrated CI/CD secrets from customers who used it, including HashiCorp. That exposure included HashiCorp's GPG private key used to sign Terraform and Vault releases, and HashiCorp had to rotate the key and re-sign existing releases. Separately, the 2025 tj-actions/changed-files GitHub Actions compromise (CVE-2025-30066) hit CI pipelines broadly, including many that run Terraform, by leaking secrets through action logs.

How do you prevent a Terraform supply chain attack?

Pin provider versions and commit the lock file so every teammate and CI run uses the exact checksummed binary. Pin modules to exact versions or a commit SHA instead of a floating branch reference, since the lock file won't catch a hijacked module. Source both providers and internal modules from a private registry you control rather than pulling directly from the public registry. Pin third-party GitHub Actions to a commit SHA, not a tag. Run CI/CD service accounts at least privilege so a compromised action can't reach cloud admin credentials. Layer in policy as code to block dangerous provisioners like unreviewed local-exec or remote-exec calls.

Does a private module or provider registry stop supply chain attacks?

It narrows the attack surface significantly, but it isn't a single silver bullet. A private registry means your teams consume modules and providers that your platform team has vetted and published, instead of resolving directly against the public registry's namespace, where tag mutation and typosquatting risks live. Scalr's private provider registry additionally requires every version to be GPG-signed and checksum-verified before it's accepted. You still need version pinning, CI/CD hardening, and policy as code around what a Terraform run is allowed to do, because a private registry controls what enters your catalog, not what a compromised CI job does with valid credentials.

Is OpenTofu more or less exposed to supply chain attacks than Terraform?

About the same, since OpenTofu forked from Terraform 1.5.7 and inherited the identical provider lock file model, module version-constraint gap, and Module Registry Protocol. Two differences are worth knowing: OpenTofu's registry indexing code is open source and auditable, unlike HashiCorp's closed-source backend, and OpenTofu's CLI warns rather than fails when a provider's GPG signing key has expired, a deliberate design choice made in late 2023. Neither changes the fundamental module-vs-provider trust gap described above. For the full technical breakdown, see [Terraform Module Supply Chain Attacks](/learning-center/terraform-module-supply-chain-attacks) and [Malicious Terraform Providers](/learning-center/terraform-provider-supply-chain-attacks).
About the author
Ryan Feedirector of platform engineering at Scalr
Ryan Fee is the director of platform engineering at Scalr, with over 15 years of experience improving infrastructure experiences at companies large and small.