Join our Newsletter — 33% off our NHI Course
Home› Guides› Terraform Module and Provider Supply Chain Guide: Pinning,…
Guide Software & AI Supply Chain

Terraform Module and Provider Supply Chain Guide: Pinning, Lock Files and Private Registries

← All guides
By NHI Mgmt Group, edited by Lalit Choda Updated 14 November 2026 16 min read
On this page

Every terraform init downloads code that will later run with your most privileged cloud credentials, and most teams have never decided which of that code they trust. Providers are executables that Terraform launches and hands the pipeline's cloud credentials to; modules are configuration written by someone else that can create any resource, run local commands and read any value passed into it. This guide covers the supply chain of those two dependency types in Terraform and OpenTofu: what the dependency lock file does and does not protect, how to pin modules that the lock file ignores, how signing and checksums work, and how to run provider mirrors and a private registry so that only reviewed versions reach production. It is for platform engineers, cloud security teams and anyone who owns an infrastructure as code pipeline. The credentials side (state files, provider credentials, OIDC) is covered in the Terraform and IaC Pipeline Credential Security Guide; this page does not repeat it.

Key takeaways

  • The lock file covers providers only. HashiCorp documents that the dependency lock file tracks only provider dependencies; modules are selected fresh from their version constraint on every init, so they need exact pins of their own.
  • Checksums are trust on first use. Terraform verifies each installed provider against hashes recorded the first time it was locked, so the security of every later run depends on reviewing the change that first added or upgraded that entry.
  • Git tags move, commits do not. A module sourced from Git without a ref follows the default branch, and a tag can be re-pointed; pin third-party Git modules to a full commit SHA.
  • Mirrors and a private registry turn trust into a decision. A provider_installation block with a network or filesystem mirror, plus a private module registry, means only versions someone approved can be installed.
  • Some settings silently switch verification off. dev_overrides disables version and checksum checks, and the plugin cache option that may break the lock file weakens it; neither belongs on a CI runner.

What terraform init actually pulls in

A Terraform root module has two kinds of external dependency, and they reach your environment by different paths with different protections. Treating them as one "dependency" problem is the root of most gaps.

DependencyTypical sourcesWhat it can do once installedBuilt-in verificationPrimary control
Provider pluginPublic Terraform or OpenTofu registry, network mirror, filesystem mirror, private registryRuns as a local executable with the credentials passed to its provider block or found in the environment; calls cloud and SaaS APIs on your behalfRegistry signature checked at install; package hashes checked against .terraform.lock.hclCommitted lock file with hashes for every platform, mirror or allowlist, reviewed upgrades
Registry modulePublic registry, private registryDeclares any resource the provider credentials allow, including IAM roles, network rules and data source reads; can include provisionersNone beyond the version constraintExact version pin, review of each new version, private registry copy
Git or VCS moduleGitHub, Bitbucket, generic Git over HTTPS or SSHSame as a registry moduleNone; follows whatever the ref resolves toref pinned to a full commit SHA, internal fork for third-party code
Object storage or HTTP moduleS3, GCS, HTTP archive URLsSame as a registry moduleNone at the Terraform levelBucket write access limited to the publishing pipeline, object versioning, prefer a registry
Nested (transitive) modulesWhatever the module author choseSame as a registry module, but invisible in your root configurationNoneInventory the full module tree, forbid floating sources in internal modules

Two module features deserve particular attention in reviews because they reach beyond cloud APIs: local-exec provisioners and the external data source both run programs on the machine executing Terraform, and the external data source does so during plan. A malicious or careless module change can therefore act on a CI runner before anyone approves an apply. This is why ATT&CK places the scenario under T1195.001 Compromise Software Dependencies and Development Tools, and why plan jobs need the same dependency controls as apply jobs.

Provider integrity: how the lock file and signing work

When terraform init (or tofu init) installs a provider, two checks happen. First, for registry installs, the release is signed: HashiCorp requires every provider release published to its registry to carry a SHA256SUMS file listing a SHA-256 checksum for each zip package, and a detached GPG signature of that file made with a key the publisher registered. Terraform verifies this during init. Second, Terraform records the selected version and a set of hashes in .terraform.lock.hcl, and on every later init it refuses any package that matches none of the recorded checksums.

Lock file elementWhat it meansSecurity implication
versionThe exact provider version selected from the constraintsinit re-selects this version even when a newer one exists, until someone runs init with -upgrade
constraintsThe constraints that were in forceInformational only; Terraform does not use it to decide what to install
h1: hashesPreferred scheme: a SHA-256 hash of the package contents, the same whether zipped or unpackedWorks for mirrors and caches; the hash you want present for every platform you run on
zh: hashesLegacy scheme: SHA-256 of each official zip package indexed in the origin registryOnly meaningful for the registry's own zip files, not for unpacked mirror layouts

HashiCorp describes this model as trust on first use. The lock file proves that today's package is the same as the one recorded earlier; it says nothing about whether the recorded one was good. The control that matters is therefore the review of the pull request that changes the lock file:

  • Commit the lock file: HashiCorp recommends keeping it in version control so dependency changes go through code review like any other change. A pipeline that runs init without a committed lock file trusts whatever the registry serves at that moment.
  • Lock for every platform: a provider installed from a mirror gets checksums only for the platform where init ran, and the configuration then fails on other platforms. Teams react by deleting the lock file. Use terraform providers lock -platform=linux_amd64 -platform=darwin_arm64 (one flag per platform you use) so the file carries hashes for developer laptops and runners alike.
  • Treat -upgrade as a change request: init with -upgrade ignores existing selections and picks the newest version allowed by the constraints. Run it in a dedicated branch, read the provider changelog, and let the lock file diff be the thing reviewers approve.
  • Enforce signatures where the tool allows it: OpenTofu documents an OPENTOFU_ENFORCE_GPG_VALIDATION environment variable that, when set to true, restricts installation to providers signed with a cryptographic signature. Init output shows the signing key ID, which is worth capturing in pipeline logs.

Lock files written by Terraform and OpenTofu share the same name and hash schemes, but the provider addresses differ (registry.terraform.io versus registry.opentofu.org), so a team moving between the two should regenerate and review the file rather than assume it carries over.

Pinning modules the lock file ignores

Because the lock file does not cover modules, Terraform selects the newest module version that satisfies the constraint on every fresh init. A constraint such as ~> 5.0 on a public module means a new minor release published by its maintainer can reach your plan without any change in your repository. HashiCorp's own guidance for modules is to require specific versions so updates happen only when you choose.

Dependency and contextRecommended constraintReason
Third-party registry module in a root moduleExact version, for example version = "5.8.1"Nothing else records which module version ran
Internal registry module with disciplined semantic versioningExact version in production roots; a range is acceptable in sandboxesHashiCorp allows ranges where your organisation consistently uses semantic versioning
Third-party Git module?ref= a full commit SHANo ref follows the default branch; tags can be moved to new commits
Internal Git moduleRelease tag, protected so tags cannot be rewritten, or a commit SHAReadable versions, while protection makes the tag immutable in practice
Providers in a root module~> with a lower and upper bound, plus the lock fileHashiCorp recommends root modules bound both ends; the lock file fixes the exact build
Providers in a reusable moduleMinimum version onlyHashiCorp recommends reusable modules set a floor so callers can resolve one version

Two practical points make pinning stick. Variables used in a module source or version must be declared with const = true, because modules install at init before plan values exist; avoid building sources dynamically at all, because a reviewer reading the root module should see exactly what is fetched. And pinning a root module does nothing for the modules it calls: open each third-party module and check its own sources, since a pinned module that calls a child module on a floating Git branch reintroduces the risk one level down. Automated update tools can raise pull requests for new module and provider versions, which keeps exact pins from going stale; see the Dependency Update Automation Guide.

Mirrors, private registries and the provider installation policy

Pinning decides which version you want; the installation path decides where it can come from. The CLI configuration file's provider_installation block replaces Terraform's default behaviour, which is to look in local mirror directories and then download directly from the origin registry.

  • network_mirror: an HTTPS URL (with a trailing slash) serving providers under the network mirror protocol. HashiCorp's documentation warns not to configure network mirror URLs you do not trust, because the mirror decides what you receive the first time a hash is recorded.
  • filesystem_mirror: a local or mounted directory of provider packages, packed or unpacked. Suited to air-gapped runners and golden runner images.
  • direct: the origin registry. Use include and exclude patterns to decide which providers may still be fetched directly; where both are set, exclusions take priority. Note that the pattern */* means registry.terraform.io only, not every host.
  • Building the mirror: terraform providers mirror downloads the providers a configuration needs (using the lock file versions by default) for each -platform you name, and writes the JSON index files needed to serve the directory as a network mirror. Run it in a controlled pipeline from reviewed configurations, so the mirror only ever contains approved versions.

For modules, a private registry gives the same chokepoint. In HCP Terraform and Terraform Enterprise the private registry is visible only to members of the organisation, can synchronise selected public providers and modules so teams use a curated set, and supports policies (Sentinel is the documented example) that require modules to come from the private registry. Publishing rights sit with the owners team and teams granted the Manage private registry permission, which makes that permission a supply chain control in its own right: review who holds it as part of your Terraform Cloud and HCP Security Guide baseline. Tag-based publishing creates a version for each semantic version tag pushed to the connected repository; deleting the tag later does not remove the published version, so a bad release must be deleted in the registry itself.

Choosing an installation model

ModelFitsTrade-offs
Direct from public registry with committed lock fileSmall teams, a handful of providers, no regulatory supply chain requirementLowest effort; trust rests on registry signing and lock file review; no control over what a fresh init can fetch for new providers
Direct with include allowlistMid-sized teams that want a short list of permitted provider namespacesBlocks unapproved providers without running infrastructure; versions still come straight from the registry
Internal network mirror plus private module registryLarge organisations, many teams, audit requirements for software provenanceMirror and registry become critical infrastructure with their own access control; strongest control over what can be installed
Filesystem mirror baked into runner imagesAir-gapped or highly regulated environmentsFully deterministic; every provider upgrade requires an image rebuild

Small organisations get most of the value from three things: a committed multi-platform lock file, exact module pins and a short provider allowlist. Mirrors and a private registry pay off once several teams share modules and you need to prove which code built which environment.

Where identity fits

The supply chain has its own non-human credentials, separate from the cloud credentials the pipeline uses. Private registries and private Git module sources need tokens: Terraform reads registry tokens from credentials blocks in the CLI configuration or from TF_TOKEN_ environment variables named after the host (supported from Terraform 1.2.0), and HashiCorp notes that organisation tokens cannot be used for CLI actions, so pipelines use team tokens. Give plan and apply jobs a read-only registry or deploy key token, keep publishing rights in a separate release pipeline identity, and rotate both. A token that can publish a module version to your private registry is equivalent to write access on every environment that consumes that module. For token handling patterns in pipelines, see the CI/CD Pipeline Identity Security Guide.

Rolling it out

  1. Inventory: run init across every root module, collect each .terraform.lock.hcl and the resolved module tree (the .terraform/modules/modules.json file lists sources and versions), and list every provider namespace and module source in use.
  2. Fix the lock files: commit a lock file for every root module, regenerate it with terraform providers lock for all platforms you run, and add a CI check that fails if init would modify it (run init with -lockfile=readonly).
  3. Pin modules: change every third-party registry module to an exact version and every third-party Git source to a commit SHA. Do internal modules next, starting with those that create IAM, network and logging resources.
  4. Restrict the installation path: ship a CLI configuration to runners with a provider_installation block, first with an include allowlist on direct, later with a network mirror. Test on one pipeline, compare the lock file before and after, then widen.
  5. Stand up the private registry: publish internal modules there, synchronise the approved public ones, and add a policy that warns (advisory mode) on modules from elsewhere before enforcing it.
  6. Automate upgrades: let an update bot raise pull requests, require a named reviewer for lock file and module version changes, and record the provider changelog link in the pull request.

Rollback is simple because every step is configuration: revert the lock file or pin to restore the previous version, or remove the provider_installation block from the runner image to fall back to direct installs. Keep the previous mirror contents rather than deleting them when you add new versions.

Detection and measurement

Most supply chain signals in Terraform appear as changes in files you already control, which makes detection cheap.

  • Lock file diffs: alert on any pull request that adds a provider or changes a version or hash set, and on lock file changes merged without review.
  • Init output: keep init logs from CI; they show where each provider was installed from and, for signed providers, the key ID. A provider suddenly coming from a different source or key deserves investigation.
  • Configuration drift in runners: check runner images and CLI configuration for dev_overrides, plugin_cache_may_break_dependency_lock_file or the TF_PLUGIN_CACHE_MAY_BREAK_DEPENDENCY_LOCK_FILE variable.
  • Registry activity: review publish and delete events in the private registry and changes to who holds publishing permission.
  • Static checks: IaC scanners can flag unpinned module sources and missing version constraints before merge; see the Checkov and IaC Scanners Guide.
MetricTarget
Root modules with a committed lock file containing h1 hashes for every runner platform100%
Third-party module references pinned to an exact version or commit SHA100%
Provider installs in CI served from the mirror or allowlist (not unrestricted direct)Rising to 100% in regulated environments
Lock file and module version changes merged with a second reviewer100%
Median age of provider versions behind latest approved releaseWithin your patch window
Identities holding private registry publishing rightsSmall, named, reviewed quarterly

Framework mapping

FrameworkReferenceHow this guide applies it
MITRE ATT&CKT1195.001 Compromise Software Dependencies and Development ToolsMalicious or tampered providers and modules pulled at init
NIST SP 800-53 Rev. 5SR-3 Supply Chain Controls and Processes; SR-4 Provenance; SR-11 Component AuthenticityApproved sources, recorded versions and hashes, registry signatures
NIST SP 800-53 Rev. 5CM-14 Signed Components; SI-7 Software, Firmware, and Information IntegritySignature validation and checksum enforcement at install
NIST SP 800-53 Rev. 5CM-7(5) Authorized Software: Allow-by-exceptionprovider_installation allowlists and private registry policy
NIST CSF 2.0GV.SC-07; ID.AM-02; PR.PS-02Assessing supplier risk, inventorying software in use, keeping it maintained

For how build and release provenance standards apply more widely, see the SLSA and Build Provenance Guide.

Common failure patterns

  • Deleting the lock file to fix a platform error: a lock created on a Mac fails on a Linux runner, someone removes it, and every later run trusts whatever the registry serves. Generate hashes for all platforms instead.
  • Floating module constraints: ~> ranges or no version at all on public modules, so a maintainer's new release, or a compromised maintainer account, changes production on the next init.
  • Git modules on a branch or tag: third-party modules sourced from the default branch or a tag that can be re-pointed. Commit SHAs remove the ambiguity.
  • Running fetched code without checking it: pipelines that download a helper script or binary for Terraform jobs and run it directly. In the Codecov Breach, a modified uploader script sent CI environment variables to an attacker for about two months until a customer compared it with the published checksum.
  • Verification switched off for convenience: dev_overrides left in a shared CLI configuration, or the plugin cache told to ignore the lock file, quietly bypass the checks this guide relies on.
  • Over-wide publishing rights: many people or tokens able to publish to the private registry, turning one compromised account into a change to every consuming workspace.

Practitioner checklist

  • Commit .terraform.lock.hcl for every root module with h1 hashes for every platform you run.
  • Run CI init with -lockfile=readonly so unreviewed provider changes fail the job.
  • Pin every third-party registry module to an exact version and every Git module to a full commit SHA.
  • Check nested modules for floating sources, not just your root configuration.
  • Ship a provider_installation block to runners: allowlist first, mirror when scale or audit needs it.
  • Keep dev_overrides and lock-file-breaking cache settings out of shared and CI configurations.
  • Restrict private registry publishing to a release pipeline identity and a small named team.
  • Use read-only, host-scoped registry tokens in plan and apply jobs and rotate them.
  • Route provider and module upgrades through pull requests with a second reviewer.

Standards and references

Related NHI Mgmt Group resources: Terraform and IaC Pipeline Credential Security Guide · Terraform Cloud and HCP Security Guide · Malicious Package and Dependency Confusion Guide · Atlantis Security

Free weekly newsletter

Subscribe to the NHI & AI Identity Journal

The latest on NHI and Agentic AI security – articles, research, breaches, news and events every week.

Bonus 33% off our NHI Course when you subscribe.

NHIMG Editorial Note
Researched and written by the NHI Mgmt Group editorial team using AI tools, under the editorial direction of Lalit Choda. Facts are drawn from the sources listed on this page and checked by our automated quality process. Last updated 14 November 2026. Spotted an error? Use our contact page.
NHI Mgmt Group, the independent authority on Non-Human Identity, IAM and Agentic AI security. nhimg.org