If your platform team publishes shared Terraform modules, you’ve probably run into the same problem eventually: how do you let other repositories pull those modules in CI without handing out a personal access token tied to someone’s GitHub account? PATs work, but they’re a liability — they expire unpredictably, they’re bound to a human, and when that person leaves the team, your pipelines break.
The fix is a GitHub App. It gives your automation its own non-human identity, with scoped permissions and short-lived tokens minted fresh on every workflow run. Here’s how to set the whole pattern up, from module publishing through to consumption in Actions.
Who this is for
This guide is for platform teams that:
- Publish reusable Terraform modules in a private GitHub Enterprise repository
- Consume those modules from other private repositories
- Want CI authentication that isn’t tied to personal accounts
The goal
By the end, you’ll have:
- A module producer repository that publishes pinned module versions
- Consumer repositories that download those modules in GitHub Actions
- Authentication handled by a GitHub App with short-lived tokens
- Azure authentication for Terraform handled via OIDC
Part 1: Publish modules in a dedicated repository
Structure your module repo like this:
modules/terraform_azurerm_<module_name>examples/terraform_azurerm_<module_name>docs/MODULE-CONSUMPTION.mddocs/MODULE-PUBLISHING.md
Always pin module usage to immutable tags — never a moving ref like main:
module "example" { source = "git::https://<your-ghe-host>/<org>/<module-repo>.git//modules/terraform_azurerm_<module_name>?ref=v1.1.3"}
A few publishing practices that save you pain later:
- Use semantic version tags
- Never consume modules from moving refs
- Keep your examples runnable and validated, not just illustrative
Part 2: Why a GitHub App beats a PAT
A PAT represents a user. Whoever generated it, the token acts as them — with their permissions, tied to their account’s lifecycle.
A GitHub App gives you something better:
- A non-human identity built for automation
- Permissions scoped to specific repositories
- Short-lived installation tokens, minted fresh at workflow runtime
The result: no long-lived, user-bound credentials sitting in your CI secrets.
Part 3: Create and install the GitHub App
Create the app in your GitHub Enterprise settings with the minimum permissions it actually needs:
- Repository Contents: Read-only
- Repository Metadata: Read-only
Install it only on the repositories that need it — for module consumption, that’s your module repo (<org>/<module-repo>).
Generate a private key and store both values as secrets in the consumer repository:
GHE_MODULES_APP_IDGHE_MODULES_APP_PRIVATE_KEY
Two things that trip people up here:
- The App ID must be numeric
- The private key must include the full PEM content,
BEGINandENDlines included
Part 4: The consumer workflow pattern
In your consumer workflow, mint a GitHub App token and rewrite git URLs before terraform init runs:
- name: Create GitHub App token for private modules id: ghe_app_token if: ${{ env.GHE_MODULES_APP_ID != '' && env.GHE_MODULES_APP_PRIVATE_KEY != '' }} uses: actions/create-github-app-token@v1 with: app-id: ${{ env.GHE_MODULES_APP_ID }} private-key: ${{ env.GHE_MODULES_APP_PRIVATE_KEY }} owner: ${{ github.repository_owner }} github-api-url: ${{ github.api_url }}- name: Configure git for private modules if: ${{ steps.ghe_app_token.outputs.token != '' }} run: | git config --global url."https://x-access-token:${{ steps.ghe_app_token.outputs.token }}@<your-ghe-host>/".insteadOf "https://<your-ghe-host>/"
From there, Terraform runs exactly as it would otherwise:
terraform initterraform validateterraform planterraform apply # where relevant
Part 5: Validating the setup
After you’ve wired this up, check your workflow logs for:
- Successful GitHub App token creation
- Successful module download from the private module repository
- Terraform plan running cleanly for both test and prod, where applicable
Part 6: Troubleshooting
If module authentication fails, work through this checklist:
- Is the app installed on the module repo?
- Does it have Contents and Metadata read permissions?
- Are
GHE_MODULES_APP_IDandGHE_MODULES_APP_PRIVATE_KEYactually present in the workflow environment?
Wrapping up
This pattern — pinned module versions, a purpose-built GitHub App identity, and short-lived tokens minted at runtime — gives teams a secure, repeatable way to scale Terraform module consumption across repositories, without a single personal credential in sight.






























