Skip to main content
Security9 min read

Terraform Module Registries Are Supply-Chain Infrastructure—Secure Them Like Production

A recent compromise of Coder’s registry infrastructure shows how Terraform modules can become a direct path to cloud credentials and production systems. Engineering teams should treat IaC registries as critical supply-chain infrastructure, with stronger controls around provenance, CI permissions, lockfiles, and detection.

A Terraform module registry can feel like plumbing: useful, invisible, and mostly someone else’s problem. But when that registry is compromised, it can become a delivery mechanism for code that runs inside your most privileged automation paths.

That is the lesson from the recently reported Coder incident, where attackers compromised Cloudflare infrastructure, added unauthorized registry servers, and delivered malicious Terraform modules containing credential-stealing code, according to BleepingComputer. For developers, platform engineers, and CTOs, the takeaway is clear: infrastructure-as-code dependencies are not just developer convenience. They are part of the software supply chain.

Context: IaC Dependencies Now Sit on the Critical Path

Terraform Module Registries Are Supply-Chain Infrastructure—Secure Them Like Production
Terraform Module Registries Are Supply-Chain Infrastructure—Secure Them Like Production

Terraform modules are designed to make infrastructure reusable. A team can standardize a VPC, Kubernetes cluster, identity configuration, monitoring stack, or deployment pattern and consume it across environments with a small source reference and version constraint.

That convenience is one reason modules are so widely used. It is also why they are attractive to attackers.

In traditional application development, teams have spent years improving controls around package managers such as npm, PyPI, Maven, and NuGet. They scan dependencies, pin versions, maintain artifact repositories, and monitor for malicious packages. Infrastructure-as-code dependencies deserve the same treatment, because the blast radius can be even larger.

A malicious application dependency might steal application secrets or tamper with runtime behavior. A malicious Terraform module may execute in a CI pipeline that has access to cloud credentials, state backends, production workspaces, service account keys, deployment tokens, and private network configuration. In many organizations, Terraform automation is one of the most privileged systems in the engineering environment.

That makes registry trust a security boundary.

What Happened in the Coder Registry Incident

BleepingComputer reported that attackers compromised Coder’s Cloudflare infrastructure and used it to add unauthorized registry servers. Those servers delivered malicious Terraform modules that included credential-stealing code.

The important detail is not only that malicious modules existed. It is that the registry infrastructure itself was abused as the distribution mechanism.

That changes how engineering teams should reason about risk. If a developer intentionally installs an unknown module from an untrusted source, the control failure is obvious. But if a familiar registry endpoint, namespace, or module path is compromised, even experienced teams can be exposed. The trust decision may have been made months or years earlier, then embedded into CI workflows, templates, and internal documentation.

This is how supply-chain attacks scale: compromise the place teams already trust, then wait for automation to do the rest.

Why Terraform Modules Are High-Value Attack Targets

They Run Near Sensitive Credentials

Terraform itself does not need to be malicious for a module to create risk. Modules can influence providers, resources, data sources, provisioners, external scripts, templates, outputs, and other behavior that occurs during planning or applying infrastructure.

Many CI systems run Terraform with environment variables or workload identities that grant access to cloud APIs. Those permissions may include the ability to create roles, read secrets, access object storage, modify networking, or deploy compute. If malicious code can execute in that context, credential theft is only one possible outcome.

The best-case scenario is a failed pipeline. The worst case is persistence in your cloud environment.

They Are Often Treated as Static Building Blocks

Application dependencies tend to receive regular scrutiny because developers touch them frequently. Terraform modules, by contrast, can fade into the background. A module source may be copied into dozens of repositories and left unchanged for years.

That creates maintenance debt. Teams may not know which modules are still in use, which versions are pinned, whether a source registry is still trusted, or whether ownership has changed. Modernization programs often focus on application frameworks and runtime upgrades, but IaC dependencies should be included in the same inventory and lifecycle planning.

If a module defines your production network, identity roles, or Kubernetes baseline, it is not “just configuration.” It is operational code.

Terraform Locking Is Not a Complete Answer

Lockfiles are important, but teams need to understand their limits.

Terraform’s dependency lock file, .terraform.lock.hcl, is primarily focused on provider selections and checksums. It helps ensure that the same provider packages are used consistently across runs. However, Terraform module integrity and provenance are a different problem. Depending on how modules are sourced, teams may need additional controls such as strict version pinning, private mirrors, source allowlists, commit SHA references for Git-based modules, and review workflows for module updates.

In other words, “we have a lockfile” should not end the conversation. It should start a more precise one: What exactly is locked, what is verified, and what can still change upstream?

Registry Trust Models Need a Security Review

Most organizations consume Terraform modules through a mix of public registries, vendor registries, Git repositories, internal module catalogs, and sometimes direct archive URLs. Each source type has a different trust model.

A mature review should answer questions like:

  • Which registries and source domains are approved for Terraform modules?
  • Who can add a new module source to a production repository?
  • Are module versions pinned, or can CI resolve newer versions automatically?
  • Do we use private mirrors or internal registries for critical modules?
  • Can we detect if a module source changes ownership, redirects, or resolves to an unexpected endpoint?
  • Are module updates reviewed with the same seriousness as application dependency updates?

For CTOs and engineering leaders, this is not about slowing teams down. It is about making the safe path the easy path. If developers need reusable infrastructure patterns, give them a curated internal module registry, clear ownership, automated checks, and fast review cycles.

Practical Implications for Engineering Teams

1. Inventory Terraform Module Usage

Start by discovering where modules are used. Search repositories for module blocks and collect source addresses, version constraints, environments, and owning teams. Include archived repositories if they can still run in CI.

This inventory becomes the foundation for modernization. It helps teams identify duplicated patterns, stale modules, deprecated sources, and high-risk dependencies used in sensitive environments.

A platform like Vibgrate can support this kind of maintenance work by helping teams understand dependency sprawl, prioritize upgrades, and coordinate modernization across many repositories rather than treating each codebase as an isolated project.

2. Pin Versions and Control Update Paths

Avoid floating references for production infrastructure. Use explicit versions for registry modules. For Git-based modules, prefer immutable references such as commit SHAs for critical infrastructure, while maintaining a controlled process to promote newer versions.

Automated dependency update tools can still be valuable, but they should create pull requests that trigger review, tests, policy checks, and security scanning. The goal is not to freeze infrastructure forever. The goal is to make change intentional and observable.

3. Reduce CI Credential Exposure

Assume that CI is a high-value target. Terraform pipelines should use least-privilege credentials scoped to the environment and task. Avoid long-lived static cloud keys where possible. Prefer short-lived federated credentials, workload identity, or tightly scoped service accounts.

Separate plan and apply permissions when feasible. A pull request validation job should not have the same access as a production apply job. If a malicious module attempts to read environment variables or call cloud APIs during validation, the available credentials should be limited.

4. Review Use of Provisioners and External Execution

Terraform provisioners, external data sources, local-exec patterns, and wrapper scripts can create opportunities for code execution. Some use cases are legitimate, but they should be visible and reviewed.

Security teams should flag unexpected shell execution, network calls, or file reads inside IaC workflows. Platform teams can provide approved patterns that reduce the need for ad hoc scripts embedded in infrastructure modules.

5. Add Detection for IaC Supply-Chain Behavior

Detection should cover both the code and the pipeline.

Useful signals include:

  • Terraform runs contacting unexpected domains
  • New module sources introduced in pull requests
  • Changes to registry hostnames or namespaces
  • CI jobs accessing secret stores outside normal patterns
  • Terraform processes spawning shells or unexpected binaries
  • Sudden changes in cloud API calls during plan or apply

Network egress controls can help here. Many CI environments allow outbound access to the entire internet by default. Restricting Terraform runners to approved registries, cloud APIs, and internal services can turn a silent compromise into a blocked connection and an alert.

6. Treat Internal Modules as Products

Internal Terraform modules should have owners, versioning policies, changelogs, test coverage, deprecation plans, and security review. This is maintenance work, not bureaucracy.

When internal modules are neglected, teams often bypass them or copy old versions into local repositories. That increases drift and makes upgrades harder. When modules are maintained like products, they become a modernization accelerator: teams can adopt improved security defaults, updated cloud patterns, and compliant configurations through a controlled release process.

What CTOs Should Ask This Quarter

This incident is a good prompt for leadership-level questions:

  • Do we know every external Terraform module source used in production?
  • Can a developer introduce a new registry source without review?
  • Are Terraform runners using least-privilege, short-lived credentials?
  • Do we have a tested process to revoke or replace compromised IaC dependencies?
  • Are module updates included in our dependency management and modernization roadmap?
  • Can security teams detect suspicious behavior during Terraform init, plan, and apply?

If the answer to several of these is “not yet,” that is normal. Many organizations are still maturing their IaC supply-chain practices. The important step is to make the risk visible and assign ownership.

Conclusion: Secure the Registry, Secure the Path to Production

The Coder registry compromise is a reminder that infrastructure delivery depends on more than cloud permissions and Terraform code review. It also depends on the integrity of the registries, modules, networks, and automation paths that sit between developers and production.

Treat Terraform module registries as critical supply-chain infrastructure. Build inventories, pin and verify dependencies, limit CI credentials, monitor registry behavior, and modernize internal modules with the same care you apply to application platforms. As infrastructure becomes more automated, the teams that maintain the safest paths to production will be the teams that can move quickly without handing attackers the keys to the cloud.

Vibgrate CLI

See a real scan run

A replay of the actual CLI running against our test repositories — live progress, real findings, a genuine DriftScore. Nothing executes in your browser.

Replay
demo@vibgrate — bash
❯ npx @vibgrate/cli scan
 
╭──────────────────────────────────────────╮
│ Vibgrate Drift Report │
╰──────────────────────────────────────────╯
 
── node-turborepo (node) .
Runtime: >=18.0.0 (6 majors behind)
Frameworks:
Turbo: 1.13.4 → 2.11.8 (1 behind)
TypeScript: 5.9.3 → 7.0.2 (2 behind)
Dependencies:
1 current 1 1-behind 3 2+ behind 1 unknown
 
── @repo/admin (node) apps/admin
Frameworks:
TanStack Query: 5.104.2 → 5.104.2 (current)
React: 18.3.1 → 19.3.0 (1 behind)
React DOM: 18.3.1 → 19.3.0 (1 behind)
TypeScript: 5.9.3 → 7.0.2 (2 behind)
Vite: 5.4.21 → 8.3.4 (3 behind)
Dependencies:
3 current 9 1-behind 3 2+ behind 4 unknown
 
── @repo/api (node) apps/api
Frameworks:
Express: 4.22.3 → 5.3.0 (1 behind)
TypeScript: 5.9.3 → 7.0.2 (2 behind)
Vitest: 1.6.1 → 5.0.4 (4 behind)
Dependencies:
7 current 4 1-behind 4 2+ behind 4 unknown
 
── @repo/web (node) apps/web
Frameworks:
Next.js: 14.2.35 → 16.4.0 (2 behind)
React: 18.3.1 → 19.3.0 (1 behind)
React DOM: 18.3.1 → 19.3.0 (1 behind)
TypeScript: 5.9.3 → 7.0.2 (2 behind)
Dependencies:
2 current 6 1-behind 3 2+ behind 5 unknown
 
── @repo/config (node) packages/config
Frameworks:
TypeScript: 5.9.3 → 7.0.2 (2 behind)
Dependencies:
2 current 2 1-behind 5 2+ behind 0 unknown
 
── @repo/database (node) packages/database
Frameworks:
Prisma: 5.22.0 → 7.10.0 (2 behind)
TypeScript: 5.9.3 → 7.0.2 (2 behind)
Dependencies:
1 current 0 1-behind 3 2+ behind 1 unknown
 
── @repo/types (node) packages/types
Frameworks:
TypeScript: 5.9.3 → 7.0.2 (2 behind)
Dependencies:
0 current 0 1-behind 1 2+ behind 1 unknown
 
── @repo/ui (node) packages/ui
Frameworks:
React: 18.3.1 → 19.3.0 (1 behind)
TypeScript: 5.9.3 → 7.0.2 (2 behind)
React: 18.3.1 → 19.3.0 (1 behind)
Dependencies:
1 current 4 1-behind 1 2+ behind 1 unknown
 
── @repo/utils (node) packages/utils
Frameworks:
TypeScript: 5.9.3 → 7.0.2 (2 behind)
Vitest: 1.6.1 → 5.0.4 (4 behind)
Dependencies:
0 current 1 1-behind 2 2+ behind 1 unknown
 
Tech Stack
Frontend: React, React DOM
Meta-frameworks: Next.js
Bundlers: tsx, Turbo, Vite
CSS / UI: Autoprefixer, PostCSS, Tailwind CSS
Backend: Express
ORM / Database: Prisma, Prisma Client
Testing: Vitest
Lint & Format: ESLint, ESLint Prettier, ESLint React, Prettier, typescript-eslint
 
Services & Integrations
Auth: JWT 9.0.3
Databases: Prisma 5.22.0
 
TypeScript
v5.3.3 · strict ✔ · MIXED · target: ES2022
 
Build & Deploy
Package Managers: pnpm
Monorepo: npm-workspaces, pnpm-workspaces, turbo
 
Product Purpose Signals
Frameworks: react, nextjs
Evidence: 177
Top Signals:
- [heading] Dashboard (apps/admin/src/pages/Dashboard.tsx)
- [title] Revenue Overview (apps/admin/src/pages/Dashboard.tsx)
- [copy] workspace:* (packages/ui/package.json)
- [copy] ./dist (packages/ui/tsconfig.json)
- [copy] ./src/index.ts (packages/ui/package.json)
- [copy] @repo/config/tsconfig-base.json (packages/ui/tsconfig.json)
- [copy] @repo/ui (packages/ui/package.json)
- [copy] #3b82f6 (apps/admin/src/pages/Dashboard.tsx)
Unknowns:
- No pricing or billing evidence found.
- No integrations/connectors evidence found.
- No route structure evidence found.
 
Security Posture
Lockfile ✖ · .env ✔ · node_modules ✔
 
Platform
Native modules: turbo
 
Code Quality
Files: 36 · Functions: 183 · Avg complexity: 2.62 · Avg length: 21.13 lines
Max nesting: 2 · Circular deps: 0 · Dead code: 0%
God files: apps/admin/src/pages/Products (448 lines)
 
Database Schema
postgresql · 8 models · 1 enum
Models: Address, CartItem, Category, Order, OrderItem (+3 more)
 
Findings (16 errors, 11 warnings)
✖ @types/node is 6 major versions behind (spec: ^20.11.0, latest: 26.6.5).
vibgrate/dependency-major-lag in .
✖ 60% of dependencies are 2+ major versions behind in node-turborepo.
vibgrate/dependency-rot in .
✖ Node.js runtime ">=18.0.0" reached end-of-life on 2025-04-30 (latest: 24.0.0).
vibgrate/runtime-eol in .
✖ vite is 3 major versions behind (spec: ^5.0.12, latest: 8.3.4).
vibgrate/dependency-major-lag in apps/admin
✖ Vite is 3 major versions behind (current: 5.4.21, latest: 8.3.4).
vibgrate/framework-major-lag in apps/admin
✖ @types/node is 6 major versions behind (spec: ^20.11.0, latest: 26.6.5).
vibgrate/dependency-major-lag in apps/api
✖ vitest is 4 major versions behind (spec: ^1.2.1, latest: 5.0.4).
vibgrate/dependency-major-lag in apps/api
✖ Vitest is 4 major versions behind (current: 1.6.1, latest: 5.0.4).
vibgrate/framework-major-lag in apps/api
✖ @types/node is 6 major versions behind (spec: ^20.11.0, latest: 26.6.5).
vibgrate/dependency-major-lag in apps/web
✖ eslint-plugin-react-hooks is 3 major versions behind (spec: ^4.6.0, latest: 7.1.1).
vibgrate/dependency-major-lag in packages/config
✖ 56% of dependencies are 2+ major versions behind in @repo/config.
vibgrate/dependency-rot in packages/config
✖ 75% of dependencies are 2+ major versions behind in @repo/database.
vibgrate/dependency-rot in packages/database
✖ 100% of dependencies are 2+ major versions behind in @repo/types.
vibgrate/dependency-rot in packages/types
✖ vitest is 4 major versions behind (spec: ^1.2.1, latest: 5.0.4).
vibgrate/dependency-major-lag in packages/utils
✖ 67% of dependencies are 2+ major versions behind in @repo/utils.
vibgrate/dependency-rot in packages/utils
✖ Vitest is 4 major versions behind (current: 1.6.1, latest: 5.0.4).
vibgrate/framework-major-lag in packages/utils
⚠ TypeScript is 2 major versions behind (current: 5.9.3, latest: 7.0.2).
vibgrate/framework-major-lag in .
⚠ TypeScript is 2 major versions behind (current: 5.9.3, latest: 7.0.2).
vibgrate/framework-major-lag in apps/admin
⚠ TypeScript is 2 major versions behind (current: 5.9.3, latest: 7.0.2).
vibgrate/framework-major-lag in apps/api
⚠ Next.js is 2 major versions behind (current: 14.2.35, latest: 16.4.0).
vibgrate/framework-major-lag in apps/web
⚠ TypeScript is 2 major versions behind (current: 5.9.3, latest: 7.0.2).
vibgrate/framework-major-lag in apps/web
⚠ TypeScript is 2 major versions behind (current: 5.9.3, latest: 7.0.2).
vibgrate/framework-major-lag in packages/config
⚠ Prisma is 2 major versions behind (current: 5.22.0, latest: 7.10.0).
vibgrate/framework-major-lag in packages/database
⚠ TypeScript is 2 major versions behind (current: 5.9.3, latest: 7.0.2).
vibgrate/framework-major-lag in packages/database
⚠ TypeScript is 2 major versions behind (current: 5.9.3, latest: 7.0.2).
vibgrate/framework-major-lag in packages/types
⚠ TypeScript is 2 major versions behind (current: 5.9.3, latest: 7.0.2).
vibgrate/framework-major-lag in packages/ui
⚠ TypeScript is 2 major versions behind (current: 5.9.3, latest: 7.0.2).
vibgrate/framework-major-lag in packages/utils
 
╭──────────────────────────────────────────╮
│ Top Priority Actions │
╰──────────────────────────────────────────╯
 
1. Upgrade EOL runtime in node-turborepo
End-of-life runtimes no longer receive security patches and block ecosystem upgrades.
./.
>=18.0.0 → 24.0.0 (6 majors behind)
Impact: −10 drift points (runtime & EOL)
 
2. Fix security posture: no lockfile found
Without a lockfile, installs are non-deterministic. Run the install command to generate one and commit it.
./
Missing: package-lock.json, pnpm-lock.yaml, or yarn.lock
 
3. Upgrade Vitest 1.6.1 → 5.0.4 in @repo/api (+2 more)
4 major versions behind. Major framework drift increases breaking change risk and blocks access to security fixes and performance improvements.
./apps/api
Vitest: 1.6.1 → 5.0.4 (4 majors behind)
./packages/utils
Vitest: 1.6.1 → 5.0.4 (4 majors behind)
./apps/admin
Vite: 5.4.21 → 8.3.4 (3 majors behind)
Impact: −5–15 drift points
 
4. Reduce dependency rot in @repo/types (100% severely outdated)
1 of 1 dependencies are 2+ majors behind. Run `npm outdated` and prioritise packages with known CVEs or breaking API changes.
./packages/types
typescript: 5.9.3 → 7.0.2 (2 majors behind)
Impact: −5–10 drift points
 
5. Reduce dependency rot in @repo/database (75% severely outdated)
3 of 4 dependencies are 2+ majors behind. Run `npm outdated` and prioritise packages with known CVEs or breaking API changes.
./packages/database
@prisma/client: 5.22.0 → 7.10.0 (2 majors behind)
prisma: 5.22.0 → 7.10.0 (2 majors behind)
typescript: 5.9.3 → 7.0.2 (2 majors behind)
Impact: −5–10 drift points
 
╭──────────────────────────────────────────╮
│ Architecture Layers │
╰──────────────────────────────────────────╯
 
Archetype: nextjs (80% confidence)
Files classified: 24 (11 unclassified)
Folders classified: 8
apps/admin/src presentation 100% 4 files
apps/admin/src/pages presentation 100% 2 files
apps/api/src/middleware middleware 100% 2 files
apps/api/src/routes routing 100% 2 files
apps/web/src/app presentation 100% 4 files
apps/web/src/app/products presentation 100% 2 files
apps/web/src/app/products/[id] presentation 100% 1 file
packages/ui/src presentation 100% 6 files
Unclassified source (sample): 11
 
presentation 15 files drift ████████████████████ 100 risk high
routing 4 files drift ████████████████████ 100 risk high
middleware 2 files drift ███████▍░░░░░░░░░░░░ 37 risk moderate
config 2 files drift ░░░░░░░░░░░░░░░░░░░░ 0 risk none
shared 1 file drift ████████████████████ 100 risk high
 
╭──────────────────────────────────────────╮
│ DriftScore Summary │
╰──────────────────────────────────────────╯
 
DriftScore: 70/100
Risk Level: HIGH
Projects: 9
Classified: 8 nano · 1 micro · 0 small · 0 standard
Billable: 0.42 · 9 detected → 0.42 billable projects (micro-project pricing)
0.1 micro · 0.32 nano
These fractions add up across repositories, then round down to whole billable projects.
 
Score Breakdown
Runtime: ████████████████████ 100
Frameworks: ███████████▊░░░░░░░░ 59
Dependencies: ██████▌░░░░░░░░░░░░░ 33
EOL Risk: ████████████████████ 100
 
Scanned at 2026-10-14T06:07:19.277Z · 5.4s · 286 files scanned · 56 workspace files · 27 dirs
❯
Press Run to start.