OpenTofu Provider and Module Supply Chain Security

OpenTofu Provider and Module Supply Chain Security

Problem

Every tofu init or terraform init invocation performs a sequence of network operations that most engineers treat as boilerplate: download provider plugins from the OpenTofu Registry or a configured mirror, verify checksums against .terraform.lock.hcl, fetch module source archives from Git repositories, HTTP servers, or registry endpoints, and extract those archives before any infrastructure is provisioned. This initialization phase runs with the operator’s full cloud credentials present in the environment — AWS_ACCESS_KEY_ID, GOOGLE_CREDENTIALS, ARM_CLIENT_SECRET — because the same shell session is used for both tofu init and tofu apply. A crash or hang during init can leave infrastructure mid-provisioned, and a maliciously constructed response from any of these network sources can interact with the Go runtime that tofu is built on.

In April 2026, four medium-severity CVEs were disclosed against OpenTofu 1.9 through 1.11.5, all exploitable specifically during tofu init. CVE-2026-32280 describes maliciously crafted TLS certificate chains that include nested cross-signed certificates. When a tofu init process connects to a provider mirror or module source HTTPS server presenting such a chain, the Go crypto/x509 path-building algorithm enters exponential recursion, saturating the CPU on a single goroutine and stalling or crashing the tofu process. An attacker who controls even one HTTPS endpoint contacted during init — a third-party module source, a private mirror, or a BGP-hijacked registry — can exploit this without any authentication.

CVE-2026-32281 amplifies CPU usage through inefficient policy validation that triggers when processing certain provider configuration structures. Unlike CVE-2026-32280, this requires the attacker to influence provider configuration metadata returned by the registry, but the effect is the same: a CPU spike during tofu init that blocks the CI runner.

CVE-2026-32283 involves unauthenticated TLS 1.3 KeyUpdate records. When a Go HTTP client maintains a persistent connection to a server — which tofu init does when fetching multiple assets from a module source — a malicious server can send repeated KeyUpdate records, consuming memory in the Go TLS stack and eventually causing a deadlock. The connection never closes cleanly, and the tofu process hangs until the OS kills it or a timeout fires.

CVE-2026-32288 is the most direct: the Go archive/tar package does not bound memory allocation when processing old GNU sparse headers. A malicious module distributed as a zip archive containing a tar file with crafted GNU sparse headers can cause tofu to allocate gigabytes of memory during extraction, crashing the CI runner or triggering OOM killing. This requires no network interception — the malicious payload is the module archive itself.

OpenTofu is a fork of Terraform maintained by the Linux Foundation, and like Terraform, its release artifacts are Go binaries. The four April 2026 CVEs were all inherited through OpenTofu’s net/http, crypto/tls, and archive/tar dependencies — vulnerabilities in the Go standard library that any Go binary linking those packages inherits. The fixes arrived as a Go runtime version bump bundled into OpenTofu 1.10.7. Critically, OpenTofu’s release pull request on GitHub was publicly visible before the release was tagged. Engineers who watched the opentofu/opentofu repository saw the go.sum diff show a jump from Go 1.23.x to 1.24.2, cross-referenced the Go CVE database at pkg.go.dev/vuln/list, and identified the specific CVEs being patched — before the release announcement was published. HashiCorp Terraform had the same underlying Go dependency vulnerabilities but released its patch on a different schedule, creating a window where one tool was patched and the other was not.

Target systems: OpenTofu 1.9 through 1.11.5 (fixed in 1.10.7 and later), Terraform releases bundling Go versions older than 1.24.2 (verify with terraform version -json | jq .go_version), CI/CD pipelines running tofu init against third-party or community module sources.

Threat Model

  1. Malicious module archive provider. An attacker controls a GitHub repository used as a Terraform module source — either by compromising the repository owner’s credentials or by registering a typosquatted module path. They push a new tag containing a zip archive with GNU sparse tar headers crafted to trigger CVE-2026-32288. Any pipeline running tofu init against that module source allocates unbounded memory and is OOM-killed. If this happens during a tofu apply run, the provider state may be partially written, leaving real infrastructure orphaned without corresponding state entries.

  2. Provider mirror compromise or BGP hijack. An attacker who operates a malicious provider mirror, or who can poison BGP routes toward registry.opentofu.org, presents a TLS certificate with nested cross-signed chains. Connecting tofu processes spend exponential CPU time building certificate paths (CVE-2026-32280) and hang. The CI pipeline does not fail fast — it stalls for minutes or hours consuming a runner while blocking deployments. In a Kubernetes-based runner pool, this can exhaust the runner node’s CPU quota across multiple concurrent jobs.

  3. Patch-gap attacker. An adversary watches the opentofu/opentofu GitHub repository for release pull requests. The go.mod and go.sum diffs in the 1.10.7 release PR show the Go version bumped to 1.24.2. The attacker cross-references https://pkg.go.dev/vuln/list, identifies CVE-2026-32280, CVE-2026-32283, and CVE-2026-32288 as the motivation, and immediately begins scanning CI pipelines on public repositories for tofu version output indicating OpenTofu 1.10.6 or earlier. Pipelines that use third-party module sources and have not yet upgraded are targeted with malicious responses. The window between the public release PR and operators upgrading is measured in days to weeks.

  4. Legitimate module account takeover. A threat actor compromises the credentials of a trusted module maintainer on the OpenTofu registry or on GitHub. They inject additional required_providers blocks or malicious resource configurations into the module that exfiltrate AWS_ACCESS_KEY_ID or similar credentials via a DNS lookup or HTTP request during tofu apply. Because the module was previously trusted and its version is pinned in .terraform.lock.hcl, many teams do not re-audit it on routine upgrades.

The blast radius across all four scenarios is amplified by the fact that tofu init runs in the same process and environment as tofu apply. A crash during init that leaves a runner in an inconsistent state may cause the next scheduled run to execute tofu apply against stale local state. Exfiltration via a compromised module requires no elevated network permissions — cloud metadata endpoints (169.254.169.254) are reachable from most CI runners by default.

Configuration / Implementation

Upgrading OpenTofu and verifying the Go version

The immediate remediation for all four April 2026 CVEs is upgrading to OpenTofu 1.10.7 or later and confirming the bundled Go version is 1.24.2 or higher.

# Check current version
tofu version

# Upgrade on macOS
brew upgrade opentofu

# Upgrade on Debian/Ubuntu
sudo apt-get update && sudo apt-get install --only-upgrade opentofu

# Verify the bundled Go version — must be >= 1.24.2
tofu version -json | jq -r '.go_version'

For Terraform, the same Go version check applies:

terraform version -json | jq -r '.go_version'

Pin the minimum version in CI by adding a version guard at the top of your init script:

#!/usr/bin/env bash
set -euo pipefail

REQUIRED_GO_MINOR=24
REQUIRED_GO_PATCH=2

go_version=$(tofu version -json | jq -r '.go_version')
# go_version is in form "go1.24.2"
major=$(echo "$go_version" | sed 's/go//' | cut -d. -f2)
patch=$(echo "$go_version" | sed 's/go//' | cut -d. -f3)

if [[ "$major" -lt "$REQUIRED_GO_MINOR" ]] || \
   { [[ "$major" -eq "$REQUIRED_GO_MINOR" ]] && [[ "$patch" -lt "$REQUIRED_GO_PATCH" ]]; }; then
  echo "ERROR: OpenTofu bundled Go version $go_version is below go1.${REQUIRED_GO_MINOR}.${REQUIRED_GO_PATCH}" >&2
  exit 1
fi

Provider installation network isolation

Restrict tofu init to a private provider mirror so that the only HTTPS endpoints contacted during initialization are ones you control. This eliminates the BGP-hijack and malicious mirror threat vectors.

Create or update ~/.tofurc (or the project-local .terraformrc for Terraform) with a provider_installation block:

# ~/.tofurc
provider_installation {
  network_mirror {
    url     = "https://providers.internal.example.com/tofu-mirror/"
    include = ["registry.opentofu.org/*/*"]
  }
  # Deny all direct registry access
  direct {
    exclude = ["registry.opentofu.org/*/*"]
  }
}

The internal mirror must serve providers in the OpenTofu network mirror protocol format (the same format as registry.terraform.io mirrors). You can populate it using tofu providers mirror:

# Run once, from a trusted workstation, to seed the internal mirror
tofu providers mirror -platform=linux_amd64 -platform=darwin_arm64 ./mirror-dir/

# Publish mirror-dir/ to your internal HTTPS server
rsync -av ./mirror-dir/ providers.internal.example.com:/srv/tofu-mirror/

Set an environment variable to route all outbound HTTPS through a corporate proxy with TLS inspection for any registries that cannot be mirrored locally:

export HTTPS_PROXY="https://proxy.internal.example.com:8080"
export NO_PROXY="providers.internal.example.com"

Enforcing .terraform.lock.hcl integrity in CI

The lock file records exact provider checksums and must be committed to version control. Any deviation indicates either an unauthorized provider change or a compromised mirror.

Commit the lock file and fail CI if it changes without a corresponding pull request:

# In CI, after tofu init, verify the lock file was not modified
tofu init -lockfile=readonly

# Verify the lock file matches what is committed in Git
if ! git diff --exit-code .terraform.lock.hcl; then
  echo "ERROR: .terraform.lock.hcl has changed during CI init. Possible supply chain modification." >&2
  exit 1
fi

The -lockfile=readonly flag causes tofu init to fail immediately if it would need to update the lock file, rather than silently writing new checksums. This catches both legitimate upgrades that were not committed and malicious checksum substitutions.

When a provider upgrade is intentional, regenerate the lock file locally and review the diff in the pull request:

# Regenerate lock file for all target platforms
tofu providers lock \
  -platform=linux_amd64 \
  -platform=linux_arm64 \
  -platform=darwin_arm64

Add a CI check that blocks merging if .terraform.lock.hcl changes without a corresponding change to provider version constraints in versions.tf:

# .github/workflows/lockfile-check.yml
name: Lock file integrity
on: [pull_request]
jobs:
  lockfile:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - name: Verify lockfile matches committed version
        run: |
          tofu init -lockfile=readonly
          git diff --exit-code .terraform.lock.hcl

Module source pinning

Never reference a module source by branch name. Branches are mutable references; a branch can be force-pushed to point at a malicious commit after your pipeline last ran. Always pin to a specific tag or, for maximum integrity, a full commit SHA.

# Bad — branch is mutable
module "vpc" {
  source = "git::https://github.com/example-org/terraform-aws-vpc.git?ref=main"
}

# Better — tag is more stable, but can be re-tagged on some hosts
module "vpc" {
  source = "git::https://github.com/example-org/terraform-aws-vpc.git?ref=v3.14.0"
}

# Best — commit SHA is immutable
module "vpc" {
  source = "git::https://github.com/example-org/terraform-aws-vpc.git?ref=a7c3f2e1b4d5e6f7a8b9c0d1e2f3a4b5c6d7e8f9"
  version = "~> 3.14"
}

For registry modules, pin to an exact version and include it in the lock file:

module "s3_bucket" {
  source  = "terraform-aws-modules/s3-bucket/aws"
  version = "4.1.2"  # exact pin, not "~> 4.1"
}

In CI, prevent tofu init from upgrading module sources beyond what is pinned:

tofu init -upgrade=false

TLS certificate validation and timeout hardening

Configure a network timeout to ensure that TLS stall attacks (CVE-2026-32280, CVE-2026-32283) cannot hang CI indefinitely. OpenTofu respects the TOFU_NETWORK_TIMEOUT environment variable (value in seconds):

export TOFU_NETWORK_TIMEOUT=30

Point tofu at a controlled CA bundle rather than relying on the system certificate store, which may include CAs whose issuance policies you do not control:

export SSL_CERT_FILE=/etc/ssl/certs/internal-ca-bundle.pem

If you operate a TLS-inspecting proxy, add its CA certificate to that bundle. This also provides a second layer of certificate validation in front of the Go crypto/x509 path-building code — a well-implemented proxy that rejects malformed chains prevents CVE-2026-32280 payloads from reaching the tofu process at all.

Do not use TF_CLI_ARGS_init=-no-color as a substitute for any security control; that flag only affects terminal output formatting.

Monitoring OpenTofu for Go dependency CVEs

Set up ongoing monitoring so that the next batch of Go standard library CVEs is detected before they can be exploited in your pipelines.

For teams using OpenTofu as a Go library:

# Install govulncheck
go install golang.org/x/vuln/cmd/govulncheck@latest

# Scan your module
govulncheck ./...

For teams running the OpenTofu CLI binary, check the bundled Go version against the Go vulnerability database:

# Emit the Go version embedded in the binary
TOFU_GO_VERSION=$(tofu version -json | jq -r '.go_version')
echo "OpenTofu built with: $TOFU_GO_VERSION"

# Compare against current Go release
curl -s https://go.dev/dl/?mode=json | jq -r '.[0].version'

Subscribe to OpenTofu release notifications:

# Watch the GitHub releases feed via gh CLI
gh api repos/opentofu/opentofu/releases --jq '.[0] | {tag_name, published_at, body}'

Set up a weekly CI job that checks the bundled Go version and opens a ticket if it is more than one minor version behind the current Go release:

# .github/workflows/tofu-go-version-monitor.yml
name: OpenTofu Go version monitor
on:
  schedule:
    - cron: '0 9 * * 1'  # Monday 09:00 UTC
jobs:
  check:
    runs-on: ubuntu-latest
    steps:
      - name: Check OpenTofu Go version
        run: |
          TOFU_GO=$(tofu version -json | jq -r '.go_version' | sed 's/go//')
          LATEST_GO=$(curl -s 'https://go.dev/dl/?mode=json' | jq -r '.[0].version' | sed 's/go//')
          echo "OpenTofu Go: $TOFU_GO, Latest Go: $LATEST_GO"
          TOFU_MINOR=$(echo "$TOFU_GO" | cut -d. -f2)
          LATEST_MINOR=$(echo "$LATEST_GO" | cut -d. -f2)
          if [[ "$LATEST_MINOR" -gt "$TOFU_MINOR" ]]; then
            echo "::warning::OpenTofu Go version is behind. Check for security patches."
          fi

Watch the OpenTofu go.mod and go.sum files for dependency bumps that may indicate ahead-of-announcement CVE patches:

# Check if go.sum has changed in the latest OpenTofu release compared to the previous one
gh api repos/opentofu/opentofu/contents/go.sum \
  --jq '.sha' \
  -H "Accept: application/vnd.github.v3+json"

Expected Behaviour

Signal Unpatched OpenTofu (≤ 1.10.6) Patched + hardened (≥ 1.10.7)
GNU sparse tar headers in module zip (CVE-2026-32288) tofu init allocates unbounded memory; CI runner OOM-killed; job fails with no useful error message tofu init processes archive normally; no unbounded allocation; job completes within memory limits
TLS chain with nested cross-signed certs (CVE-2026-32280) tofu init stalls at “Initializing provider plugins”; CPU at 100% on one core; job times out after runner hard limit Network timeout fires at 30 seconds (TOFU_NETWORK_TIMEOUT=30); job fails fast with timeout error; alert fires
Lock file drift detected in CI tofu init silently writes new checksums to .terraform.lock.hcl; changed file not caught unless diffed explicitly -lockfile=readonly causes immediate failure: Error: the lock file cannot be updated; PR gate blocks merge
Third-party module source serves malicious archive Archive extracted without integrity check beyond checksum if checksum not in lock file; malicious files written to .terraform/modules/ Module pinned to commit SHA; -upgrade=false prevents fetching new ref; lock file readonly prevents checksum substitution
OpenTofu release PR visible with Go version bump Patch-gap attacker has days to weeks to target pipelines before operators upgrade Weekly Go version monitor job detects version lag within 7 days; Renovate or Dependabot opens upgrade PR automatically

Trade-offs

Aspect Benefit Cost Mitigation
Private provider mirror Eliminates direct registry exposure; prevents malicious mirror and BGP-hijack vectors; enables air-gapped operation Operational overhead: mirror must be populated and kept current for every provider version used across all teams Automate mirror population with a scheduled job running tofu providers mirror; alert on 404s during init that indicate missing versions
-lockfile=readonly in CI Prevents silent checksum updates; makes supply chain changes visible as CI failures Blocks legitimate provider upgrades from landing without an explicit lock file regeneration commit Enforce a workflow: engineers run tofu providers lock locally, commit the updated lock file, and include it in the same PR as the version constraint change
Module version pinning to exact tag or SHA Immutable reference prevents mutable-branch attacks; makes supply chain changes auditable in Git history Misses security patches shipped in newer module versions unless pinned version is actively monitored Renovate or Dependabot with module source scanning; subscribe to upstream module release notifications
TOFU_NETWORK_TIMEOUT=30 Ensures TLS stall CVEs (CVE-2026-32280, CVE-2026-32283) fail fast rather than hanging CI for hours May cause false failures on slow registries or high-latency networks Increase timeout to 60 seconds as a fallback; route traffic through proxy to reduce external latency; use private mirror as primary

Failure Modes

Failure Symptom Detection Recovery
Private mirror missing a provider version tofu init fails: Error: no provider versions match the required constraints or 404 from mirror Mirror access logs show 404; CI job fails at init step; alert on init failure rate Populate mirror with the missing version using tofu providers mirror; temporarily allow direct registry access for that provider while mirror is updated
-lockfile=readonly blocks legitimate provider upgrade CI fails: Error: the lock file cannot be updated; engineer has bumped version constraint without regenerating lock file CI failure message explicitly names -lockfile=readonly as the cause Engineer runs tofu providers lock -platform=linux_amd64 -platform=darwin_arm64 locally; commits updated .terraform.lock.hcl; re-runs CI
TLS-inspecting proxy breaks provider signature verification tofu init fails: certificate signed by unknown authority or signature verification failed Error message in init output; absence of successful init logs in CI Add proxy CA certificate to SSL_CERT_FILE bundle; or configure proxy to passthrough TLS for registry.opentofu.org specifically and rely on OpenTofu’s own checksum verification
Module source tag deleted upstream tofu init fails: couldn't find remote ref refs/tags/v1.2.3 Init failure in CI; git fetch error in verbose output Vendor the module locally (copy module files into ./modules/ directory); update source to ./modules/vpc; remove git ref dependency; audit why the tag was deleted