Provider and Module Supply Chain
Scenario: A configuration references a module through a mutable Git branch rather than a pinned commit, an attacker compromises that upstream branch, and the malicious code runs with the team's own production credentials on the very nextinit/apply- with nobody having reviewed a single line of what changed.
You've already met the individual pieces of this defense across earlier modules - now let's see them together as one supply-chain discipline. Pin the Terraform CLI version itself (required_version), pin provider version constraints and rely on the lock file's checksums (Module 3), and pin module sources to immutable tags or commit SHAs rather than mutable branches (Module 6) - each layer closes off one specific way an unreviewed change could silently enter your infrastructure pipeline.
Beyond pinning, review every dependency upgrade with the same seriousness you'd give an application dependency bump, prefer trusted registries or an internally-curated mirror over pulling directly from arbitrary public sources, verify provenance/signatures where the ecosystem offers them, run security scanning over both your own configuration and any third-party modules you consume, and restrict what your CI runners can reach on the network at all (limiting where a compromised dependency could even attempt to exfiltrate data to). The mental model worth keeping: a module is executable infrastructure logic that runs with your credentials the moment you call it, and a provider is native compiled code that Terraform loads and executes directly - both are genuine supply-chain attack surfaces, not passive configuration files.
Analogy: IaC dependencies - providers and modules alike - are like outside contractors who get handed real keys to your building the moment you bring them on. A contractor's good reputation is a reasonable starting signal, but it is not itself a security control - you still need to verify their actual identity, put a real contract in place, inspect their work, and restrict what areas of the building their key set can actually open.
A worked configuration
terraform {
required_version = "~> 1.9.0"
required_providers {
fakecloud = { source = "shellgenius/fakecloud", version = "~> 1.0" }
}
}
Mirroring dependencies internally gives you resilience against an upstream outage and a natural checkpoint for review before anything reaches your pipeline - but don't let mirroring turn into indefinite pinning out of pure inertia. A dependency frozen forever "because it works" quietly accumulates known, patched vulnerabilities and eventual incompatibility with everything else moving forward around it. Maintain a deliberate, regular upgrade cadence specifically for security patches, even on dependencies you'd otherwise be tempted to never touch again.
Common mistakes in production
- Referencing a module through a mutable branch name instead of an immutable tag or commit SHA - a future push to that branch changes what your pipeline runs with zero review.
- Pinning a dependency once and never revisiting it again "because it works" - this quietly accumulates unpatched, known vulnerabilities over time rather than avoiding risk.
Goal: Treat the plan as a change contract: understand every create, update, replacement, and destroy before approving it. Reinforce this lesson in the tf-version-lockfile Terraform lab. Open/labs/terraformand choose slugtf-version-lockfile; the lab runs real Terraform against the offline FakeCloud provider.