
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.
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:
aws, azurerm, google, and thousands more) that translate your HCL into API calls against a cloud or SaaS platform.terraform/tofu commands.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.
| 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.
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.
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:
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.
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.
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.
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.
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.
1. HashiCorp is the latest victim of Codecov supply-chain attack – BleepingComputer
4. GitHub Action tj-actions/changed-files supply chain attack – Wiz Blog
5. Terraform Dependency Lock File – HashiCorp Developer Docs
