Modern DevSecOps Foundations Module 3 · IaC Foundations

The IaC Supply Chain

Last reviewed · content updated

Intermediate

What you'll learn

~18 min
  • Pin the full dependency surface - knowing exactly what the lock file does and does not cover
  • Vendor recoverably for air-gapped and registry-down days
  • Navigate the 2026 tool landscape honestly: Terraform, OpenTofu, and the graveyard

Your infrastructure executes other people’s code

Every terraform apply runs providers you downloaded and modules someone else wrote — with credentials that can reshape your estate. The 2025-26 application-world attacks (self-replicating package worms, rewritten mutable tags on a CI action used by tens of thousands of repos) all exploited one thing: mutable references — and IaC’s references are just as mutable unless you pin them. This lesson is the pinning discipline, plus the one fact that surprises even experienced practitioners.

The fact: the lock file does not cover modules

.terraform.lock.hcl PINS: providers - exact versions + content HASHES.
Commit it. Always. Non-negotiable.
DOES NOT COVER: modules. At all.
module "storage" {
source = "registry.example/org/storage/azurerm"
version = "~> 0.14" # <- this RANGE is your only module control,
} # and it re-resolves at every init

A module pinned ~> (or not at all) is a mutable reference: whoever controls the registry entry controls what your next init fetches — no hash verifies it. The discipline that follows: exact module versions everywhere (version = "0.14.3" — the wrappers from 3.1 already do this), prefer sources that are content-addressable (Git sources pinned to a commit SHA, not a branch or tag — tags move; that’s the whole CI-action attack), and treat the wrapper library as your module choke point: consumers reference your wrappers by your release tags (Module 2’s versioning scheme — acceptable precisely because your platform enforces tag immutability/protection on that repo; internal tags without immutability are just prettier branches), and only the platform team touches upstream references. One team watching one boundary beats forty teams watching none.

Add the timing dimension the package ecosystems adopted across 2025-26: cooldown windows — not consuming a dependency release until it has aged some days — went mainstream in 2026, increasingly shipped as a default because worm-class attacks are usually caught within hours-to-days of publication. Apply it to IaC: upstream module and provider bumps land on a schedule (weekly, after a soak), by PR, through the version matrix — never by a range auto-resolving at 2 a.m.

Recoverable vendoring: the air-gap trick worth stealing

For estates that must build offline (or just survive a registry outage), the reference estate ships a genuinely clever pattern. Naive vendoring — committing upstream source into your repo — works but bloats the repo and blurs upstream provenance. The estate’s move:

committed: main.tf ALWAYS points at the registry source + exact version
.avm-source.lock - a sidecar recording, per module: the
upstream source address, version, the sha256 of the fetched
archive (a version SELECTS bytes; the digest VERIFIES them),
and the local vendor path
transient: vendor/ - upstream source, fetched by tooling, gitignored
bundle time: the air-gap bundler reads the sidecar, fetches exact versions,
REWRITES source addresses to local paths in the bundle copy,
and the bundle builds offline - reproducibly, because every
fetch is checked against the recorded digest
restore: the rewrite is reversible; the repo of record never changed

Properties: the repo stays thin and registry-truthful; the offline story is reproducible from committed metadata; and vendoring is a build output, not a fork you’ll never un-fork. Even if you never air-gap, the sidecar pattern is your registry-outage insurance for the price of a lock-file convention.

The 2026 landscape, stated honestly

The tool facts you need, volatile bits fenced into the callout below: Terraform remains the market default and this training’s teaching target. OpenTofu — the community fork born of the 2023 license change, a CNCF Sandbox project since 2025 — is a credible alternative with real differentiators (client-side state encryption is the headline; Terraform still lacks it) and real adoption, while Terraform’s recent releases have been visibly closing the fork’s feature gaps. Mainstream code runs on both; language-level divergence is growing in both directions. The working rules: pick one per repo (don’t dual-run), pin the binary version like any dependency, and revisit the choice annually as a documented decision — not a religion. Meanwhile, know the graveyard, because stale tutorials will cheerfully recommend the dead: tfsec folded into Trivy; Terrascan archived; driftctl unmaintained since 2023 (drift detection now lives in the TACOS platforms and in disciplines like the soak-window pattern this estate uses). Checkov and Trivy are the living scanners — next lesson wires them.

🔍Version-sensitive specifics (verify before citing — Aug 2026)

Terraform 1.15.x / OpenTofu 1.12.x current lines. Recent changes that invalidate older materials: S3-native state locking is GA and the DynamoDB lock-table pattern is DEPRECATED (update those runbooks); ephemeral values/write-only arguments landed in both tools (secrets-out-of-state is becoming table stakes); Terraform’s “Stacks” orchestration is HCP-only and consumption-billed, while Terragrunt’s stacks live on the free CLI side; OpenTofu ships OCI-registry support and state encryption. IBM completed the HashiCorp acquisition Feb 2025 — BUSL license and registry access unchanged since. Azure-side: azurerm 5.x is current (5.0 GA late July 2026), with opt-in plan-time preflight validation covering a small resource subset at launch - check the coverage list before relying on it (Lesson 3.4 uses it).

Run the supply-chain hardening pass on our IaC estate, as one PR:
1. AUDIT: every module source across the estate - registry-with-range,
registry-exact, git-branch, git-tag, git-SHA, local - as a table; every
provider constraint; whether every stack's lock file is committed
2. FIX: ranges -> exact versions; EXTERNAL git branches/tags -> SHAs
(internal refs to our own protected release tags stay - 3.1's
sanctioned pin); missing lock
files committed; a comment on each pointing at the version matrix
3. SIDECAR: generate the recoverable-vendoring lock for our wrapper
library's upstreams
4. CADENCE: the weekly bump workflow - tooling proposes upstream updates
as PRs after a 7-day cooldown, with the matrix updated in the same PR
5. GUARD: a CI check that fails on any new range, or any branch/tag
reference outside our allowlisted internal release tags -
the pass is only real if regression is impossible
ℹOn GitHub: the same discipline for Actions

The identical logic governs CI dependencies: pin third-party actions to commit SHAs (the tag-rewrite attack is the canonical case), and note GitHub lets orgs ENFORCE SHA-pinning platform-wide via Actions policy — governance the ADO side approximates with template control (2.3). One discipline — immutable references, chosen aging, enforced centrally — across app packages, IaC modules, and CI actions.

KNOWLEDGE CHECK

An engineer reads the hardening PR and pushes back on exact module pins: 'Ranges like ~> 0.14 exist so we get patch fixes automatically — exact pins mean we'll run vulnerable versions until someone remembers to bump.' What's the correct resolution?

Key takeaway

Pin the whole surface: lock files for providers (committed, always), exact versions or SHAs for modules (the lock file won’t save you there), SHA-pinned CI actions, cooldown-aged scheduled bumps by PR, and a recoverable vendoring sidecar for the offline day. Know the living tools from the dead, pick one engine per repo deliberately, and let one team guard the one boundary. Next: the gates that scan what all of this deploys.

Search lessons