TerraformXX · Supply Chain: Providers and ModulesProduction Terraform
Third-Party Module Risks
What you'll learn
- Explain the public Registry trust model: who publishes, who signs, who mirrors
- Identify maintenance signals that predict a module will keep receiving security fixes
- Recognise licence classes and the obligations each imposes on a commercial consumer
- Pin a third-party module to an exact version and configure the right upgrade workflow
- Quantify the cost of an unpinned module in concrete incident terms
Prerequisites
Verified against Terraform CLI 1.9.x · OpenTofu 1.7.x · HCL 2.0 · bpg/proxmox provider 0.66+ · hashicorp/local provider 2.5+ · hashicorp/null provider 3.2+ · hashicorp/random provider 3.6+ · hashicorp/http provider 3.4+ · Ubuntu 24.04 LTS · Debian 12 (Bookworm) · 2026-08-13
The Terraform Registry has tens of thousands of modules. A new module is published every day. Most are proof-of-concept; some are production-grade; some are malicious. The registry does not curate; the registry hosts. The trust model is opt-in by the consumer, not vetted by the platform.
Adopting a third-party module is a one-line change in HCL:
module "vpc" {
source = "terraform-aws-modules/vpc/aws"
version = "~> 5.0"
}
The cost of the one-line change is the cost of every line of HCL in the module. If the module grants wildcard IAM, the consumer has wildcard IAM. If the module calls a paid API, the consumer pays for it. If the module is abandoned, the consumer is stuck.
The public Registry trust model
The Registry is a hosting platform. Anyone with a GitHub
account can publish a module by tagging a repository that
follows the terraform-<PROVIDER>-<NAME> convention. The
Registry does not review the code; the Registry does not
verify the maintainer’s identity beyond the GitHub
account; the Registry does not enforce a maintenance
cadence.
The Registry does enforce:
- Repository format. The module must be in a
repository named
terraform-<PROVIDER>-<NAME>. The repository must be public on GitHub. - Tag format. The repository must have semver tags
(
v1.0.0,v1.1.0, etc.). - Tag signing. The Registry recommends signed tags; signing is not enforced.
- Required files. A
README.md, aLICENSE, and aversions.tfare required.
The Registry does not enforce:
- Code quality, tests, or examples beyond a minimum example.
- Maintainer identity beyond the GitHub account.
- A maintenance commitment.
- A security review of any kind.
The trust you place in a third-party module is your own. The Registry is the host; you are the auditor.
Your configuration
|
v
source = "terraform-aws-modules/vpc/aws"
|
v
Registry serves module tarball for v5.x.y
|
v
Downloaded bytes run inside your terraform plan/apply
|
v
Module's code executes with your credentials
The bytes run with your credentials. That is the supply-chain surface.
Maintenance signals
A module without maintenance is a module that will not get a security fix. A module that does not get a security fix is a module that will become a CVE. The maintenance signals:
- Recency of release. A module that has not released in 12 months is suspect. A module that has not released in 24 months is abandoned.
- Recency of commit. A repository whose last commit is in the last 30 days is alive. A repository whose last commit is in the last 12 months is alive but slow.
- Issue response time. Open issues from a year ago with no response is abandonment. Open issues from a week ago with a maintainer comment is engagement.
- Terraform core compatibility. When a new Terraform core ships, does the maintainer ship a compatible release within weeks? A module that lags two major core versions behind is at risk of breaking.
- Deprecation discipline. Does the maintainer deprecate features before removing them? Does the CHANGELOG note when a feature will be removed?
# Severity: READ-ONLY
gh repo view terraform-aws-modules/vpc/aws \
--json pushedAt,stargazerCount
{"pushedAt":"2026-08-09T15:32:00Z","stargazerCount":3127}
A pushedAt within the last week is a healthy signal.
A stargazerCount of 3000 is a popularity signal; it is
not a maintenance signal. A popular abandoned module is
still abandoned.
Licence classes
The licence is the legal contract. Three classes matter for a commercial consumer:
- Permissive. MIT, Apache 2.0, BSD-2-Clause, BSD-3-Clause, MPL 2.0. The consumer may use, modify, and redistribute the module. Attribution is required.
- Copyleft. GPL, LGPL, AGPL. The consumer’s obligations depend on the variant. AGPL in particular imposes obligations on network-distributed software. Internal use is usually fine; legal review is warranted.
- Unlicensed. A repository without a
LICENSEfile is, by default, all-rights-reserved. The consumer has no rights. The module cannot be adopted legally.
# Severity: READ-ONLY
gh repo view terraform-aws-modules/vpc/aws \
--json license --jq '.license.key'
# "apache-2.0"
A repository whose license.key is null is unlicensed.
A repository whose licence is GPL-3.0 may be acceptable
for internal use but warrants a legal review. A
repository whose licence is a custom “do not use in
production” text is, in effect, all-rights-reserved.
Pinning discipline
A module without a version = attribute downloads the
latest matching version on every terraform init. The
“matching version” is whatever the constraint resolves to
against the current Registry state. Two plans run a week
apart may resolve to different versions.
# Anti-pattern: no pin.
module "vpc" {
source = "terraform-aws-modules/vpc/aws"
}
# Acceptable: range pin.
module "vpc" {
source = "terraform-aws-modules/vpc/aws"
version = "~> 5.0"
}
# Conservative: exact pin.
module "vpc" {
source = "terraform-aws-modules/vpc/aws"
version = "= 5.9.0"
}
The trade-off:
- No pin. Highest agility, lowest reproducibility.
The next
initmay produce a different plan. - Range pin (
~> 5.0). Agility within the major, reproducibility across the major. PATCH and MINOR upgrades are picked up; MAJOR upgrades require a constraint edit. - Exact pin (
= 5.9.0). Highest reproducibility, lowest agility. Every upgrade is a deliberate PR.
For internal modules, a range pin is the right default. For third-party modules, the discipline is range pin plus a deliberate upgrade PR per MINOR. The PR is the audit trail: the diff between the old and new versions is reviewed; the plan is reviewed; the apply is reviewed.
The cost of an unpinned module
A concrete walkthrough. A team adopts a third-party module without a pin:
module "logging" {
source = "third-party/logging/aws"
}
Six months pass. The maintainer publishes a new MINOR version that:
- Renames an output from
log_group_arntolog_group_arn_v2. - Changes the default of
enable_xrayfromfalsetotrue. - Adds a CloudWatch Logs resource subscription to a new destination.
On the next CI run:
Error: Module does not have output "log_group_arn"
on main.tf line 42, in module "app":
42: log_group = module.logging.log_group_arn
The team is now blocked. The fix is to update the
consumer, but the consumer’s plan shows the new default
(enable_xray = true) as a change. The team is now
applying a behaviour change they did not audit, in a
production environment, under time pressure.
The cost:
- Hours of triage to identify the change.
- A rushed PR to update the consumer’s references.
- A rushed plan review under operational pressure.
- An apply that ships a behaviour change (X-Ray enabled) that was never reviewed by the security team.
- A post-mortem that names “no version pin” as the root cause.
If the module had been pinned to ~> 5.0 and the
upgrade PR had been reviewed, the change would have been
caught in code review. If the module had been pinned to
= 5.9.0 exactly, the change would have required a
deliberate PR to bump.
What a maintenance signal change looks like
A team adopts terraform-aws-modules/eks/aws at
version 19.0.0. The evaluation record notes a healthy
maintainer (terraform-aws-modules, sponsored by Anton
Babenko) and a release cadence of about one minor per
month.
Twelve months later, the audit’s diff:
eks: 19.x -> 20.x (MAJOR)
- 20.0.0 release date: 2026-04-15
- 21.0.0 release date: 2026-08-01
- maintenance signal: unchanged
- licence: unchanged (apache-2.0)
- security advisory: GHSA-xxxx-yyyy-zzzz addressed in 20.3.0
The audit confirms the maintainer is still active, the licence is unchanged, and the security advisory was addressed. The audit triggers a re-evaluation; the re-evaluation triggers an upgrade PR; the upgrade PR is reviewed and applied.
The cadence is the discipline. Without the cadence, the team does not notice the security advisory; the team does not upgrade; the team remains on a vulnerable version.
Production failure modes
-
No pin. A module’s new MINOR version is downloaded on the next
init. The new version has a renamed output. The consumer’s plan errors. The team triages under pressure. The fix is to require aversion =attribute on every module block. -
Range pin too permissive. A consumer pins to
>= 5.0.0with no upper bound. The maintainer ships a breaking6.0.0. The consumer’s init silently upgrades. The fix is to use a pessimistic constraint (~> 5.0) or an explicit upper bound (>= 5.0.0, < 6.0.0). -
No maintenance tracking. A team adopts a module without recording its maintenance signals. The maintainer walks away. The team’s quarterly audit has nothing to diff against. The fix is to record the initial maintenance signals in the evaluation record and to diff quarterly.
-
Licence ignored. A team adopts an unlicensed module because the code “looks fine”. The legal team flags the adoption six months later. The team rewrites the module under deadline. The fix is to make the licence check a CI gate.
-
Mirror not updated. A team mirrors the Registry internally for compliance. The mirror falls behind. A consumer’s plan against the mirror resolves to an older version than the public Registry. The diff is invisible. The fix is a nightly sync with an alert on failure.
-
Abandoned module in critical path. A third-party module is in the team’s networking stack. The maintainer abandons the repository. The next Terraform core release breaks the module. The team’s upgrade path is a fork. The fix is a quarterly review that flags modules whose
pushedAtis older than 12 months and to maintain a fork plan.
Security implications
- A third-party module executes with your credentials. The module’s code is the trust boundary; the Registry is the host, not the verifier.
- An unpinned module is an unaudited upgrade. The upgrade can change IAM permissions, add resources, or alter defaults. The discipline is to pin.
- A maintenance signal change is the early warning. A module whose maintainer disappears is a module that will not get a CVE fix. The discipline is to track the signal.
Performance implications
- The Registry is a download. The cost is a network
round trip per
init. A local mirror eliminates the cost for offline runners. - A range pin (
~> 5.0) may cause a download on every CI run if the consumer does not cache the module tarball. The fix is to cache the.terraform/modules/directory across CI jobs.
What comes next
The next lesson covers the CI/CD supply chain: how the runner image, the secret storage, and the audit trail combine to make the pipeline itself a supply chain control.
Verification
- Every third-party module has a
version =attribute on itsmoduleblock. - The version constraint is a pessimistic or
bounded range, not an unbounded
>=. - Every third-party module has an evaluation
record committed to
infra-evaluations. - The evaluation record is updated within 30 days of any major version release.
- No third-party module in use has a
LICENSEclassified as “unlicensed” or “custom restrictive”.
Knowledge check · 7 questions
Q1. What does the public Terraform Registry verify before publishing a module?
Q2. What is the right `version =` for a production third-party module?
Q3. A module with no `LICENSE` file is, by default, all-rights-reserved and not safe to adopt legally.
Q4. Which signal most reliably predicts that a module will receive a security fix when a CVE is published?
Q5. Which of the following are valid licence classes for a commercial consumer of a third-party module? (Select all that apply.)
Q6. Why is an unpinned third-party module a supply-chain risk even when the maintainer is trusted?
Q7. A team pins a third-party module to `~> 5.0`. Six months later, the maintainer ships a breaking `6.0.0`. What is the right action?
Passing score: 75%. Answers are checked in this browser.