Private module registry `modules.v1` `/versions` returns an empty collection, breaking `terraform init`

Private module registry modules.v1 /versions returns an empty collection, breaking terraform init

Category: Terraform (Terraform - HashiCorp Discuss)

GET /api/registry/v1/modules/{ns}/{name}/{provider}/versions on app.terraform.io returns 200 with an empty list instead of the module’s versions when authenticated with a team token for a non-owners team. With an owners-team token, the identical request returns the full version list.

Request Non-owners team token Owners team token
v1/modules/myorg/mymodule/azurerm/versions 200 {"modules":[]} 200, full version list

The docs say a team token is enough for this — Use artifacts from the private registry states “All users in an organization can view the private registry and use the available providers, Stack component configurations, and modules”, and “Team Token: Allows you to access the private registry of that team’s organization and the registries from any other organizations that have configured sharing.”
The only owners-scoped registry row in the API tokens access-levels chart is “Manage Terraform modules”, which is publishing, not consuming.

More pointedly, the HashiCorp support guide How to create a Terraform API token with read-only access to the private module registry prescribes exactly the setup that fails for me — create a team, “ensure that no permissions are selected”, generate a team token — and states the outcome is “a team API token that can be used to authenticate with and read from the Private Module Registry, suitable for use in CI/CD pipelines”. My team has no organization permissions, which is what that guide asks for.

And with the non-owners token the failure is silent and isolated to that one endpoint: the namespace listing returns every module in the namespace with its versions, the module endpoint returns 0.0.6, and the download endpoint returns a working archivist URL for that same 0.0.6. The credential can fetch the artifact but cannot be told it exists. If it really lacks read access, this should be a 403 — an empty 200 leaves Terraform reporting “invalid response” instead of an auth error.

Every terraform init with the team token fails.

Environment

  • HCP Terraform (app.terraform.io), Free tier
  • Terraform CLI 1.12.2, reproduced on darwin_arm64 locally and linux_amd64 in CI
  • Auth: team API token. GET /api/v2/account/details returns 200 with is-service-account: true
  • Reproduced on two different modules in the same namespace

Reproduction

Identical Authorization: Bearer <team token> on every request, captured 2026-08-24.

# Request (prefix /api/registry/) Actual
1 v1/modules/myorg/mymodule/azurerm/versions 200 {"modules":[]}
2 v1/modules/myorg/mymodule/azurerm 200, id myorg/mymodule/azurerm/0.0.6
3 v1/modules/myorg 200, every module in the namespace listed with versions
4 v1/modules/myorg/mymodule/azurerm/0.0.6/download 204 + x-terraform-get: https://archivist.terraform.io/...
5 private/v1/modules/myorg/mymodule/azurerm/versions 200, full version list
6 private/v1/modules/myorg/mymodule/azurerm/0.0.6/download 404
7 v1/providers/myorg/myprovider/versions 200, full version list
8 v1/modules/myorg/othermodule/azurerm/versions 200 {"modules":[]}

TF_LOG=trace output

Minimal reproduction, a directory containing only:

module "kv" {
  source  = "app.terraform.io/myorg/mymodule/azurerm"
  version = "0.0.6"
}

TF_TOKEN_app_terraform_io=<team token> TF_LOG=trace terraform init, exit 1:

[TRACE] ModuleInstaller: kv is a registry module at app.terraform.io/myorg/mymodule/azurerm
[DEBUG] kv listing available versions of app.terraform.io/myorg/mymodule/azurerm at app.terraform.io
[DEBUG] Service discovery for app.terraform.io at https://app.terraform.io/.well-known/terraform.json
[DEBUG] fetching module versions from "https://app.terraform.io/api/registry/v1/modules/myorg/mymodule/azurerm/versions"
[DEBUG] GET https://app.terraform.io/api/registry/v1/modules/myorg/mymodule/azurerm/versions

Error: Invalid response from remote module registry

Service discovery resolves modules.v1, exactly one request goes out — the endpoint in row 1 — and init fails on the empty collection.

With a CLI host block overriding modules.v1 to .../api/registry/private/v1/modules/, init gets one step further and dies on
the download instead:

Downloading app.terraform.io/myorg/mymodule/azurerm 0.0.6 for kv...
[DEBUG] GET https://app.terraform.io/api/registry/private/v1/modules/myorg/mymodule/azurerm/0.0.6/download
[ERROR] kv ... module "app.terraform.io/myorg/mymodule/azurerm" version "0.0.6" not found

Error: Error accessing remote module registry
Failed to retrieve a download URL for ... version "0.0.6" not found

Same version the unprefixed base downloads fine (row 4).

Why I read this as a server-side bug

  • Request 1 is the only endpoint terraform init can use — service discovery at /.well-known/terraform.json returns "modules.v1":"/api/registry/v1/modules/", the same authenticated or not.
  • Whatever access rule produces the empty list isn’t applied consistently: with the same non-owners token, request 4 returns a live signed download URL for 0.0.6 and request 3 lists the namespace with version numbers attached. Requests 2 and 7 also succeed. /versions is the only endpoint in the chain that behaves as though the token were unauthorized — and it does so silently.
  • Not module-specific (request 8).
  • No client-side workaround. versions works only under the private base and download only under the unprefixed base, and Terraform uses one base URL for both. With a CLI host block pointing modules.v1 at /api/registry/private/v1/modules/, version resolution succeeds (Downloading app.terraform.io/myorg/mymodule/azurerm 0.0.6) then fails with Failed to retrieve a download URL ... not found.

Not a regression

To head off the obvious question: nothing broke. Earlier successful inits against this registry used a different credential type. A team token only became available once SSO was enabled on the org, which created a built-in team I could attach a token to — before that there was no suitable team. The first team token I used behaved this way, and it has never worked.

Questions

  1. Is requiring an owners-team token to resolve private module versions intended? If so, registry/using needs correcting, because it says any team token grants private registry access and scopes only “Manage Terraform modules” to owners.
  2. Either way, should this be a 403 rather than an empty 200? As it stands the CLI cannot distinguish “you may not see this” from “this module has no versions”, and reports “invalid response” for what is really an authorization outcome.
  3. If owners really is required, how are people running least-privilege CI against the private registry? Making every pipeline token an org owner is not a posture I want to adopt.
  4. Has anyone seen /versions return empty while the sibling endpoints on the same base path — including download for the very version it omits — work?

Current workaround is pinning module sources to git refs, which bypasses the registry and loses version constraints.

1 Like