DEV Community

Cover image for The IaC Supply Chain Nobody's Securing — Part 1: Modules, SBOMs, and the Visibility Gap
Alejandro Velez
Alejandro Velez

Posted on

The IaC Supply Chain Nobody's Securing — Part 1: Modules, SBOMs, and the Visibility Gap

Why IaC Supply Chain Security Matters

Your application's supply chain is visible. You scan npm packages with Snyk, generate SBOMs with Syft, and track CVEs with Dependency-Track. Your CI/CD pipeline blocks deploys when a critical vulnerability surfaces in a transitive dependency.

But what about the infrastructure code that provisions your entire cloud environment?

Every terraform apply downloads modules from public registries, executes provider binaries with full cloud credentials, and provisions resources that handle production traffic. A single compromised Terraform module can exfiltrate your AWS credentials, create backdoor IAM roles with AdministratorAccess, modify security groups to allow inbound traffic from 0.0.0.0/0, or deploy cryptominers on your compute instances.

Imagine this: your platform team manages 12 AWS accounts with Terragrunt/openTofu/Terraform. You're consuming 40+ Terraform modules — some from the public registry, some from a private registry, a few forked into your org's GitHub, and others pulled through an artifact proxy. Now answer these questions:

  • Which registry modules use exact versions (version = "5.16.0") vs. ranges (version = "~> 5.0") that resolve to whatever latest matches on every clean CI run — with no lock file to freeze the result? And for Git-sourced modules, which ones reference a branch (?ref=main) instead of a tag or commit SHA?
  • Your forked modules — how far have they drifted from upstream? Are you missing security patches the original maintainer shipped months ago?
  • If a module publisher's GitHub account gets compromised tonight — or your private registry has a vulnerability like Tapir's CVE-2024-56802 — which stacks across which accounts are affected?
  • When the HashiCorp GPG key expired in April 2026 and CI pipelines broke worldwide — did you know which providers were impacted within minutes, or did you spend a weekend triaging?

If you can't answer all four confidently, your IaC supply chain is your single biggest blind spot. In my experience leading platform engineering teams across multi-account AWS environments, most teams can't answer even one — and that's what drove me to build the governance layer described in this post.

This post covers how to fix that gap — from understanding IaC-specific attack vectors, through generating CycloneDX 1.6 SBOMs for Terraform modules, to enforcing version governance policies as CI/CD gates using OPA/Rego.

The Pattern: IaC Supply Chain Governance

IaC Supply Chain Governance

The pattern has three layers:

  1. Inventory — know what you're consuming (modules, providers, versions, sources)
  2. Governance — define policies about what's acceptable (freshness, provenance, pinning)
  3. Enforcement — block violations in CI/CD before they reach production

IaC vs. Application Supply Chain: The Security Gap

The application supply chain and the IaC supply chain share the same risk model but differ in critical ways:

Dimension App Dependencies (npm, pip) IaC Dependencies (Terraform modules)
Lock file package-lock.json, poetry.lock ⚠️ .terraform.lock.hcl covers providers only — modules have NO lock file
Cryptographic verification ✅ Registry checksums, Sigstore ⚠️ Providers: GPG signed. Modules: NONE
Execution privileges Application-level sandbox Full cloud credentials (IAM role with infra permissions)
Blast radius Application process Entire AWS account (VPCs, databases, IAM, networking)
Version pinning Enforced by lock file Manual — ~> ranges allow silent upgrades
Typosquatting protection npm has namespace scoping Terraform Registry has no namespace reservation
SBOM tooling Mature (Syft, Trivy, CycloneDX plugins) Emerging but limited — Syft/sbomify cover providers via .terraform.lock.hcl; modules require source-code parsing (no native support)

The asymmetry is stark: IaC modules run with higher privileges, have weaker protections, and less visibility than application packages.

Where ThothCTL Closes the Gap

Existing tools (Syft, sbomify) generate SBOMs only from the lock file — meaning they inventory providers but miss modules entirely. ThothCTL closes this gap by:

  1. Parsing .tf source files — extracts module declarations, source URLs, version constraints, and nested module dependencies (not just what's in the lock file)
  2. Version freshness checking — queries registries in real-time to calculate staleness (iac:days-since-update, iac:versions-behind) for both modules AND providers
  3. Full CycloneDX 1.6 output — uses Formulation (stack composition), Evidence (discovery method), and Properties (pinning status, source type) to produce a complete IaC-specific SBOM
  4. OPA policy enforcement on SBOM data — the generated SBOM becomes the input for Rego policies that gate deployments (no version ranges, max staleness, approved sources only)
  5. Recursive multi-stack support — scans Terragrunt monorepos with 50+ stacks in one command, producing a unified inventory across the entire platform

In short: Syft tells you which providers are installed. ThothCTL tells you which modules AND providers are used, how stale they are, whether they're pinned, and blocks the deploy if they violate policy.

🚨 Real-World IaC Supply Chain Incidents

This isn't theoretical. The Terraform ecosystem has already experienced supply chain attacks and near-misses:

Erosion of Trust — Terraform Registry (BoostSecurity, 2023)

BoostSecurity researchers disclosed that Terraform modules lack cryptographic guarantees from the dependency lock file. Unlike providers (which get h1: and zh: hashes in .terraform.lock.hcl), modules are downloaded based solely on the source URL and version constraint.

The attack vector: abuse pull_request_target in GitHub Actions workflows of popular modules to gain write access to the repository, then publish a backdoored version. HashiCorp responded with HCSEC-2024-04 security improvements — but modules still have no lock file protection.

CVE-2024-56802 — Tapir Private Registry

A critical vulnerability in Tapir (a popular self-hosted Terraform registry) allowed attackers to guess deploy keys and gain unauthorized write access. This means even private registries that teams trust implicitly can be compromised.

HashiCorp GPG Key Expiration (April 2026)

Over the weekend of April 18-20, 2026, CI pipelines worldwide broke with openpgp: key expired. HashiCorp's GPG signing key for provider releases had expired — revealing a single point of failure that every organization's infrastructure pipeline depended on. OpenTofu was unaffected (different signing key), and the community renewed calls for keyless signing via Sigstore (OpenTofu issue #307).

Registry API as Single Point of Failure (GitHub #20527)

An open issue since 2019 notes that the Terraform Registry API is a single point of failure for secure provider distribution. If the registry is compromised or returns tampered metadata, all terraform init operations become attack vectors.

The npm Parallel: What's Coming for IaC

Microsoft's May 2026 discovery of 14 typosquatted npm packages stealing CI/CD secrets demonstrates the pattern that will hit IaC registries. The Terraform Registry has no namespace reservation — registering hashicorp-aws/vpc/aws to impersonate the official hashicorp/terraform-aws-vpc module requires no verification.

The Module vs. Provider Security Gap

Understanding this gap is critical for building defenses:

┌─────────────────────────────────────────────────────────────────────┐
│              PROVIDER SECURITY (✅ Reasonably Protected)            │
├─────────────────────────────────────────────────────────────────────┤
│                                                                     │
│  ✅ GPG signed by publisher                                         │
│  ✅ Checksums in .terraform.lock.hcl (h1: and zh: hashes)           │
│  ✅ Lock file committed to VCS — team-wide consistency              │
│  ✅ OpenTofu: OPENTOFU_ENFORCE_GPG_VALIDATION=true (strict mode)    │
│  ⚠️  GPG key management is fragile (April 2026 incident)             │
│  ⚠️  No keyless signing (Sigstore) yet                               │
│                                                                     │
├─────────────────────────────────────────────────────────────────────┤
│              MODULE SECURITY (🚫 Essentially Unprotected)           │
├─────────────────────────────────────────────────────────────────────┤
│                                                                     │
│  🚫 NO cryptographic signing                                        │
│  🚫 NO checksums in .terraform.lock.hcl                             │
│  🚫 NO integrity verification on download                           │
│  🚫 NO namespace reservation (typosquatting possible)               │
│  🚫 Version ranges (~>, >=) allow silent upgrades                   │
│  ⚠️  Only defense: exact version pin + trust the source URL          │
│                                                                     │
└─────────────────────────────────────────────────────────────────────┘
Enter fullscreen mode Exit fullscreen mode

Key insight: Since the tooling itself provides no module protection, any defense must be built at the CI/CD layer — which is exactly what we'll implement next.

📦 CycloneDX 1.6 for Infrastructure: Beyond Application SBOMs

CycloneDX 1.6 (ECMA-424, standardized June 2024) introduced three capabilities that make IaC supply chain tracking possible:

CycloneDX 1.6 Feature Application Use Case IaC Use Case
Formulation Build pipeline metadata Stack composition — which modules compose which stacks (Terraform workspaces, Terragrunt units, or OpenTofu projects)
Evidence How a component was identified Source URL verification — module origin + commit hash at download time
Attestations (CDXA) Security standard compliance Module passes policy checks — scan results as signed evidence
Cryptographic BOM (CBOM) Cryptographic algorithm inventory Provider signing key metadata — track which GPG keys signed which providers
Dependency Graph Package dependency tree Module dependency tree — nested modules + provider requirements

How an IaC SBOM Differs from an App SBOM

An application SBOM lists packages with their versions and licenses. An IaC SBOM needs fundamentally different information:

{
  "bomFormat": "CycloneDX",
  "specVersion": "1.6",
  "serialNumber": "urn:uuid:3e671687-395b-41f5-a30f-a58921a69b79",
  "version": 1,
  "metadata": {
    "timestamp": "2026-07-24T20:00:00Z",
    "component": {
      "type": "application",
      "name": "platform-team/networking-stack",
      "version": "1.4.2"
    },
    "tools": [{
      "vendor": "ThothForge",
      "name": "thothctl",
      "version": "0.24.3"
    }]
  },
  "components": [
    {
      "type": "library",
      "group": "registry.terraform.io/hashicorp",
      "name": "terraform-aws-modules/vpc/aws",
      "version": "5.16.0",
      "purl": "pkg:terraform/terraform-aws-modules/vpc/aws@5.16.0",
      "hashes": [],
      "externalReferences": [
        {
          "type": "vcs",
          "url": "https://github.com/terraform-aws-modules/terraform-aws-vpc"
        }
      ],
      "evidence": {
        "identity": {
          "field": "purl",
          "confidence": 0.9,
          "methods": [
            {
              "technique": "source-code-analysis",
              "value": "Parsed from module source in stacks/networking/main.tf:12"
            }
          ]
        }
      },
      "properties": [
        {"name": "iac:source-type", "value": "registry"},
        {"name": "iac:version-constraint", "value": "= 5.16.0"},
        {"name": "iac:pinned", "value": "true"},
        {"name": "iac:last-updated", "value": "2026-06-15"},
        {"name": "iac:days-since-update", "value": "39"},
        {"name": "iac:latest-available", "value": "5.17.1"},
        {"name": "iac:versions-behind", "value": "1"}
      ]
    },
    {
      "type": "framework",
      "group": "registry.terraform.io/hashicorp",
      "name": "aws",
      "version": "5.82.0",
      "purl": "pkg:terraform/hashicorp/aws@5.82.0",
      "hashes": [
        {"alg": "SHA-256", "content": "abc123...from-lock-file"}
      ],
      "properties": [
        {"name": "iac:component-type", "value": "provider"},
        {"name": "iac:gpg-signed", "value": "true"},
        {"name": "iac:lock-file-protected", "value": "true"},
        {"name": "iac:signing-key-expiry", "value": "2028-04-20"}
      ]
    }
  ],
  "formulation": [
    {
      "components": [
        {"ref": "pkg:terraform/terraform-aws-modules/vpc/aws@5.16.0"}
      ],
      "workflows": [
        {
          "uid": "build-networking-stack",
          "tasks": [
            {"uid": "terraform-init", "name": "Download modules and providers"},
            {"uid": "terraform-plan", "name": "Generate execution plan"},
            {"uid": "thothctl-scan", "name": "Security scan with Checkov+Trivy"}
          ]
        }
      ]
    }
  ]
}
Enter fullscreen mode Exit fullscreen mode

Key differences from app SBOMs:

  • purl format: Uses pkg:terraform/ scheme (not pkg:npm/ or pkg:pypi/)
  • Properties: IaC-specific metadata (pinning status, staleness, source type)
  • Evidence: How the component was discovered (source-code-analysis of .tf files)
  • Formulation: Which CI/CD workflow produced this infrastructure
  • Hashes: Only available for providers (from lock file) — modules have none

Hands On

🛠️ Generating IaC SBOMs in CI/CD

In my experience, the most practical approach is integrating IaC SBOM generation into the build phase of the DevSecOps workflow:

Using ThothCTL Inventory

# ─── Generate CycloneDX 1.6 SBOM for the entire project ───
thothctl inventory iac --report-type cyclonedx --check-versions

# Output: Reports/sbom-cyclonedx-1.6.json
# Contains: all modules, providers, versions, staleness, source URLs

# ─── Works with any IaC tool combination ───
# Terraform projects: auto-detected
# OpenTofu projects: auto-detected (or explicit --tftool tofu)
# Terragrunt monorepos: scans recursively across all units
Enter fullscreen mode Exit fullscreen mode
# ─── Generate with project name for tracking across releases ───
thothctl inventory iac \
  --report-type cyclonedx \
  --check-versions \
  --project-name "platform-networking-v1.4.2"

# ─── As part of the DevSecOps workflow engine (build phase) ───
thothctl workflow devsecops --phase build
# The build phase automatically:
#   1. Scans all .tf files for module/provider declarations
#   2. Checks latest available versions against registries
#   3. Generates CycloneDX 1.6 SBOM
#   4. Reports stale dependencies

Enter fullscreen mode Exit fullscreen mode

Finally you can watch the report running:


thothctl  dashboard launch 

  _____ _           _   _      ____ _____ _     
 |_   _| |__   ___ | |_| |__  / ___|_   _| |    
   | | | '_ \ / _ \| __| '_ \| |     | | | |    
   | | | | | | (_) | |_| | | | |___  | | | |___ 
   |_| |_| |_|\___/ \__|_| |_|\____| |_| |_____|

   🔱 AI-Powered Infrastructure Lifecycle CLI


   🚀 Dashboard v0.24.3 - PRODUCTION Mode
   🌐 URL: http://127.0.0.1:8080

🎯 Dashboard Features:
   • 📦 Infrastructure Inventory (SBOM) with version tracking
   • 🔒 Security scan results from Checkov, Trivy & more  
   • 💰 AWS cost analysis with optimization recommendations
   • ⚠️  Blast radius assessment for change impact analysis
   • 🌙 Dark mode, export options & responsive design

⚡ PRODUCTION Mode Active - Optimized for production performance

Enter fullscreen mode Exit fullscreen mode

Some pictures for the findings:

Thothctl Dashboard -SBOM

Thothctl Dashboard -inventory

Thothctl Dashboard -Compatibility

You can find the demo in youtube:

GitHub Actions Integration

A common use case is integrate into ours CI process, here an example to use in github actions.

name: IaC Supply Chain
on:
  pull_request:
    paths: ['stacks/**', 'modules/**']
  schedule:
    - cron: '0 6 * * 1'  # Weekly Monday 6am — catch new CVEs

jobs:
  supply-chain:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - name: Setup ThothCTL
        run: pip install thothctl

      - name: Generate IaC SBOM
        run: |
          thothctl inventory iac \
            --report-type cyclonedx \
            --check-versions \
            --project-name "${{ github.repository }}-${{ github.sha }}"

      - name: Enforce version governance
        run: |
          thothctl workflow devsecops --phase build \
            --enforcement hard

      - name: Upload SBOM as release artifact
        if: github.ref == 'refs/heads/main'
        uses: actions/upload-artifact@v4
        with:
          name: iac-sbom-${{ github.sha }}
          path: Reports/sbom-cyclonedx-1.6.json

      - name: Post SBOM to Dependency-Track (optional)
        if: github.ref == 'refs/heads/main'
        run: |
          curl -X POST "${{ secrets.DTRACK_URL }}/api/v1/bom" \
            -H "X-Api-Key: ${{ secrets.DTRACK_API_KEY }}" \
            -H "Content-Type: multipart/form-data" \
            -F "projectName=${{ github.repository }}" \
            -F "projectVersion=${{ github.sha }}" \
            -F "bom=@Reports/sbom-cyclonedx-1.6.json"
Enter fullscreen mode Exit fullscreen mode

Enterprise Module Sourcing: Different Patterns, Different Risks

The scenarios above assume public registry consumption, but enterprise teams source modules in fundamentally different ways. Each pattern mitigates some risks while introducing others:

Pattern 1: Private Registry (Terraform Cloud/Enterprise, Spacelift, Scalr)

module "vpc" {
  source  = "app.terraform.io/your-org/vpc/aws"
  version = "5.16.0"
}
Enter fullscreen mode Exit fullscreen mode

What it mitigates: Typosquatting (namespace is your org), unauthorized publishing (access-controlled)

What it doesn't solve:

  • Modules still have no lock file — version ranges resolve on init
  • No cryptographic signing of module contents
  • If the private registry is compromised (see Tapir CVE-2024-56802), all consumers are affected
  • Internal modules can still go stale — nobody forces you to update them

Pattern 2: Git-Sourced Modules (Forked or Internal)

module "vpc" {
  source = "git::https://github.com/your-org/terraform-aws-vpc.git?ref=v5.16.0"
}
Enter fullscreen mode Exit fullscreen mode

What it mitigates: Registry dependency removed, you control the source, can pin to exact commit SHA

What it doesn't solve:

  • If pinned to a tag (?ref=v5.16.0), tags can be moved/rewritten — use commit SHA for true immutability
  • Forked modules drift from upstream — security patches in the original don't auto-propagate
  • No centralized inventory — "which teams consume which fork version?" becomes tribal knowledge
  • Internal CI/CD credentials to clone the repo become the new attack surface

Pattern 3: Artifact Proxy / Module Mirror

# .terraformrc
provider_installation {
  filesystem_mirror {
    path    = "/opt/terraform/mirror"
    include = ["registry.terraform.io/*/*"]
  }
}
Enter fullscreen mode Exit fullscreen mode

Or using a registry proxy (Artifactory, Nexus, custom):

module "vpc" {
  source  = "terraform.internal.company.com/cached/vpc/aws"
  version = "5.16.0"
}
Enter fullscreen mode Exit fullscreen mode

What it mitigates: External registry outages, network restrictions in air-gapped environments, centralized caching

What it doesn't solve:

  • The proxy caches what it first pulled — if you cached a compromised version, it stays compromised
  • Staleness amplified: teams consume the proxy version without knowing upstream released a patch
  • Mirror synchronization becomes its own supply chain problem ("when did we last sync?")
  • Integrity depends entirely on the proxy's security posture

Pattern 4: Monorepo with Local Modules

module "vpc" {
  source = "../../modules/vpc"
}
Enter fullscreen mode Exit fullscreen mode

What it mitigates: No external dependency at all, full version control, reviewed via PR

What it doesn't solve:

  • No versioning — every consumer gets whatever is on main
  • Breaking changes propagate immediately to all stacks
  • Module code quality depends entirely on your internal review process
  • Harder to share across teams/repos — leads to copy-paste drift

How Supply Chain Governance Adapts

The governance layer described in this post works across all four patterns because ThothCTL parses .tf source files directly — it doesn't depend on a specific registry API:

Governance Capability Public Registry Private Registry Git Source Local Modules
Module inventory (SBOM)
Version freshness check ✅ (queries registry) ✅ (queries private registry) ⚠️ (compares Git tags) N/A (no version)
Pinning enforcement ✅ (exact version) ✅ (exact version) ✅ (commit SHA vs tag vs branch) N/A
Approved source policy ✅ (allowed Git orgs) ✅ (allowed paths)
Staleness detection ⚠️ (compares upstream if fork)
Cryptographic verification ❌ (modules unsigned) ❌ (modules unsigned) ⚠️ (Git commit signatures if enforced)

The key takeaway: private registries and forks reduce some attack vectors (typosquatting, unauthorized publishing) but introduce new ones (fork drift, stale mirrors, proxy compromise). True governance requires visibility across all sourcing patterns — which is why parsing source code beats relying on any single registry API.

What's Next — Part 2: Governance, Enforcement, and Results

Visibility without enforcement is just a dashboard. In Part 2 of this series, we turn the SBOM into a governance engine:

  • OPA/Rego policies — no version ranges, max staleness thresholds, approved source provenance
  • Module dependency graph visualization — understand how risk propagates across stacks
  • Scaling governance across 12+ accounts — centralized policy repos, per-team staleness budgets
  • 6-week implementation path — from first SBOM to hard enforcement gates
  • Measured results — from zero visibility to full governance in production

The SBOM you generated in this post becomes the input document for every Rego policy in Part 2. The infrastructure is the same — the governance layer is what makes it safe.

What's your IaC supply chain visibility today? Can you answer those four questions from the introduction? I'd love to hear what sourcing patterns you're using and what keeps you up at night 👇

✨ Alejandro Velez, Platform Engineering Latam Lead @ GFT | AWS Ambassador

Top comments (0)