Fixing Terraform 'Module Source Not Found' for Private GitHub Registry Access Denied Errors
Threat/Impact Level: HIGH | Exploitability/Downtime Risk: HIGH | Time to Fix: 10 mins
TL;DR
- What broke: Terraform cannot pull a module from a private GitHub repository because the GitHub token is missing, has insufficient scopes (
reporequired), or the.terraformrccredentials block is misconfigured. - How to fix it: Set a valid
GITHUB_TOKENwithreposcope and wire it into.terraformrcas acredentialsblock forgithub.com, or switch to SSH-based module sourcing. - Use the Client-Side Sandbox above to paste your
module {}block and.terraformrc— it will auto-refactor the authentication config without sending your token anywhere.
The Incident (What Does the Error Mean?)
Raw error output from terraform init:
│ Error: Module source not found
│
│ on main.tf line 12, in module "vpc":
│ 12: source = "github.com/your-org/terraform-modules//vpc"
│
│ Could not download module "vpc" (main.tf:12) source code from
│ "https://github.com/your-org/terraform-modules//vpc":
│ error downloading 'https://github.com/your-org/terraform-modules//vpc':
│ /usr/bin/git exited with 128: Cloning into '...'...
│ remote: Repository not found.
│ fatal: repository 'https://github.com/your-org/terraform-modules/' not found
│ (or access denied)
Immediate consequence: terraform init hard-fails. No plan, no apply, no pipeline. Every downstream job — PR validation, staging deploys, production promotions — is blocked until credentials are resolved.
The Attack Vector / Blast Radius
This isn't just a broken build. The failure mode exposes two compounding risks:
Credential sprawl via workarounds. Engineers under pressure hardcode a PAT directly into the
sourceURL (https://token:[email protected]/...). That token now lives in.terraform.lock.hcl, state files, CI logs, and potentially remote state backends — all readable by anyone with S3/GCS read access.Overly-scoped tokens. To "just make it work," teams generate a classic PAT with
repo+admin:org+workflowscopes instead of a fine-grained PAT scoped to a single repository. A leaked token of that scope allows an attacker to exfiltrate all private repos, modify Actions workflows, and pivot into CI/CD secrets.
Blast radius: Private source code exfiltration → secrets harvesting from CI workflows → lateral movement into cloud environments via OIDC or long-lived keys stored in Actions secrets.
How to Fix It (The Solution)
Basic Fix — Environment Variable + .terraformrc
Terraform's Git-based module fetcher respects a credentials block in .terraformrc. Wire your GitHub token in without ever touching the source URL.
Step 1: Generate a fine-grained GitHub PAT with Contents: Read-only on the specific module repository only.
Step 2: Configure .terraformrc
- # No credentials block — Terraform falls back to unauthenticated git clone
+ credentials "github.com" {
+ token = "ghp_YourFineGrainedTokenHere"
+ }
Step 3: Verify your module source format
- source = "https://github.com/your-org/terraform-modules//vpc"
+ source = "github.com/your-org/terraform-modules//vpc?ref=v1.4.0"
Note: Drop the https:// prefix. Terraform's module registry protocol handles the scheme. Always pin a ref.
Enterprise Best Practice — SSH Key Auth + Fine-Grained PAT in CI
For CI/CD pipelines (GitHub Actions, GitLab CI, Atlantis), never store PATs in .terraformrc committed to the repo. Use one of the following patterns:
Pattern A: SSH Deploy Key (recommended for single-repo module sources)
- source = "github.com/your-org/terraform-modules//vpc?ref=v1.4.0"
+ source = "[email protected]:your-org/terraform-modules.git//vpc?ref=v1.4.0"
Add a read-only SSH deploy key to the module repo. Mount the private key in CI via a secret and configure ~/.ssh/config:
+ Host github.com
+ HostName github.com
+ User git
+ IdentityFile ~/.ssh/terraform_deploy_key
+ StrictHostKeyChecking no
Pattern B: GitHub Actions — Token injected at runtime via env
- # Hardcoded token in .terraformrc checked into repo
- credentials "github.com" {
- token = "ghp_hardcodedBADPractice"
- }
+ # .terraformrc uses env var interpolation (Terraform 1.1+)
+ credentials "github.com" {
+ token = "$GITHUB_TOKEN"
+ }
In your GitHub Actions workflow:
+ - name: Terraform Init
+ env:
+ GITHUB_TOKEN: ${{ secrets.TF_MODULE_GITHUB_TOKEN }}
+ run: terraform init
The secret TF_MODULE_GITHUB_TOKEN is a fine-grained PAT scoped only to the module repository with Contents: Read-only. Zero other permissions.
💡 Tired of pasting proprietary configs into ChatGPT? Generic AI tools log your company's ARNs, DB strings, and private keys. StackEngine is a zero-backend, pure Client-Side WASM utility. Drop your failing config into the sandbox above. We redact your secrets locally in the browser and auto-generate the refactored code using your own API key.
Prevention in CI/CD
1. Checkov — Detect hardcoded tokens in Terraform files
Add to your pre-commit or CI pipeline:
checkov -d . --check CKV_SECRET_6
Checkov's secrets scanning will flag any ghp_, github_pat_, or bearer token patterns in .tf and .terraformrc files.
2. OPA/Conftest policy — Enforce ref pinning on all module sources
package terraform.modules
deny[msg] {
module := input.configuration.root_module.module_calls[_]
not contains(module.source, "?ref=")
msg := sprintf("Module '%v' must pin a ref tag. Unpinned modules are a supply chain risk.", [module.source])
}
Run with:
conftest test plan.json --policy ./policies/
3. tflint — Validate module source format before terraform init runs
tflint --enable-rule=terraform_module_pinned_source
4. Rotate and audit PATs quarterly. Use GitHub's GET /orgs/{org}/credential-authorizations API to enumerate all active tokens and their last-used timestamps. Revoke anything idle for 30+ days.
5. Prefer Terraform Private Registry (Terraform Cloud/Enterprise) over raw GitHub sources for shared modules. The registry enforces versioning, access control, and audit logs natively — removing the GitHub auth problem entirely.